diff --git a/spring-core/src/main/java/org/springframework/aot/generator/CodeContribution.java b/spring-core/src/main/java/org/springframework/aot/generator/CodeContribution.java new file mode 100644 index 0000000000..964c7090a4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generator/CodeContribution.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generator; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.javapoet.support.MultiStatement; + +/** + * A code contribution that gathers the code, the {@linkplain RuntimeHints + * runtime hints}, and the {@linkplain ProtectedElement protected elements} + * that are necessary to execute it. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public interface CodeContribution { + + /** + * Return the {@linkplain MultiStatement statements} that can be used to + * append code. + * @return the statements instance to use to contribute code + */ + MultiStatement statements(); + + /** + * Return the {@linkplain RuntimeHints hints} to use to register + * potential optimizations for contributed code. + * @return the runtime hints + */ + RuntimeHints runtimeHints(); + + /** + * Return the {@linkplain ProtectedAccess protected access} to use to + * analyze any privileged access, if necessary. + * @return the protected access + */ + ProtectedAccess protectedAccess(); + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generator/DefaultCodeContribution.java b/spring-core/src/main/java/org/springframework/aot/generator/DefaultCodeContribution.java new file mode 100644 index 0000000000..3fd34922fe --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generator/DefaultCodeContribution.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generator; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.javapoet.support.MultiStatement; + +/** + * A default {@link CodeContribution} implementation. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class DefaultCodeContribution implements CodeContribution { + + private final MultiStatement statements; + + private final RuntimeHints runtimeHints; + + private final ProtectedAccess protectedAccess; + + + protected DefaultCodeContribution(MultiStatement statements, RuntimeHints runtimeHints, + ProtectedAccess protectedAccess) { + + this.statements = statements; + this.runtimeHints = runtimeHints; + this.protectedAccess = protectedAccess; + } + + /** + * Create an instance with the {@link RuntimeHints} instance to use. + * @param runtimeHints the runtime hints instance to use + */ + public DefaultCodeContribution(RuntimeHints runtimeHints) { + this(new MultiStatement(), runtimeHints, new ProtectedAccess()); + } + + @Override + public MultiStatement statements() { + return this.statements; + } + + @Override + public RuntimeHints runtimeHints() { + return this.runtimeHints; + } + + @Override + public ProtectedAccess protectedAccess() { + return this.protectedAccess; + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generator/ProtectedAccess.java b/spring-core/src/main/java/org/springframework/aot/generator/ProtectedAccess.java new file mode 100644 index 0000000000..337be6f442 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generator/ProtectedAccess.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generator; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Gather the need of non-public access and determine the priviledged package + * to use, if necessary. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class ProtectedAccess { + + private final List elements; + + public ProtectedAccess() { + this.elements = new ArrayList<>(); + } + + /** + * Specify whether the protected elements registered in this instance are + * accessible from the specified package name. + * @param packageName the target package name + * @return {@code true} if the registered access can be safely used from + * the specified package name + */ + public boolean isAccessible(String packageName) { + return getProtectedElements(packageName).isEmpty(); + } + + /** + * Return the privileged package name to use for the specified package + * name, or {@code null} if none is required. + * @param packageName the target package name to use + * @return the privileged package name to use, or {@code null} + * @throws ProtectedAccessException if a single privileged package cannot + * be identified + * @see #isAccessible(String) + */ + @Nullable + public String getPrivilegedPackageName(String packageName) throws ProtectedAccessException { + List protectedElements = getProtectedElements(packageName); + if (protectedElements.isEmpty()) { + return null; + } + List packageNames = protectedElements.stream() + .map(element -> element.getType().toClass().getPackageName()) + .distinct().toList(); + if (packageNames.size() == 1) { + return packageNames.get(0); + } + throw new ProtectedAccessException("Multiple packages require a privileged access: " + + packageNames, protectedElements); + } + + private List getProtectedElements(String packageName) { + List matches = new ArrayList<>(); + for (ProtectedElement element : this.elements) { + if (!element.getType().toClass().getPackage().getName().equals(packageName)) { + matches.add(element); + } + } + return matches; + } + + /** + * Analyze the specified {@linkplain ResolvableType type}, including its + * full type signature. + * @param type the type to analyze + */ + public void analyze(ResolvableType type) { + if (isProtected(type)) { + registerProtectedType(type, null); + } + } + + /** + * Analyze accessing the specified {@link Member} using the default + * {@linkplain Options#DEFAULTS options}. + * @param member the member to analyze + */ + public void analyze(Member member) { + analyze(member, Options.DEFAULTS); + } + + /** + * Analyze accessing the specified {@link Member} using the specified + * {@link Options options}. + * @param member the member to analyze + * @param options the options to use + */ + public void analyze(Member member, Options options) { + if (isProtected(member.getDeclaringClass())) { + registerProtectedType(member.getDeclaringClass(), member); + } + if (!options.useReflection && isProtected(member.getModifiers())) { + registerProtectedType(member.getDeclaringClass(), member); + } + if (member instanceof Field field) { + ResolvableType fieldType = ResolvableType.forField(field); + if (options.assignReturnType && isProtected(fieldType)) { + registerProtectedType(fieldType, field); + } + } + else if (member instanceof Constructor constructor) { + analyzeParameterTypes(constructor, i -> + ResolvableType.forConstructorParameter(constructor, i)); + } + else if (member instanceof Method method) { + ResolvableType returnType = ResolvableType.forMethodReturnType(method); + if (!options.assignReturnType && isProtected(returnType)) { + registerProtectedType(returnType, method); + } + analyzeParameterTypes(method, i -> ResolvableType.forMethodParameter(method, i)); + } + } + + private void analyzeParameterTypes(Executable executable, Function parameterTypeFactory) { + + for (int i = 0; i < executable.getParameters().length; i++) { + ResolvableType parameterType = parameterTypeFactory.apply(i); + if (isProtected(parameterType)) { + registerProtectedType(parameterType, executable); + } + } + } + + boolean isProtected(ResolvableType resolvableType) { + return isProtected(new HashSet<>(), resolvableType); + } + + private boolean isProtected(Set seen, ResolvableType target) { + if (seen.contains(target)) { + return false; + } + seen.add(target); + ResolvableType nonProxyTarget = target.as(ClassUtils.getUserClass(target.toClass())); + if (isProtected(nonProxyTarget.toClass())) { + return true; + } + Class declaringClass = nonProxyTarget.toClass().getDeclaringClass(); + if (declaringClass != null) { + if (isProtected(declaringClass)) { + return true; + } + } + if (nonProxyTarget.hasGenerics()) { + for (ResolvableType generic : nonProxyTarget.getGenerics()) { + return isProtected(seen, generic); + } + } + return false; + } + + private boolean isProtected(Class type) { + Class candidate = ClassUtils.getUserClass(type); + return isProtected(candidate.getModifiers()); + } + + private boolean isProtected(int modifiers) { + return !Modifier.isPublic(modifiers); + } + + private void registerProtectedType(ResolvableType type, @Nullable Member member) { + this.elements.add(ProtectedElement.of(type, member)); + } + + private void registerProtectedType(Class type, Member member) { + registerProtectedType(ResolvableType.forClass(type), member); + } + + /** + * Options to use to analyze if invoking a {@link Member} requires + * privileged access. + */ + public static class Options { + + /** + * Default options that does fallback to reflection and does not + * assign the default type. + */ + public static final Options DEFAULTS = new Options(); + + private final boolean useReflection; + + private final boolean assignReturnType; + + /** + * Create a new instance with the specified options. + * @param useReflection whether the writer can automatically use + * reflection to invoke a protected member if it is not public + * @param assignReturnType whether the writer needs to assign the + * return type, or if it is irrelevant + */ + public Options(boolean useReflection, boolean assignReturnType) { + this.useReflection = useReflection; + this.assignReturnType = assignReturnType; + } + + private Options() { + this(true, false); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generator/ProtectedAccessException.java b/spring-core/src/main/java/org/springframework/aot/generator/ProtectedAccessException.java new file mode 100644 index 0000000000..0a2c3fb8f5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generator/ProtectedAccessException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generator; + +import java.util.List; + +/** + * Thrown when a code block requires privileged access on multiple packages. + * + * @author Stephane Nicoll + * @since 6.0 + */ +@SuppressWarnings("serial") +public class ProtectedAccessException extends RuntimeException { + + private final List protectedElements; + + public ProtectedAccessException(String message, List protectedElements) { + super(message); + this.protectedElements = protectedElements; + } + + /** + * Return the {@linkplain ProtectedElement protected elements}. + * @return the protected access + */ + public List getProtectedElements() { + return this.protectedElements; + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generator/ProtectedElement.java b/spring-core/src/main/java/org/springframework/aot/generator/ProtectedElement.java new file mode 100644 index 0000000000..eb2615ed7b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generator/ProtectedElement.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generator; + +import java.lang.reflect.Member; + +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * A {@link Member} that is non-public, with the related type. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class ProtectedElement { + + private final ResolvableType type; + + @Nullable + private final Member target; + + + private ProtectedElement(ResolvableType type, @Nullable Member member) { + this.type = type; + this.target = member; + } + + /** + * Return the {@link ResolvableType type} that is non-public. For a plain + * protected {@link Member member} access, the type of the declaring class + * is used. Otherwise, the type in the member signature, such as a parameter + * type for an executable, or the return type of a field is used. + * @return the type that is not public + */ + public ResolvableType getType() { + return this.type; + } + + /** + * Return the {@link Member} that is not publicly accessible. + * @return the member + */ + @Nullable + public Member getMember() { + return this.target; + } + + static ProtectedElement of(ResolvableType type, @Nullable Member member) { + return new ProtectedElement(type, member); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generator/package-info.java b/spring-core/src/main/java/org/springframework/aot/generator/package-info.java new file mode 100644 index 0000000000..beef9af746 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generator/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for components that contribute generated code equivalent + * to a runtime behavior. + */ +@NonNullApi +@NonNullFields +package org.springframework.aot.generator; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/test/java/org/springframework/aot/generator/DefaultCodeContributionTests.java b/spring-core/src/test/java/org/springframework/aot/generator/DefaultCodeContributionTests.java new file mode 100644 index 0000000000..ce69671ddf --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generator/DefaultCodeContributionTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generator; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultCodeContribution}. + * + * @author Stephane Nicoll + */ +class DefaultCodeContributionTests { + + @Test + void newCodeContributionIsEmpty() { + CodeContribution contribution = new DefaultCodeContribution(new RuntimeHints()); + assertThat(contribution.statements().isEmpty()).isTrue(); + assertThat(contribution.protectedAccess().isAccessible("com.example")).isTrue(); + } + + @Test + void codeContributionReusesRuntimeHints() { + RuntimeHints runtimeHints = new RuntimeHints(); + CodeContribution contribution = new DefaultCodeContribution(runtimeHints); + assertThat(contribution.runtimeHints()).isSameAs(runtimeHints); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generator/ProtectedAccessTests.java b/spring-core/src/test/java/org/springframework/aot/generator/ProtectedAccessTests.java new file mode 100644 index 0000000000..5a996272eb --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generator/ProtectedAccessTests.java @@ -0,0 +1,258 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.aot.generator; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generator.ProtectedAccess.Options; +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.aot.generator.visibility.ProtectedParameter; +import org.springframework.core.testfixture.aot.generator.visibility.PublicFactoryBean; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ProtectedAccess}. + * + * @author Stephane Nicoll + */ +class ProtectedAccessTests { + + private final ProtectedAccess protectedAccess = new ProtectedAccess(); + + @Test + void analyzeWithPublicConstructor() throws NoSuchMethodException { + this.protectedAccess.analyze(PublicClass.class.getConstructor()); + assertThat(this.protectedAccess.isAccessible("com.example")).isTrue(); + } + + @Test + void analyzeWithPackagePrivateConstructorAndDefaultOptions() { + this.protectedAccess.analyze(ProtectedAccessor.class.getDeclaredConstructors()[0]); + assertThat(this.protectedAccess.isAccessible("com.example")).isTrue(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")).isNull(); + } + + @Test + void analyzeWithPackagePrivateConstructorAndReflectionDisabled() { + this.protectedAccess.analyze(ProtectedAccessor.class.getDeclaredConstructors()[0], + new Options(false, true)); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(ProtectedAccessor.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(ProtectedAccessor.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPackagePrivateClass() { + this.protectedAccess.analyze(ProtectedClass.class.getDeclaredConstructors()[0]); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(ProtectedClass.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(ProtectedClass.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPackagePrivateDeclaringType() { + this.protectedAccess.analyze(method(ProtectedClass.class, "stringBean")); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(ProtectedClass.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(ProtectedClass.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPackagePrivateConstructorParameter() { + this.protectedAccess.analyze(ProtectedParameter.class.getConstructors()[0]); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(ProtectedParameter.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(ProtectedParameter.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPackagePrivateMethod() { + this.protectedAccess.analyze(method(PublicClass.class, "getProtectedMethod")); + assertThat(this.protectedAccess.isAccessible("com.example")).isTrue(); + } + + @Test + void analyzeWithPackagePrivateMethodAndReflectionDisabled() { + this.protectedAccess.analyze(method(PublicClass.class, "getProtectedMethod"), + new Options(false, false)); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + } + + @Test + void analyzeWithPackagePrivateMethodReturnType() { + this.protectedAccess.analyze(method(ProtectedAccessor.class, "methodWithProtectedReturnType")); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(ProtectedAccessor.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(ProtectedAccessor.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPackagePrivateMethodParameter() { + this.protectedAccess.analyze(method(ProtectedAccessor.class, "methodWithProtectedParameter", + ProtectedClass.class)); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(ProtectedClass.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(ProtectedClass.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPackagePrivateField() { + this.protectedAccess.analyze(field(PublicClass.class, "protectedField")); + assertThat(this.protectedAccess.isAccessible("com.example")).isTrue(); + } + + @Test + void analyzeWithPackagePrivateFieldAndReflectionDisabled() { + this.protectedAccess.analyze(field(PublicClass.class, "protectedField"), + new Options(false, true)); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(PublicClass.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(PublicClass.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPublicFieldAndProtectedType() { + this.protectedAccess.analyze(field(PublicClass.class, "protectedClassField"), + new Options(false, true)); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(ProtectedClass.class.getPackageName()); + assertThat(this.protectedAccess.isAccessible(ProtectedClass.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithPackagePrivateGenericArgument() { + this.protectedAccess.analyze(method(PublicFactoryBean.class, "protectedTypeFactoryBean")); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.isAccessible(PublicFactoryBean.class.getPackageName())).isTrue(); + } + + @Test + void analyzeTypeWithProtectedGenericArgument() { + this.protectedAccess.analyze(PublicFactoryBean.resolveToProtectedGenericParameter()); + assertThat(this.protectedAccess.isAccessible("com.example")).isFalse(); + assertThat(this.protectedAccess.isAccessible(PublicFactoryBean.class.getPackageName())).isTrue(); + } + + @Test + void analyzeWithRecursiveType() { + assertThat(this.protectedAccess.isProtected(ResolvableType.forClassWithGenerics( + SelfReference.class, SelfReference.class))).isTrue(); + } + + @Test + void getProtectedPackageWithPublicAccess() throws NoSuchMethodException { + this.protectedAccess.analyze(PublicClass.class.getConstructor()); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")).isNull(); + } + + @Test + void getProtectedPackageWithProtectedAccessInOnePackage() { + this.protectedAccess.analyze(method(PublicFactoryBean.class, "protectedTypeFactoryBean")); + assertThat(this.protectedAccess.getPrivilegedPackageName("com.example")) + .isEqualTo(PublicFactoryBean.class.getPackageName()); + } + + @Test + void getProtectedPackageWithProtectedAccessInSeveralPackages() { + Method protectedMethodFirstPackage = method(PublicFactoryBean.class, "protectedTypeFactoryBean"); + Method protectedMethodSecondPackage = method(ProtectedAccessor.class, "methodWithProtectedParameter", + ProtectedClass.class); + this.protectedAccess.analyze(protectedMethodFirstPackage); + this.protectedAccess.analyze(protectedMethodSecondPackage); + assertThatThrownBy(() -> this.protectedAccess.getPrivilegedPackageName("com.example")) + .isInstanceOfSatisfying(ProtectedAccessException.class, ex -> + assertThat(ex.getProtectedElements().stream().map(ProtectedElement::getMember)) + .containsOnly(protectedMethodFirstPackage, protectedMethodSecondPackage)); + } + + private static Method method(Class type, String name, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(type, name, parameterTypes); + assertThat(method).isNotNull(); + return method; + } + + private static Field field(Class type, String name) { + Field field = ReflectionUtils.findField(type, name); + assertThat(field).isNotNull(); + return field; + } + + + @SuppressWarnings("unused") + public static class PublicClass { + + String protectedField; + + public ProtectedClass protectedClassField; + + String getProtectedMethod() { + return this.protectedField; + } + + } + + @SuppressWarnings("unused") + public static class ProtectedAccessor { + + ProtectedAccessor() { + } + + public String methodWithProtectedParameter(ProtectedClass type) { + return "test"; + } + + public ProtectedClass methodWithProtectedReturnType() { + return new ProtectedClass(); + } + } + + @SuppressWarnings("unused") + static class ProtectedClass { + + public ProtectedClass() { + } + + public String stringBean() { + return "public"; + } + + } + + static class SelfReference> { + + @SuppressWarnings("unchecked") + T getThis() { + return (T) this; + } + + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/ProtectedParameter.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/ProtectedParameter.java new file mode 100644 index 0000000000..6812d6a13b --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/ProtectedParameter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.testfixture.aot.generator.visibility; + +public class ProtectedParameter { + + public ProtectedParameter(ProtectedType type) { + + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/ProtectedType.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/ProtectedType.java new file mode 100644 index 0000000000..d1ce752b3b --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/ProtectedType.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.testfixture.aot.generator.visibility; + +class ProtectedType { + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/PublicFactoryBean.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/PublicFactoryBean.java new file mode 100644 index 0000000000..9e9bf8d58f --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/aot/generator/visibility/PublicFactoryBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.testfixture.aot.generator.visibility; + +import org.springframework.core.ResolvableType; + +public class PublicFactoryBean { + + private final Class type; + + public PublicFactoryBean(Class type) { + this.type = type; + } + + public static PublicFactoryBean protectedTypeFactoryBean() { + return new PublicFactoryBean<>(ProtectedType.class); + } + + public static ResolvableType resolveToProtectedGenericParameter() { + return ResolvableType.forClassWithGenerics(PublicFactoryBean.class, ProtectedType.class); + } + +}