Skip to content

Commit

Permalink
Migrate and verify annotation utility methods
Browse files Browse the repository at this point in the history
Migrate all possible `AnnotationUtils` and `AnnotatedElementUtils`
method to the `MergedAnnotation` API, verify results against the old
implementations.

All migrated methods now call both the new API and the old version and
ensure that the same results or exceptions are raised. A full build of
both Spring Framework and Spring Boot has been executed to ensure, as
much as possible, that the migration does not cause unexpected
regressions.

See spring-projectsgh-22562
  • Loading branch information
philwebb committed Mar 14, 2019
1 parent 5316769 commit 6a7e022
Show file tree
Hide file tree
Showing 9 changed files with 1,136 additions and 192 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
* @param <S> the type of source supported by this extractor
* @see Annotation
* @see AliasFor
* @see AnnotationUtils#synthesizeAnnotation(Annotation, Object)
* @see AnnotationUtils#synthesizeAnnotation(Annotation, java.lang.reflect.AnnotatedElement)
*/
abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements AnnotationAttributeExtractor<S> {

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ public AnnotationAttributes(Class<? extends Annotation> annotationType) {
this.displayName = annotationType.getName();
}

/**
* Create a possibly already validated new, empty
* {@link AnnotationAttributes} instance for the specified
* {@code annotationType}.
* @param annotationType the type of annotation represented by this
* {@code AnnotationAttributes} instance; never {@code null}
* @param validated if the attributes are considered already validated
* @since 5.2
*/
AnnotationAttributes(Class<? extends Annotation> annotationType, boolean validated) {
Assert.notNull(annotationType, "'annotationType' must not be null");
this.annotationType = annotationType;
this.displayName = annotationType.getName();
this.validated = validated;
}

/**
* Create a new, empty {@link AnnotationAttributes} instance for the
* specified {@code annotationType}.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
/*
* Copyright 2002-2019 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.core.annotation;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;

import org.springframework.core.annotation.InternalAnnotationUtils.DefaultValueHolder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
* Internal class used to help migrate annotation util methods to a new implementation.
*
* @author Phillip Webb
* @since 5.2
*/
final class MigrateMethod {

private MigrateMethod() {
}

/**
* Create a new {@link ReplacementMethod} builder for the deprecated method.
* @param originalMethod the original method being migrated
* @return a replacement builder.
*/
static <T> ReplacementMethod<T> from(Supplier<T> originalMethod) {
return new ReplacementMethod<>(originalMethod);
}

/**
* Create a new {@link ReplacementVoidMethod} for the deprecated method.
* @param originalMethod the original method being migrated
* @return a replacement builder.
*/
static ReplacementVoidMethod fromCall(Runnable originalMethod) {
return new ReplacementVoidMethod(originalMethod);
}

private static boolean isEquivalent(@Nullable Object result, @Nullable Object expectedResult) {
if (ObjectUtils.nullSafeEquals(result, expectedResult)) {
return true;
}
if (result == null && String.valueOf(expectedResult).startsWith(
"@org.springframework.lang.")) {
// Original methods don't filter spring annotation but we do
return true;
}
if (result == null || expectedResult == null) {
return false;
}
if (result instanceof DefaultValueHolder && expectedResult instanceof DefaultValueHolder) {
return isEquivalent(((DefaultValueHolder) result).defaultValue,
((DefaultValueHolder) expectedResult).defaultValue);
}
if (result instanceof Map && expectedResult instanceof Map) {
return isEquivalentMap((Map<?, ?>) result, (Map<?, ?>) expectedResult);
}
if (result instanceof List && expectedResult instanceof List) {
return isEquivalentList((List<?>) result, (List<?>) expectedResult);
}
if (result instanceof Object[] && expectedResult instanceof Object[]) {
return isEquivalentArray((Object[]) result, (Object[]) expectedResult);
}
if (result instanceof Object[]) {
if (isEquivalentArray((Object[]) result, new Object[] { expectedResult })) {
return true;
}
}
if (!(result instanceof Object[]) && expectedResult instanceof Object[]) {
if (isEquivalentArray(new Object[] { result }, (Object[]) expectedResult)) {
return true;
}
}
return false;
}

private static boolean isEquivalentMap(Map<?, ?> result, Map<?, ?> expectedResult) {
if (result.size() != expectedResult.size()) {
return false;
}
for (Map.Entry<?, ?> entry : result.entrySet()) {
if (!expectedResult.containsKey(entry.getKey())) {
return false;
}
if (!isEquivalent(entry.getValue(), expectedResult.get(entry.getKey()))) {
return false;
}
}
return true;
}

private static boolean isEquivalentList(List<?> result, List<?> expectedResult) {
if (result.size() != expectedResult.size()) {
return false;
}
for (int i = 0; i < result.size(); i++) {
if (!isEquivalent(result.get(i), expectedResult.get(i))) {
return false;
}
}
return true;
}

private static boolean isEquivalentArray(Object[] result, Object[] expectedResult) {
if (result.length != expectedResult.length) {
return false;
}
for (int i = 0; i < result.length; i++) {
if (!isEquivalent(result[i], expectedResult[i])) {
return false;
}
}
return true;
}

/**
* Builder to complete replacement details for a deprecated annotation method.
* @param <T> the return type
*/
static class ReplacementMethod<T> {

private final Supplier<T> originalMethod;

@Nullable
private Supplier<String> description;

private boolean skipOriginalExceptionCheck;

private BooleanSupplier skipEquivalentCheck = () -> false;

ReplacementMethod(Supplier<T> deprecatedMethod) {
this.originalMethod = deprecatedMethod;
}

/**
* Add a description for the method.
* @param description a description supplier
* @return this instance
*/
public ReplacementMethod<T> withDescription(Supplier<String> description) {
this.description = description;
return this;
}

public ReplacementMethod<T> withSkippedOriginalExceptionCheck() {
this.skipOriginalExceptionCheck = true;
return this;
}

public ReplacementMethod<T> withSkippedEquivalentCheck(BooleanSupplier supplier) {
this.skipEquivalentCheck = supplier;
return this;
}

/**
* Provide the replacement method that should be used instead of the deprecated
* one. The replacement method is called, and when appropriate the result is
* checked against the deprecated method.
* @param replacementMethod the replacement method
* @return the result of the replacement method
*/
public T to(Supplier<T> replacementMethod) {
T result = toNullable(replacementMethod);
if (result == null) {
throw new IllegalStateException("Unexpected null result");
}
return result;
}

/**
* Provide the replacement method that should be used instead of the deprecated
* one. The replacement method is called, and when appropriate the result is
* checked against the deprecated method.
* @param replacementMethod the replacement method
* @return the result of the replacement method
*/
@Nullable
public T toNullable(Supplier<T> replacementMethod) {
T result = tryInvoke(replacementMethod);
T expectedResult = this.originalMethod.get();
if (!isEquivalent(result, expectedResult)) {
if (this.skipEquivalentCheck.getAsBoolean()) {
return expectedResult;
}
String description = (this.description != null ? " [" +
this.description.get() + "]" : "");
throw new IllegalStateException("Expected " + expectedResult +
" got " + result + description);
}
return result;
}

private T tryInvoke(Supplier<T> replacementMethod) {
try {
return replacementMethod.get();
}
catch (RuntimeException expected) {
try {
T expectedResult = this.originalMethod.get();
if (this.skipOriginalExceptionCheck) {
return expectedResult;
}
throw new Error("Expected exception not thrown", expected);
}
catch (RuntimeException actual) {
if (!expected.getClass().isInstance(actual)) {
throw new Error(
"Exception is not " + expected.getClass().getName(),
actual);
}
throw actual;
}
}
}

}

/**
* Builder to complete replacement details for a deprecated annotation method that
* returns void.
*/
static class ReplacementVoidMethod {

private final Runnable originalMethod;

private final List<Object[]> argumentChecks = new ArrayList<>();

public ReplacementVoidMethod(Runnable originalMethod) {
this.originalMethod = originalMethod;
}

public ReplacementVoidMethod withArgumentCheck(Object originalArgument,
Object replacementArgument) {
this.argumentChecks.add(
new Object[] { originalArgument, replacementArgument });
return this;
}

public void to(Runnable replacementMethod) {
tryInvoke(this.originalMethod);
replacementMethod.run();
for (Object[] arguments : this.argumentChecks) {
Object expectedArgument = arguments[0];
Object actualArgument = arguments[1];
Assert.state(isEquivalent(actualArgument, expectedArgument),
() -> "Expected argument mutation of " + expectedArgument
+ " got " + actualArgument);
}
}

private void tryInvoke(Runnable replacementMethod) {
try {
replacementMethod.run();
}
catch (RuntimeException expected) {
try {
this.originalMethod.run();
throw new Error("Expected exception not thrown", expected);
}
catch (RuntimeException actual) {
if (!expected.getClass().isInstance(actual)) {
throw new Error(
"Exception is not " + expected.getClass().getName(),
actual);
}
throw actual;
}
}
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand Down Expand Up @@ -459,7 +459,9 @@ public void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnot
exception.expectMessage(either(
containsString("values of [{duplicateDeclaration}] and [{requiredLocationsDeclaration}]")).or(
containsString("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]")));
exception.expectMessage(containsString("but only one is permitted"));
exception.expectMessage(either(
containsString("but only one is permitted")).or(
containsString("Different @AliasFor mirror values for annotation")));
getMergedAnnotationAttributes(element, ContextConfig.class);
}

Expand Down
Loading

0 comments on commit 6a7e022

Please sign in to comment.