diff --git a/src/main/asciidoc/repositories.adoc b/src/main/asciidoc/repositories.adoc index e774a194f..14f5d28d4 100644 --- a/src/main/asciidoc/repositories.adoc +++ b/src/main/asciidoc/repositories.adoc @@ -864,6 +864,70 @@ class AnAggregateRoot { The methods will be called every time one of a Spring Data repository's `save(…)` methods is called. +[[core.nullability-validation]] +== Null-safety + +Repository methods let you improve null-safety to deal with `null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes your applications safer through clean nullability declarations, expressing "value or no value" semantics without paying the cost of a wrapper like `Optional`. + +You can express null-safe repository methods by using Spring Frameworks annotations. They provide a tooling-friendly approach and opt-in for `null` checks during runtime: + +* `@NonNullApi` annotations at package level declare non-null as the default behavior + +* `@Nullable` annotations where specific parameters or return values can be `null`. + +Both annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=305[JSR-305] meta-annotations (a dormant JSR but supported by tools like IDEA, Eclipse, Findbugs, etc.) to provide useful warnings to Java developers. + +Make sure to include a JAR file containing JSR-305's `@Nonnull` annotation on your class path if you intend to use own meta-annotations. + +NOTE: Invocations of repository query methods in the scope of null-declarations, either declared on package-level or with Kotlin, are validated during runtime. Passing a `null` value to a query method parameter that is not-nullable is rejected with an exception. A query method that yields no result and is not-nullable throws `EmptyResultDataAccessException` instead of returning `null`. + +.Activating non-null defaults for a package +==== +[source, java] +---- +@org.springframework.lang.NonNullApi +package com.example; +---- +==== + +.Declaring nullability for parameters and return values +==== +[source, java] +---- +package com.example; <1> + +interface UserRepository extends Repository { + + List findByLastname(@Nullable String firstname); <2> + + @Nullable + User findByFirstnameAndLastname(String firstname, String lastname); <3> +} +---- +<1> `@NonNullApi` on package-level declares that all API within this package +defaults to non-null. +<2> `@Nullable` allows `null` usage on particular parameters. Each nullable parameter +must be annotated. +<3> Methods that may return `null` are annotated with `@Nullable`. +==== + +If you declare your repository interfaces with Kotlin, then you can use Kotlin's https://kotlinlang.org/docs/reference/null-safety.html[null-safety] to express nullability. + +.Declaring nullability for parameters and return values in Kotlin +==== +[source, java] +---- +interface UserRepository : Repository { + + fun findByLastname(username: String?): List + + fun findByFirstnameAndLastname(firstname: String, lastname: String): User? +} +---- +==== + +NOTE: Kotlin code compiles to bytecode which does not express nullability declarations using method signatures but rather compiled-in metadata. Make sure to include `kotlin-reflect` to enable introspection of Kotlin's nullability declarations. + [[core.extensions]] == Spring Data extensions diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java b/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java new file mode 100644 index 000000000..8ab571a98 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java @@ -0,0 +1,163 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.core.support; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +import java.lang.annotation.ElementType; +import java.lang.reflect.Method; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.data.util.NullableUtils; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; + +/** + * Interceptor enforcing required return value and method parameter constraints declared on repository query methods. + * Supports Kotlin nullability markers and JSR-305 Non-null annotations. + * + * @author Mark Paluch + * @since 2.0 + * @see javax.annotation.Nonnull + * @see ReflectionUtils#isNullable(MethodParameter) + * @see NullableUtils + */ +public class MethodInvocationValidator implements MethodInterceptor { + + private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); + private final Map nullabilityCache = new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); + + /** + * Returns {@literal true} if the {@code repositoryInterface} is supported by this interceptor. + * + * @param repositoryInterface the interface class. + * @return {@literal true} if the {@code repositoryInterface} is supported by this interceptor. + */ + public static boolean supports(Class repositoryInterface) { + + return ReflectionUtils.isKotlinClass(repositoryInterface) + || NullableUtils.isNonNull(repositoryInterface, ElementType.METHOD) + || NullableUtils.isNonNull(repositoryInterface, ElementType.PARAMETER); + } + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + @Nullable + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + + Method method = invocation.getMethod(); + Nullability nullability = nullabilityCache.get(method); + + if (nullability == null) { + + nullability = Nullability.of(method, discoverer); + nullabilityCache.put(method, nullability); + } + + Object[] arguments = invocation.getArguments(); + + for (int i = 0; i < method.getParameterCount(); i++) { + + if (nullability.isNullableParameter(i)) { + continue; + } + + if (arguments.length < i || arguments[i] == null) { + throw new IllegalArgumentException( + String.format("Parameter %s in %s.%s must not be null!", nullability.getMethodParameterName(i), + ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); + } + } + + Object result = invocation.proceed(); + + if (result == null && !nullability.isNullableReturn()) { + throw new EmptyResultDataAccessException("Result must not be null!", 1); + } + + return result; + } + + @Value + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + static class Nullability { + + boolean nullableReturn; + boolean[] nullableParameters; + MethodParameter methodParameters[]; + + static Nullability of(Method method, ParameterNameDiscoverer discoverer) { + + boolean nullableReturn = isNullableParameter(new MethodParameter(method, -1)); + boolean[] nullableParameters = new boolean[method.getParameterCount()]; + MethodParameter methodParameters[] = new MethodParameter[method.getParameterCount()]; + + for (int i = 0; i < method.getParameterCount(); i++) { + + MethodParameter parameter = new MethodParameter(method, i); + parameter.initParameterNameDiscovery(discoverer); + nullableParameters[i] = isNullableParameter(parameter); + methodParameters[i] = parameter; + } + + return new Nullability(nullableReturn, nullableParameters, methodParameters); + } + + String getMethodParameterName(int index) { + + String parameterName = methodParameters[index].getParameterName(); + + if (parameterName == null) { + parameterName = String.format("of type %s at index %d", + ClassUtils.getShortName(methodParameters[index].getParameterType()), index); + } + + return parameterName; + } + + boolean isNullableReturn() { + return nullableReturn; + } + + boolean isNullableParameter(int index) { + return nullableParameters[index]; + } + + private static boolean isNullableParameter(MethodParameter parameter) { + + return requiresNoValue(parameter) || NullableUtils.isExplicitNullable(parameter) + || (ReflectionUtils.isKotlinClass(parameter.getDeclaringClass()) && ReflectionUtils.isNullable(parameter)); + } + + private static boolean requiresNoValue(MethodParameter parameter) { + return parameter.getParameterType().equals(Void.class) || parameter.getParameterType().equals(Void.TYPE); + } + } +} 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 ec2ecf866..738389e2c 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 @@ -122,7 +122,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, private EvaluationContextProvider evaluationContextProvider; private BeanFactory beanFactory; - private QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); + private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); @SuppressWarnings("null") public RepositoryFactorySupport() { @@ -302,6 +302,10 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, result.setTarget(target); result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class); + if (MethodInvocationValidator.supports(repositoryInterface)) { + result.addAdvice(new MethodInvocationValidator()); + } + result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); @@ -329,7 +333,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, /** * Returns the {@link RepositoryInformation} for the given {@link RepositoryMetadata} and custom * {@link RepositoryFragments}. - * + * * @param metadata must not be {@literal null}. * @param fragments must not be {@literal null}. * @return will never be {@literal null}. @@ -341,7 +345,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, /** * Returns the {@link RepositoryComposition} for the given {@link RepositoryMetadata} and extra * {@link RepositoryFragments}. - * + * * @param metadata must not be {@literal null}. * @param fragments must not be {@literal null}. * @return will never be {@literal null}. @@ -543,6 +547,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) */ + @Override @Nullable public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable { @@ -579,6 +584,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, } } + /** * Method interceptor that calls methods on the {@link RepositoryComposition}. * @@ -622,11 +628,12 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, /** * All {@link QueryMethod}s. */ - private List queryMethods = new ArrayList<>(); + private final List queryMethods = new ArrayList<>(); /* (non-Javadoc) * @see org.springframework.data.repository.core.support.QueryCreationListener#onCreation(org.springframework.data.repository.query.RepositoryQuery) */ + @Override public void onCreation(RepositoryQuery query) { this.queryMethods.add(query.getQueryMethod()); } diff --git a/src/main/java/org/springframework/data/util/NullableUtils.java b/src/main/java/org/springframework/data/util/NullableUtils.java new file mode 100644 index 000000000..97a32b411 --- /dev/null +++ b/src/main/java/org/springframework/data/util/NullableUtils.java @@ -0,0 +1,274 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util; + +import lombok.experimental.UtilityClass; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.NonNullApi; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; + +/** + * Utility methods to introspect nullability rules declared in packages, classes and methods. + *

+ * Nullability rules are declared using {@link NonNullApi} and {@link Nullable} and JSR-305 + * {@link javax.annotation.Nonnull} annotations. By default (no annotation use), a package and its types are considered + * allowing {@literal null} values in return values and method parameters. Nullability rules are expressed by annotating + * a package with a JSR-305 meta annotation such as Spring's {@link NonNullApi}. All types of the package inherit the + * package rule. Subpackages do not inherit nullability rules and must be annotated themself. + * + *

+ * @org.springframework.lang.NonNullApi
+ * package com.example;
+ * 
+ * + * {@link Nullable} selectively permits {@literal null} values for method return values or method parameters by + * annotating the method respectively the parameters: + * + *
+ * public class ExampleClass {
+ *
+ * 	String shouldNotReturnNull(@Nullable String acceptsNull, String doesNotAcceptNull) {
+ * 		// …
+ * 	}
+ *
+ * 	@Nullable
+ * 	String nullableReturn(String parameter) {
+ * 		// …
+ * 	}
+ * }
+ * 
+ *

+ * {@link javax.annotation.Nonnull} is suitable for composition of meta-annotations and expresses via + * {@link Nonnull#when()} in which cases non-nullability is applicable. + * + * @author Mark Paluch + * @since 2.0 + * @see NonNullApi + * @see Nullable + * @see Nonnull + */ +@UtilityClass +public class NullableUtils { + + private static final String NON_NULL_CLASS_NAME = "javax.annotation.Nonnull"; + private static final String TYPE_QUALIFIER_CLASS_NAME = "javax.annotation.meta.TypeQualifierDefault"; + + private static final Optional> NON_NULL_ANNOTATION_CLASS = findClass(NON_NULL_CLASS_NAME); + + private static final Set> NULLABLE_ANNOTATIONS = findClasses(Nullable.class.getName()); + private static final Set> NON_NULLABLE_ANNOTATIONS = findClasses("reactor.util.lang.NonNullApi", + NonNullApi.class.getName()); + + private static final Set WHEN_NULLABLE = new HashSet<>(Arrays.asList("UNKNOWN", "MAYBE", "NEVER")); + private static final Set WHEN_NON_NULLABLE = new HashSet<>(Collections.singletonList("ALWAYS")); + + /** + * Determine whether {@link ElementType} in the scope of {@link Method} requires non-{@literal null} values. + * Non-nullability rules are discovered from class and package annotations. Non-null is applied when + * {@link javax.annotation.Nonnull} is set to {@link javax.annotation.meta.When#ALWAYS}. + * + * @param type the class to inspect. + * @param elementType the element type. + * @return {@literal true} if {@link ElementType} allows {@literal null} values by default. + * @see #isNonNull(Annotation, ElementType) + */ + public boolean isNonNull(Method method, ElementType elementType) { + return isNonNull(method.getDeclaringClass(), elementType) || isNonNull((AnnotatedElement) method, elementType); + } + + /** + * Determine whether {@link ElementType} in the scope of {@code type} requires non-{@literal null} values. + * Non-nullability rules are discovered from class and package annotations. Non-null is applied when + * {@link javax.annotation.Nonnull} is set to {@link javax.annotation.meta.When#ALWAYS}. + * + * @param type the class to inspect. + * @param elementType the element type. + * @return {@literal true} if {@link ElementType} allows {@literal null} values by default. + * @see #isNonNull(Annotation, ElementType) + */ + public boolean isNonNull(Class type, ElementType elementType) { + return isNonNull(type.getPackage(), elementType) || isNonNull((AnnotatedElement) type, elementType); + } + + /** + * Determine whether {@link ElementType} in the scope of {@link AnnotatedElement} requires non-{@literal null} values. + * This method determines default {@link javax.annotation.Nonnull nullability} rules from the annotated element + * + * @param element the scope of declaration, may be a {@link Package}, {@link Class}, or + * {@link java.lang.reflect.Method}. + * @param elementType the element type. + * @return {@literal true} if {@link ElementType} allows {@literal null} values by default. + */ + public boolean isNonNull(AnnotatedElement element, ElementType elementType) { + + for (Annotation annotation : element.getAnnotations()) { + + boolean isNonNull = NON_NULL_ANNOTATION_CLASS.isPresent() ? isNonNull(annotation, elementType) + : NON_NULLABLE_ANNOTATIONS.contains(annotation.annotationType()); + + if (isNonNull) { + return true; + } + } + + return false; + } + + private static boolean isNonNull(Annotation annotation, ElementType elementType) { + + if (!NON_NULL_ANNOTATION_CLASS.isPresent()) { + return false; + } + + Class annotationClass = NON_NULL_ANNOTATION_CLASS.get(); + + if (annotation.annotationType().equals(annotationClass)) { + return true; + } + + if (!AnnotationUtils.isAnnotationMetaPresent(annotation.annotationType(), annotationClass) + || !isNonNull(annotation)) { + return false; + } + + return test(annotation, TYPE_QUALIFIER_CLASS_NAME, "value", + (ElementType[] o) -> Arrays.binarySearch(o, elementType) >= 0); + } + + /** + * Determine whether a {@link MethodParameter} is explicitly annotated to be considered nullable. Nullability rules + * are discovered from method and parameter annotations. A {@link MethodParameter} is considered nullable when + * {@link javax.annotation.Nonnull} is set to one of {@link javax.annotation.meta.When#UNKNOWN}, + * {@link javax.annotation.meta.When#NEVER}, or {@link javax.annotation.meta.When#MAYBE}. + * + * @param methodParameter the method parameter to inspect. + * @return {@literal true} if the parameter is nullable, {@literal false} otherwise. + */ + public static boolean isExplicitNullable(MethodParameter methodParameter) { + + if (methodParameter.getParameterIndex() == -1) { + return isExplicitNullable(methodParameter.getMethodAnnotations()); + } + + return isExplicitNullable(methodParameter.getParameterAnnotations()); + } + + private static boolean isExplicitNullable(Annotation[] annotations) { + + for (Annotation annotation : annotations) { + + boolean isNullable = NON_NULL_ANNOTATION_CLASS.isPresent() ? isNullable(annotation) + : NULLABLE_ANNOTATIONS.contains(annotation.annotationType()); + + if (isNullable) { + return true; + } + } + + return false; + } + + /** + * Introspect {@link Annotation} for being either a meta-annotation composed from {@link Nonnull} or {@link Nonnull} + * itself expressing non-nullability. + * + * @param annotation + * @return {@literal true} if the annotation expresses non-nullability. + */ + private static boolean isNonNull(Annotation annotation) { + return test(annotation, NON_NULL_CLASS_NAME, "when", o -> WHEN_NON_NULLABLE.contains(o.toString())); + } + + /** + * Introspect {@link Annotation} for being either a meta-annotation composed from {@link Nonnull} or {@link Nonnull} + * itself expressing nullability. + * + * @param annotation + * @return {@literal true} if the annotation expresses nullability. + */ + private static boolean isNullable(Annotation annotation) { + return test(annotation, NON_NULL_CLASS_NAME, "when", o -> WHEN_NULLABLE.contains(o.toString())); + } + + @SuppressWarnings("unchecked") + private static boolean test(Annotation annotation, String metaAnnotationName, String attribute, + Predicate filter) { + + if (annotation.annotationType().getName().equals(metaAnnotationName)) { + + Map attributes = AnnotationUtils.getAnnotationAttributes(annotation); + + return !attributes.isEmpty() && filter.test((T) attributes.get(attribute)); + } + + MultiValueMap attributes = AnnotatedElementUtils + .getAllAnnotationAttributes(annotation.annotationType(), metaAnnotationName); + + if (attributes == null || attributes.isEmpty()) { + return false; + } + + List elementTypes = attributes.get(attribute); + + for (Object value : elementTypes) { + + if (filter.test((T) value)) { + return true; + } + } + return false; + } + + private static Set> findClasses(String... classNames) { + + return Arrays.stream(classNames) // + .map(NullableUtils::findClass) // + .filter(Optional::isPresent) // + .map(Optional::get) // + .collect(Collectors.toSet()); + } + + @SuppressWarnings("unchecked") + private static Optional> findClass(String className) { + + try { + return Optional.of((Class) ClassUtils.forName(className, NullableUtils.class.getClassLoader())); + } catch (ClassNotFoundException e) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index 7e798053c..908b0c89d 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -15,6 +15,9 @@ */ package org.springframework.data.util; +import kotlin.reflect.KFunction; +import kotlin.reflect.KType; +import kotlin.reflect.jvm.ReflectJvmMapping; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.experimental.UtilityClass; @@ -31,6 +34,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; import org.springframework.beans.BeanUtils; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; @@ -355,4 +359,34 @@ public class ReflectionUtils { .map(Annotation::annotationType) // .anyMatch(annotation -> annotation.getName().equals("kotlin.Metadata")); } + + /** + * Returns {@literal} whether the given {@link MethodParameter} is nullable. Nullable parameters are reference types + * and ones that are defined in Kotlin as such. + * + * @return {@literal true} if {@link MethodParameter} is nullable. + * @since 2.0 + */ + public static boolean isNullable(MethodParameter parameter) { + + if (Void.class.equals(parameter.getParameterType()) || Void.TYPE.equals(parameter.getParameterType())) { + return true; + } + + if (isKotlinClass(parameter.getDeclaringClass())) { + + KFunction kotlinFunction = ReflectJvmMapping.getKotlinFunction(parameter.getMethod()); + + if (kotlinFunction == null) { + throw new IllegalArgumentException(String.format("Cannot resolve %s to a Kotlin function!", parameter)); + } + + KType type = parameter.getParameterIndex() == -1 ? kotlinFunction.getReturnType() + : kotlinFunction.getParameters().get(parameter.getParameterIndex() + 1).getType(); + + return type.isMarkedNullable(); + } + + return !parameter.getParameterType().isPrimitive(); + } } diff --git a/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java index bdd65fe7a..4af3a9b86 100644 --- a/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/ReactiveWrapperRepositoryFactorySupportUnitTests.java @@ -20,7 +20,6 @@ import static org.mockito.Mockito.*; import io.reactivex.Completable; import io.reactivex.Maybe; -import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import rx.Single; @@ -33,6 +32,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.reactivestreams.Publisher; import org.springframework.data.repository.Repository; import org.springframework.data.repository.reactive.ReactiveSortingRepository; @@ -67,10 +67,12 @@ public class ReactiveWrapperRepositoryFactorySupportUnitTests { verify(backingRepo, times(0)).findById(1); } - @Test // DATACMNS-836 + @Test // DATACMNS-836, DATACMNS-1154 public void callsRxJava1MethodOnBaseImplementationWithExactArguments() { Serializable id = 1L; + when(backingRepo.existsById(id)).thenReturn(Mono.just(true)); + RxJava1ConvertingRepository repository = factory.getRepository(RxJava1ConvertingRepository.class); repository.existsById(id); repository.existsById((Long) id); @@ -78,10 +80,12 @@ public class ReactiveWrapperRepositoryFactorySupportUnitTests { verify(backingRepo, times(2)).existsById(id); } - @Test // DATACMNS-836, DATACMNS-1063 + @Test // DATACMNS-836, DATACMNS-1063, DATACMNS-1154 @SuppressWarnings("unchecked") public void callsRxJava1MethodOnBaseImplementationWithTypeConversion() { + when(backingRepo.existsById(any(Publisher.class))).thenReturn(Mono.just(true)); + Single ids = Single.just(1L); RxJava1ConvertingRepository repository = factory.getRepository(RxJava1ConvertingRepository.class); @@ -90,20 +94,23 @@ public class ReactiveWrapperRepositoryFactorySupportUnitTests { verify(backingRepo, times(1)).existsById(any(Publisher.class)); } - @Test // DATACMNS-988 + @Test // DATACMNS-988, DATACMNS-1154 public void callsRxJava2MethodOnBaseImplementationWithExactArguments() { Long id = 1L; + when(backingRepo.findById(id)).thenReturn(Mono.just(true)); + RxJava2ConvertingRepository repository = factory.getRepository(RxJava2ConvertingRepository.class); repository.findById(id); verify(backingRepo, times(1)).findById(id); } - @Test // DATACMNS-988 + @Test // DATACMNS-988, DATACMNS-1154 public void callsRxJava2MethodOnBaseImplementationWithTypeConversion() { Serializable id = 1L; + when(backingRepo.deleteById(id)).thenReturn(Mono.empty()); RxJava2ConvertingRepository repository = factory.getRepository(RxJava2ConvertingRepository.class); repository.deleteById(id); diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index 269dfa1d4..dbc27013f 100755 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -39,7 +39,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.SpringVersion; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -55,6 +57,7 @@ import org.springframework.data.repository.core.support.RepositoryComposition.Re import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.sample.User; import org.springframework.data.util.Version; +import org.springframework.lang.Nullable; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; import org.springframework.test.util.ReflectionTestUtils; @@ -142,6 +145,8 @@ public class RepositoryFactorySupportUnitTests { CustomRepository repository = factory.getRepository(CustomRepository.class); Pageable pageable = PageRequest.of(0, 10); + + when(backingRepo.findAll(pageable)).thenReturn(new PageImpl<>(Collections.emptyList())); repository.findAll(pageable); verify(backingRepo, times(1)).findAll(pageable); @@ -299,6 +304,52 @@ public class RepositoryFactorySupportUnitTests { verifyZeroInteractions(backingRepo); } + @Test // DATACMNS-1154 + public void considersRequiredReturnValue() { + + KotlinUserRepository repository = factory.getRepository(KotlinUserRepository.class); + + assertThatThrownBy(() -> repository.findById("")).isInstanceOf(EmptyResultDataAccessException.class) + .hasMessageContaining("Result must not be null!"); + assertThat(repository.findByUsername("")).isNull(); + } + + @Test // DATACMNS-1154 + public void considersRequiredParameter() { + + ObjectRepository repository = factory.getRepository(ObjectRepository.class); + + assertThatThrownBy(() -> repository.findByClass(null)).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must not be null!"); + } + + @Test // DATACMNS-1154 + public void shouldAllowVoidMethods() { + + ObjectRepository repository = factory.getRepository(ObjectRepository.class, backingRepo); + + repository.deleteAll(); + + verify(backingRepo).deleteAll(); + } + + @Test // DATACMNS-1154 + public void considersRequiredKotlinParameter() { + + KotlinUserRepository repository = factory.getRepository(KotlinUserRepository.class); + + assertThatThrownBy(() -> repository.findById(null)).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must not be null!"); + } + + @Test // DATACMNS-1154 + public void considersRequiredKotlinNullableParameter() { + + KotlinUserRepository repository = factory.getRepository(KotlinUserRepository.class); + + assertThat(repository.findByOptionalId(null)).isNull(); + } + private ConvertingRepository prepareConvertingRepository(final Object expectedValue) { when(factory.queryOne.execute(Mockito.any(Object[].class))).then(invocation -> { @@ -330,10 +381,13 @@ public class RepositoryFactorySupportUnitTests { interface ObjectRepository extends Repository, ObjectRepositoryCustom { + @Nullable Object findByClass(Class clazz); + @Nullable Object findByFoo(); + @Nullable Object save(Object entity); static String staticMethod() { @@ -347,7 +401,10 @@ public class RepositoryFactorySupportUnitTests { interface ObjectRepositoryCustom { + @Nullable Object findById(Object id); + + void deleteAll(); } interface PlainQueryCreationListener extends QueryCreationListener { diff --git a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java new file mode 100644 index 000000000..1f5a50d54 --- /dev/null +++ b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.annotation.ElementType; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.data.util.nonnull.NullableAnnotatedType; +import org.springframework.data.util.nonnull.packagelevel.NonNullOnPackage; +import org.springframework.data.util.nonnull.type.Jsr305NonnullAnnotatedType; +import org.springframework.data.util.nonnull.type.NonAnnotatedType; +import org.springframework.data.util.nonnull.type.NonNullableParameters; +import org.springframework.util.ReflectionUtils; + +/** + * Unit tests for {@link NullableUtils}. + * + * @author Mark Paluch + */ +public class NullableUtilsUnitTests { + + @Test // DATACMNS-1154 + public void packageAnnotatedShouldConsiderNonNullAnnotation() { + + Method method = ReflectionUtils.findMethod(NonNullOnPackage.class, "nonNullReturnValue"); + + assertThat(NullableUtils.isNonNull(method, ElementType.METHOD)).isTrue(); + assertThat(NullableUtils.isNonNull(method, ElementType.PARAMETER)).isTrue(); + assertThat(NullableUtils.isNonNull(method, ElementType.PACKAGE)).isFalse(); + } + + @Test // DATACMNS-1154 + public void packageAnnotatedShouldConsiderNonNullAnnotationForClass() { + + assertThat(NullableUtils.isNonNull(NonNullOnPackage.class, ElementType.PARAMETER)).isTrue(); + assertThat(NullableUtils.isNonNull(NonNullOnPackage.class, ElementType.PACKAGE)).isFalse(); + } + + @Test // DATACMNS-1154 + public void packageAnnotatedShouldConsiderNonNullAnnotationForMethod() { + + assertThat(NullableUtils.isNonNull(NonNullOnPackage.class, ElementType.PARAMETER)).isTrue(); + assertThat(NullableUtils.isNonNull(NonNullOnPackage.class, ElementType.PACKAGE)).isFalse(); + } + + @Test // DATACMNS-1154 + public void shouldConsiderJsr305NonNullParameters() { + + assertThat(NullableUtils.isNonNull(NonNullableParameters.class, ElementType.PARAMETER)).isTrue(); + assertThat(NullableUtils.isNonNull(NonNullableParameters.class, ElementType.FIELD)).isFalse(); + } + + @Test // DATACMNS-1154 + public void shouldConsiderJsr305NonNullAnnotation() { + + assertThat(NullableUtils.isNonNull(Jsr305NonnullAnnotatedType.class, ElementType.PARAMETER)).isTrue(); + assertThat(NullableUtils.isNonNull(Jsr305NonnullAnnotatedType.class, ElementType.FIELD)).isTrue(); + } + + @Test // DATACMNS-1154 + public void shouldConsiderNonAnnotatedTypeNullable() { + + assertThat(NullableUtils.isNonNull(NonAnnotatedType.class, ElementType.PARAMETER)).isFalse(); + assertThat(NullableUtils.isNonNull(NonAnnotatedType.class, ElementType.FIELD)).isFalse(); + } + + @Test // DATACMNS-1154 + public void shouldConsiderParametersWithoutNullableAnnotation() { + + Method method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "nonNullMethod", String.class); + + MethodParameter returnValue = new MethodParameter(method, -1); + MethodParameter parameter = new MethodParameter(method, 0); + + assertThat(NullableUtils.isExplicitNullable(returnValue)).isFalse(); + assertThat(NullableUtils.isExplicitNullable(parameter)).isFalse(); + } + + @Test // DATACMNS-1154 + public void shouldConsiderParametersNullableAnnotation() { + + Method method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "nullableReturn"); + + assertThat(NullableUtils.isExplicitNullable(new MethodParameter(method, -1))).isTrue(); + } + + @Test // DATACMNS-1154 + public void shouldConsiderParametersJsr305NullableMetaAnnotation() { + + Method method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jsr305NullableReturn"); + + assertThat(NullableUtils.isExplicitNullable(new MethodParameter(method, -1))).isTrue(); + } + + @Test // DATACMNS-1154 + public void shouldConsiderParametersJsr305NonnullAnnotation() { + + Method method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jsr305NullableReturnWhen"); + + assertThat(NullableUtils.isExplicitNullable(new MethodParameter(method, -1))).isTrue(); + } +} diff --git a/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java b/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java index d89d77a03..f19d9046c 100755 --- a/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java @@ -23,6 +23,8 @@ import java.lang.reflect.Field; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.sample.User; import org.springframework.data.util.ReflectionUtils.DescribedFieldFilter; import org.springframework.util.ReflectionUtils.FieldFilter; @@ -106,6 +108,42 @@ public class ReflectionUtilsUnitTests { assertThat(ReflectionUtils.findConstructor(ConstructorDetection.class, null, "test")).isNotPresent(); } + @Test // DATACMNS-1154 + public void discoversNoReturnType() throws Exception { + + MethodParameter parameter = new MethodParameter(DummyInterface.class.getDeclaredMethod("noReturnValue"), -1); + assertThat(ReflectionUtils.isNullable(parameter)).isTrue(); + } + + @Test // DATACMNS-1154 + public void discoversNullableReturnType() throws Exception { + + MethodParameter parameter = new MethodParameter(DummyInterface.class.getDeclaredMethod("nullableReturnValue"), -1); + assertThat(ReflectionUtils.isNullable(parameter)).isTrue(); + } + + @Test // DATACMNS-1154 + public void discoversNonNullableReturnType() throws Exception { + + MethodParameter parameter = new MethodParameter(DummyInterface.class.getDeclaredMethod("mandatoryReturnValue"), -1); + assertThat(ReflectionUtils.isNullable(parameter)).isFalse(); + } + + @Test // DATACMNS-1154 + public void discoversNullableParameter() throws Exception { + + MethodParameter parameter = new MethodParameter( + DummyInterface.class.getDeclaredMethod("nullableParameter", User.class), 0); + assertThat(ReflectionUtils.isNullable(parameter)).isTrue(); + } + + @Test // DATACMNS-1154 + public void discoversNonNullablePrimitiveParameter() throws Exception { + + MethodParameter parameter = new MethodParameter(DummyInterface.class.getDeclaredMethod("primitive", int.class), 0); + assertThat(ReflectionUtils.isNullable(parameter)).isFalse(); + } + static class Sample { public String field; diff --git a/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java new file mode 100644 index 000000000..fc1f064ab --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util.nonnull; + +import javax.annotation.meta.When; + +import org.springframework.lang.Nullable; + +/** + * @author Mark Paluch + */ +public interface NullableAnnotatedType { + + String nonNullMethod(String parameter); + + @Nullable + String nullableReturn(); + + @javax.annotation.Nullable + String jsr305NullableReturn(); + + @javax.annotation.Nonnull(when = When.MAYBE) + String jsr305NullableReturnWhen(); +} diff --git a/src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java b/src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java new file mode 100644 index 000000000..b119c30ff --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util.nonnull.packagelevel; + +/** + * @author Mark Paluch + */ +public interface NonNullOnPackage { + + String nonNullReturnValue(); +} diff --git a/src/test/java/org/springframework/data/util/nonnull/packagelevel/package-info.java b/src/test/java/org/springframework/data/util/nonnull/packagelevel/package-info.java new file mode 100644 index 000000000..02401cf3c --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/packagelevel/package-info.java @@ -0,0 +1,3 @@ + +@org.springframework.lang.NonNullApi +package org.springframework.data.util.nonnull.packagelevel; diff --git a/src/test/java/org/springframework/data/util/nonnull/type/CustomAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/type/CustomAnnotatedType.java new file mode 100644 index 000000000..c87566b98 --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/type/CustomAnnotatedType.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util.nonnull.type; + +/** + * @author Mark Paluch + */ +@CustomNonNullAnnotation +public interface CustomAnnotatedType {} diff --git a/src/test/java/org/springframework/data/util/nonnull/type/CustomNonNullAnnotation.java b/src/test/java/org/springframework/data/util/nonnull/type/CustomNonNullAnnotation.java new file mode 100644 index 000000000..c3e1424e4 --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/type/CustomNonNullAnnotation.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util.nonnull.type; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; + +/** + * @author Mark Paluch + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Nonnull +@TypeQualifierDefault({ ElementType.FIELD }) +@interface CustomNonNullAnnotation { +} diff --git a/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java new file mode 100644 index 000000000..ee3ea09f2 --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util.nonnull.type; + +import javax.annotation.Nonnull; + +/** + * @author Mark Paluch + */ +@Nonnull +public interface Jsr305NonnullAnnotatedType {} diff --git a/src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java new file mode 100644 index 000000000..75915eb5d --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java @@ -0,0 +1,21 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util.nonnull.type; + +/** + * @author Mark Paluch + */ +public class NonAnnotatedType {} diff --git a/src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java b/src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java new file mode 100644 index 000000000..e9965c4c5 --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util.nonnull.type; + +import javax.annotation.ParametersAreNonnullByDefault; + +/** + * @author Mark Paluch + */ +@ParametersAreNonnullByDefault +public interface NonNullableParameters {} diff --git a/src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt b/src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt new file mode 100644 index 000000000..d28c8dd6d --- /dev/null +++ b/src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.core.support + +import org.springframework.data.repository.Repository +import org.springframework.data.repository.sample.User + +/** + * @author Mark Paluch + */ +interface KotlinUserRepository : Repository { + + fun findByUsername(username: String): User? + + fun findById(username: String): User + + fun findByOptionalId(username: String?): User? +} \ No newline at end of file diff --git a/src/test/kotlin/org/springframework/data/util/DummyInterface.kt b/src/test/kotlin/org/springframework/data/util/DummyInterface.kt new file mode 100644 index 000000000..e6eecce95 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/util/DummyInterface.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.util + +import org.springframework.data.repository.sample.User + +/** + * @author Mark Paluch + */ +interface DummyInterface { + + fun noReturnValue() + + fun nullableReturnValue(): User? + + fun mandatoryReturnValue(): User + + fun nullableParameter(user: User?) + + fun primitive(number: Int) +} \ No newline at end of file