From fcf97e1e71ccc79b1d3a3d5895c66a9649244b52 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 12 Sep 2017 10:14:56 +0200 Subject: [PATCH] DATACMNS-1157 - Enforce nullable/non-null API constraints on repository query methods. We now validate query method invocations to check method parameters whether they accept null values and reject execution if null values are not supported. We derive nullability support from repository interfaces declared using Kotlin and Spring's NonNullApi/Nullable annotations. We also check whether a query method can return null. If a query method returns null which isn't supposed to do so, then we throw EmptyResultDataAccessException to prevent null return values. interface UserRepository extends Repository { List findByLastname(@Nullable String firstname); @Nullable User findByFirstnameAndLastname(String firstname, String lastname); } interface UserRepository : Repository { fun findByLastname(username: String?): List fun findByFirstnameAndLastname(firstname: String, lastname: String): User? } Original pull request: #241. --- src/main/asciidoc/repositories.adoc | 64 ++++ .../support/MethodInvocationValidator.java | 163 +++++++++++ .../support/RepositoryFactorySupport.java | 15 +- .../data/util/NullableUtils.java | 274 ++++++++++++++++++ .../data/util/ReflectionUtils.java | 34 +++ ...pperRepositoryFactorySupportUnitTests.java | 17 +- .../RepositoryFactorySupportUnitTests.java | 57 ++++ .../data/util/NullableUtilsUnitTests.java | 119 ++++++++ .../data/util/ReflectionUtilsUnitTests.java | 38 +++ .../util/nonnull/NullableAnnotatedType.java | 37 +++ .../packagelevel/NonNullOnPackage.java | 24 ++ .../nonnull/packagelevel/package-info.java | 3 + .../nonnull/type/CustomAnnotatedType.java | 22 ++ .../nonnull/type/CustomNonNullAnnotation.java | 34 +++ .../type/Jsr305NonnullAnnotatedType.java | 24 ++ .../util/nonnull/type/NonAnnotatedType.java | 21 ++ .../nonnull/type/NonNullableParameters.java | 24 ++ .../core/support/KotlinUserRepository.kt | 31 ++ .../data/util/DummyInterface.kt | 34 +++ 19 files changed, 1026 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java create mode 100644 src/main/java/org/springframework/data/util/NullableUtils.java create mode 100644 src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/packagelevel/package-info.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/type/CustomAnnotatedType.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/type/CustomNonNullAnnotation.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java create mode 100644 src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt create mode 100644 src/test/kotlin/org/springframework/data/util/DummyInterface.kt 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