diff --git a/build.gradle b/build.gradle index 50b2c3ee16b..cf15972bf7b 100644 --- a/build.gradle +++ b/build.gradle @@ -682,6 +682,17 @@ project("spring-context-support") { } } +project("spring-context-indexer") { + description = "Spring Context Indexer" + + dependencies { + testCompile(project(":spring-context")) + testCompile("javax.inject:javax.inject:1") + testCompile("javax.annotation:javax.annotation-api:${annotationApiVersion}") + testCompile("org.eclipse.persistence:javax.persistence:${jpaVersion}") + } +} + project("spring-web") { description = "Spring Web" apply plugin: "groovy" diff --git a/settings.gradle b/settings.gradle index 6c9be85d55f..316022a7048 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,7 @@ include "spring-beans" include "spring-beans-groovy" include "spring-context" include "spring-context-support" +include "spring-context-indexer" include "spring-core" include "spring-expression" include "spring-instrument" diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/CandidateComponentsIndexer.java b/spring-context-indexer/src/main/java/org/springframework/context/index/CandidateComponentsIndexer.java new file mode 100644 index 00000000000..2c6862b3109 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/CandidateComponentsIndexer.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import org.springframework.context.index.metadata.CandidateComponentsMetadata; +import org.springframework.context.index.metadata.ItemMetadata; + +/** + * Annotation {@link Processor} that writes {@link CandidateComponentsMetadata} + * file for spring components. + * + * @author Stephane Nicoll + * @since 5.0 + */ +@SupportedAnnotationTypes({"*"}) +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class CandidateComponentsIndexer extends AbstractProcessor { + + private MetadataStore metadataStore; + + private MetadataCollector metadataCollector; + + private TypeUtils typeUtils; + + private List stereotypesProviders; + + @Override + public synchronized void init(ProcessingEnvironment env) { + this.stereotypesProviders = getStereotypesProviders(env); + this.typeUtils = new TypeUtils(env); + this.metadataStore = new MetadataStore(env); + this.metadataCollector = new MetadataCollector(env, + this.metadataStore.readMetadata()); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + this.metadataCollector.processing(roundEnv); + roundEnv.getRootElements().forEach(this::processElement); + + if (roundEnv.processingOver()) { + writeMetaData(); + } + return false; + } + + protected List getStereotypesProviders(ProcessingEnvironment env) { + List result = new ArrayList<>(); + TypeUtils typeUtils = new TypeUtils(env); + result.add(new IndexedStereotypesProvider(typeUtils)); + result.add(new StandardStereotypesProvider(typeUtils)); + result.add(new PackageInfoStereotypesProvider()); + return result; + } + + private void processElement(Element element) { + Set stereotypes = new LinkedHashSet<>(); + this.stereotypesProviders.forEach(p -> { + stereotypes.addAll(p.getStereotypes(element)); + + }); + if (!stereotypes.isEmpty()) { + this.metadataCollector.add(new ItemMetadata( + this.typeUtils.getType(element), stereotypes)); + } + } + + protected CandidateComponentsMetadata writeMetaData() { + CandidateComponentsMetadata metadata = this.metadataCollector.getMetadata(); + if (!metadata.getItems().isEmpty()) { + try { + this.metadataStore.writeMetadata(metadata); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to write metadata", ex); + } + return metadata; + } + return null; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/IndexedStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/IndexedStereotypesProvider.java new file mode 100644 index 00000000000..6edb325a51e --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/IndexedStereotypesProvider.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** + * A {@link StereotypesProvider} implementation that extracts the stereotypes + * flagged by the {@value INDEXED_ANNOTATION} annotation. This implementation + * honors stereotypes defined this way on meta-annotations. + * + * @author Stephane Nicoll + */ +class IndexedStereotypesProvider implements StereotypesProvider { + + private static final String INDEXED_ANNOTATION = "org.springframework.stereotype.Indexed"; + + private final TypeUtils typeUtils; + + public IndexedStereotypesProvider(TypeUtils typeUtils) { + this.typeUtils = typeUtils; + } + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new LinkedHashSet<>(); + ElementKind kind = element.getKind(); + if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + return stereotypes; + } + Set seen = new HashSet<>(); + collectStereotypesOnAnnotations(seen, stereotypes, element); + seen = new HashSet<>(); + collectStereotypesOnTypes(seen, stereotypes, element); + return stereotypes; + } + + private void collectStereotypesOnAnnotations(Set seen, Set stereotypes, + Element element) { + for (AnnotationMirror annotation : this.typeUtils.getAllAnnotationMirrors(element)) { + Element next = collectStereotypes(seen, stereotypes, element, annotation); + if (next != null) { + collectStereotypesOnAnnotations(seen, stereotypes, next); + } + } + + } + + private void collectStereotypesOnTypes(Set seen, Set stereotypes, + Element type) { + if (!seen.contains(type)) { + seen.add(type); + if (isAnnotatedWithIndexed(type)) { + stereotypes.add(this.typeUtils.getType(type)); + } + Element superClass = this.typeUtils.getSuperClass(type); + if (superClass != null) { + collectStereotypesOnTypes(seen, stereotypes, superClass); + } + this.typeUtils.getDirectInterfaces(type).forEach( + i -> collectStereotypesOnTypes(seen, stereotypes, i)); + } + } + + private Element collectStereotypes(Set seen, Set stereotypes, + Element element, AnnotationMirror annotation) { + if (isIndexedAnnotation(annotation)) { + stereotypes.add(this.typeUtils.getType(element)); + } + return getCandidateAnnotationElement(seen, annotation); + } + + private Element getCandidateAnnotationElement(Set seen, AnnotationMirror annotation) { + Element element = annotation.getAnnotationType().asElement(); + if (seen.contains(element)) { + return null; + } + // We need to visit all indexed annotations. + if (!isIndexedAnnotation(annotation)) { + seen.add(element); + } + return (!element.toString().startsWith("java.lang") ? element : null); + } + + private boolean isAnnotatedWithIndexed(Element type) { + for (AnnotationMirror annotation : type.getAnnotationMirrors()) { + if (isIndexedAnnotation(annotation)) { + return true; + } + } + return false; + } + + private boolean isIndexedAnnotation(AnnotationMirror annotation) { + return INDEXED_ANNOTATION.equals(annotation.getAnnotationType().toString()); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataCollector.java b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataCollector.java new file mode 100644 index 00000000000..22701d2456b --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataCollector.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import org.springframework.context.index.metadata.ItemMetadata; +import org.springframework.context.index.metadata.CandidateComponentsMetadata; + +/** + * Used by {@link CandidateComponentsIndexer} to collect {@link CandidateComponentsMetadata}. + * + * @author Stephane Nicoll + */ +class MetadataCollector { + + private final List metadataItems = new ArrayList(); + + private final ProcessingEnvironment processingEnvironment; + + private final CandidateComponentsMetadata previousMetadata; + + private final TypeUtils typeUtils; + + private final Set processedSourceTypes = new HashSet(); + + /** + * Creates a new {@code MetadataProcessor} instance. + * @param processingEnvironment The processing environment of the build + * @param previousMetadata Any previous metadata or {@code null} + */ + public MetadataCollector(ProcessingEnvironment processingEnvironment, + CandidateComponentsMetadata previousMetadata) { + this.processingEnvironment = processingEnvironment; + this.previousMetadata = previousMetadata; + this.typeUtils = new TypeUtils(processingEnvironment); + } + + public void processing(RoundEnvironment roundEnv) { + for (Element element : roundEnv.getRootElements()) { + markAsProcessed(element); + } + } + + private void markAsProcessed(Element element) { + if (element instanceof TypeElement) { + this.processedSourceTypes.add(this.typeUtils.getType(element)); + } + } + + public void add(ItemMetadata metadata) { + this.metadataItems.add(metadata); + } + + public CandidateComponentsMetadata getMetadata() { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + for (ItemMetadata item : this.metadataItems) { + metadata.add(item); + } + if (this.previousMetadata != null) { + List items = this.previousMetadata.getItems(); + for (ItemMetadata item : items) { + if (shouldBeMerged(item)) { + metadata.add(item); + } + } + } + return metadata; + } + + private boolean shouldBeMerged(ItemMetadata itemMetadata) { + String sourceType = itemMetadata.getType(); + return (sourceType != null && !deletedInCurrentBuild(sourceType) + && !processedInCurrentBuild(sourceType)); + } + + private boolean deletedInCurrentBuild(String sourceType) { + return this.processingEnvironment.getElementUtils() + .getTypeElement(sourceType) == null; + } + + private boolean processedInCurrentBuild(String sourceType) { + return this.processedSourceTypes.contains(sourceType); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataStore.java b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataStore.java new file mode 100644 index 00000000000..70ada702187 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/MetadataStore.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import org.springframework.context.index.metadata.PropertiesMarshaller; +import org.springframework.context.index.metadata.CandidateComponentsMetadata; + +/** + * Store {@link CandidateComponentsMetadata} on the filesystem. + * + * @author Stephane Nicoll + */ +class MetadataStore { + + static final String METADATA_PATH = "META-INF/spring.components"; + + private final ProcessingEnvironment environment; + + public MetadataStore(ProcessingEnvironment environment) { + this.environment = environment; + } + + public CandidateComponentsMetadata readMetadata() { + try { + return readMetadata(getMetadataResource().openInputStream()); + } + catch (IOException ex) { + return null; + } + } + + public void writeMetadata(CandidateComponentsMetadata metadata) throws IOException { + if (!metadata.getItems().isEmpty()) { + try (OutputStream outputStream = createMetadataResource().openOutputStream()) { + new PropertiesMarshaller().write(metadata, outputStream); + } + } + } + + private CandidateComponentsMetadata readMetadata(InputStream in) throws IOException { + try { + return new PropertiesMarshaller().read(in); + } + catch (IOException ex) { + return null; + } + finally { + in.close(); + } + } + + private FileObject getMetadataResource() throws IOException { + return this.environment.getFiler() + .getResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); + } + + private FileObject createMetadataResource() throws IOException { + return this.environment.getFiler() + .createResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/PackageInfoStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/PackageInfoStereotypesProvider.java new file mode 100644 index 00000000000..6b0151199dd --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/PackageInfoStereotypesProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.HashSet; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +/** + * A {@link StereotypesProvider} implementation that provides the + * {@value STEREOTYPE} stereotype for each package-info. + * + * @author Stephane Nicoll + */ +class PackageInfoStereotypesProvider implements StereotypesProvider { + + public static final String STEREOTYPE = "package-info"; + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new HashSet<>(); + if (element.getKind() == ElementKind.PACKAGE) { + stereotypes.add(STEREOTYPE); + } + return stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/StandardStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/StandardStereotypesProvider.java new file mode 100644 index 00000000000..c119934da81 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/StandardStereotypesProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +/** + * A {@link StereotypesProvider} that extract a stereotype for each + * {@code javax.*} annotation placed on a class or interface. + * + * @author Stephane Nicoll + */ +class StandardStereotypesProvider implements StereotypesProvider { + + private final TypeUtils typeUtils; + + StandardStereotypesProvider(TypeUtils typeUtils) { + this.typeUtils = typeUtils; + } + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new LinkedHashSet<>(); + ElementKind kind = element.getKind(); + if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + return stereotypes; + } + for (AnnotationMirror annotation : this.typeUtils.getAllAnnotationMirrors(element)) { + String type = this.typeUtils.getType(annotation); + if (type.startsWith("javax.")) { + stereotypes.add(type); + } + } + return stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/StereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/StereotypesProvider.java new file mode 100644 index 00000000000..64672f62ceb --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/StereotypesProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.Set; +import javax.lang.model.element.Element; + +/** + * Provide the list of stereotypes that match an {@link Element}. If an element + * has one more stereotypes, it is referenced in the index of candidate + * components and each stereotype can be queried individually. + * + * @author Stephane Nicoll + */ +interface StereotypesProvider { + + /** + * Return the stereotypes that are present on the given {@link Element}. + * @param element the element to handle + * @return the stereotypes or an empty set if none were found + */ + Set getStereotypes(Element element); + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/TypeUtils.java b/spring-context-indexer/src/main/java/org/springframework/context/index/TypeUtils.java new file mode 100644 index 00000000000..ee9931294aa --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/TypeUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +/** + * Type utilities. + * + * @author Stephane Nicoll + */ +class TypeUtils { + + private final ProcessingEnvironment env; + + private final Types types; + + TypeUtils(ProcessingEnvironment env) { + this.env = env; + this.types = env.getTypeUtils(); + } + + public String getType(Element element) { + return getType(element != null ? element.asType() : null); + } + + public String getType(AnnotationMirror annotation) { + return getType(annotation != null ? annotation.getAnnotationType() : null); + } + + public String getType(TypeMirror type) { + if (type == null) { + return null; + } + if (type instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) type; + Element enclosingElement = declaredType.asElement().getEnclosingElement(); + if (enclosingElement != null && enclosingElement instanceof TypeElement) { + return getQualifiedName(enclosingElement) + "$" + + declaredType.asElement().getSimpleName().toString(); + } else { + return getQualifiedName(declaredType.asElement()); + } + } + return type.toString(); + } + + private String getQualifiedName(Element element) { + if (element instanceof QualifiedNameable) { + return ((QualifiedNameable) element).getQualifiedName().toString(); + } + return element.toString(); + } + + /** + * Return the super class of the specified {@link Element} or null if this + * {@code element} represents {@link Object}. + */ + public Element getSuperClass(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + if (superTypes.isEmpty()) { + return null; // reached java.lang.Object + } + return this.types.asElement(superTypes.get(0)); + } + + /** + * Return the interfaces that are directly implemented by the + * specified {@link Element} or an empty list if this {@code element} does not + * implement any interface. + */ + public List getDirectInterfaces(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + List directInterfaces = new ArrayList<>(); + if (superTypes.size() > 1) { // index 0 is the super class + for (int i = 1; i < superTypes.size(); i++) { + Element e = this.types.asElement(superTypes.get(i)); + if (e != null) { + directInterfaces.add(e); + } + } + } + return directInterfaces; + } + + public List getAllAnnotationMirrors(Element e) { + return this.env.getElementUtils().getAllAnnotationMirrors(e); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/CandidateComponentsMetadata.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/CandidateComponentsMetadata.java new file mode 100644 index 00000000000..fe69d19ea1b --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/CandidateComponentsMetadata.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.metadata; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Meta-data for candidate components. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class CandidateComponentsMetadata { + + private final List items; + + public CandidateComponentsMetadata() { + this.items = new ArrayList<>(); + } + + public void add(ItemMetadata item) { + this.items.add(item); + } + + public List getItems() { + return Collections.unmodifiableList(this.items); + } + + @Override + public String toString() { + return "CandidateComponentsMetadata{" + "items=" + this.items + '}'; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/ItemMetadata.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/ItemMetadata.java new file mode 100644 index 00000000000..6555e78f1bc --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/ItemMetadata.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.metadata; + +import java.util.HashSet; +import java.util.Set; + +/** + * Represents one entry in the index. The type defines the identify of the target + * candidate (usually fully qualified name) and the stereotypes are "markers" that can + * be used to retrieve the candidates. A typical use case is the presence of a given + * annotation on the candidate. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class ItemMetadata { + + private final String type; + + private final Set stereotypes; + + public ItemMetadata(String type, Set stereotypes) { + this.type = type; + this.stereotypes = new HashSet<>(stereotypes); + } + + public String getType() { + return this.type; + } + + public Set getStereotypes() { + return this.stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/PropertiesMarshaller.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/PropertiesMarshaller.java new file mode 100644 index 00000000000..46079597cba --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/PropertiesMarshaller.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.metadata; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Marshaller to write {@link CandidateComponentsMetadata} as properties. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class PropertiesMarshaller { + + public void write(CandidateComponentsMetadata metadata, OutputStream out) + throws IOException { + + Properties props = new Properties(); + metadata.getItems().forEach(m -> props.put(m.getType(), String.join(",", m.getStereotypes()))); + props.store(out, ""); + } + + public CandidateComponentsMetadata read(InputStream in) throws IOException { + CandidateComponentsMetadata result = new CandidateComponentsMetadata(); + Properties props = new Properties(); + props.load(in); + for (Map.Entry entry : props.entrySet()) { + String type = (String) entry.getKey(); + Set candidates = new HashSet<>(Arrays.asList(((String) entry.getValue()).split(","))); + result.add(new ItemMetadata(type, candidates)); + } + return result; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/package-info.java b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/package-info.java new file mode 100644 index 00000000000..b354f78e778 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/metadata/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support package for defining and storing the metadata that forms the index. + */ +package org.springframework.context.index.metadata; \ No newline at end of file diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/package-info.java b/spring-context-indexer/src/main/java/org/springframework/context/index/package-info.java new file mode 100644 index 00000000000..4edf83036f7 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Generates a 'META-INF/spring.candidates' at compilation time with all + * the component candidates detected in the module. + */ +package org.springframework.context.index; \ No newline at end of file diff --git a/spring-context-indexer/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/spring-context-indexer/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000000..99504ebd318 --- /dev/null +++ b/spring-context-indexer/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.springframework.context.index.CandidateComponentsIndexer \ No newline at end of file diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/CandidateComponentsIndexerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/CandidateComponentsIndexerTests.java new file mode 100644 index 00000000000..abb582b3d9e --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/CandidateComponentsIndexerTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import javax.annotation.ManagedBean; +import javax.inject.Named; +import javax.persistence.Converter; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +import org.springframework.context.index.metadata.CandidateComponentsMetadata; +import org.springframework.context.index.metadata.PropertiesMarshaller; +import org.springframework.context.index.sample.AbstractController; +import org.springframework.context.index.sample.MetaControllerIndexed; +import org.springframework.context.index.sample.SampleComponent; +import org.springframework.context.index.sample.SampleController; +import org.springframework.context.index.sample.SampleMetaController; +import org.springframework.context.index.sample.SampleMetaIndexedController; +import org.springframework.context.index.sample.SampleNone; +import org.springframework.context.index.sample.SampleRepository; +import org.springframework.context.index.sample.SampleService; +import org.springframework.context.index.sample.cdi.SampleManagedBean; +import org.springframework.context.index.sample.cdi.SampleNamed; +import org.springframework.context.index.sample.jpa.SampleConverter; +import org.springframework.context.index.sample.jpa.SampleEmbeddable; +import org.springframework.context.index.sample.jpa.SampleEntity; +import org.springframework.context.index.sample.jpa.SampleMappedSuperClass; +import org.springframework.context.index.sample.type.SampleRepo; +import org.springframework.context.index.sample.type.SampleSmartRepo; +import org.springframework.context.index.sample.type.SampleSpecializedRepo; +import org.springframework.context.index.sample.type.Repo; +import org.springframework.context.index.sample.type.SmartRepo; +import org.springframework.context.index.sample.type.SpecializedRepo; +import org.springframework.context.index.test.TestCompiler; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.context.index.test.Metadata.*; + +/** + * Tests for {@link CandidateComponentsIndexer}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexerTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private TestCompiler compiler; + + @Before + public void createCompiler() throws IOException { + this.compiler = new TestCompiler(this.temporaryFolder); + } + + @Test + public void noCandidate() throws IOException { + CandidateComponentsMetadata metadata = compile(SampleNone.class); + assertThat(metadata.getItems(), hasSize(0)); + } + + @Test + public void noAnnotation() throws IOException { + CandidateComponentsMetadata metadata = compile(CandidateComponentsIndexerTests.class); + assertThat(metadata.getItems(), hasSize(0)); + } + + @Test + public void stereotypeComponent() throws IOException { + testComponent(SampleComponent.class); + } + + @Test + public void stereotypeService() throws IOException { + testComponent(SampleService.class); + } + + @Test + public void stereotypeController() throws IOException { + testComponent(SampleController.class); + } + + @Test + public void stereotypeControllerMetaAnnotation() throws IOException { + testComponent(SampleMetaController.class); + } + + @Test + public void stereotypeRepository() throws IOException { + testSingleComponent(SampleRepository.class, Component.class); + } + + @Test + public void stereotypeControllerMetaIndex() throws IOException { + testSingleComponent(SampleMetaIndexedController.class, + Component.class, MetaControllerIndexed.class); + } + + @Test + public void stereotypeOnAbstractClass() throws IOException { + testComponent(AbstractController.class); + } + + @Test + public void cdiManagedBean() throws IOException { + testSingleComponent(SampleManagedBean.class, ManagedBean.class); + } + + @Test + public void cdiNamed() throws IOException { + testSingleComponent(SampleNamed.class, Named.class); + } + + @Test + public void persistenceEntity() throws IOException { + testSingleComponent(SampleEntity.class, Entity.class); + } + + @Test + public void persistenceMappedSuperClass() throws IOException { + testSingleComponent(SampleMappedSuperClass.class, MappedSuperclass.class); + } + + @Test + public void persistenceEmbeddable() throws IOException { + testSingleComponent(SampleEmbeddable.class, Embeddable.class); + } + + @Test + public void persistenceConverter() throws IOException { + testSingleComponent(SampleConverter.class, Converter.class); + } + + @Test + public void packageInfo() throws IOException { + CandidateComponentsMetadata metadata = compile( + "org/springframework/context/index/sample/jpa/package-info"); + assertThat(metadata, hasComponent( + "org.springframework.context.index.sample.jpa", "package-info")); + } + + @Test + public void typeStereotypeFromMetaInterface() throws IOException { + testSingleComponent(SampleSpecializedRepo.class, Repo.class); + } + + @Test + public void typeStereotypeFromInterfaceFromSuperClass() throws IOException { + testSingleComponent(SampleRepo.class, Repo.class); + } + + @Test + public void typeStereotypeFromSeveralInterfaces() throws IOException { + testSingleComponent(SampleSmartRepo.class, Repo.class, SmartRepo.class); + } + + @Test + public void typeStereotypeOnInterface() throws IOException { + testSingleComponent(SpecializedRepo.class, Repo.class); + } + + @Test + public void typeStereotypeOnInterfaceFromSeveralInterfaces() throws IOException { + testSingleComponent(SmartRepo.class, Repo.class, SmartRepo.class); + } + + @Test + public void typeStereotypeOnIndexedInterface() throws IOException { + testSingleComponent(Repo.class, Repo.class); + } + + + private void testComponent(Class... classes) throws IOException { + CandidateComponentsMetadata metadata = compile(classes); + for (Class c : classes) { + assertThat(metadata, hasComponent(c, Component.class)); + } + assertThat(metadata.getItems(), hasSize(classes.length)); + } + + private void testSingleComponent(Class target, Class... stereotypes) throws IOException { + CandidateComponentsMetadata metadata = compile(target); + assertThat(metadata, hasComponent(target, stereotypes)); + assertThat(metadata.getItems(), hasSize(1)); + } + + private CandidateComponentsMetadata compile(Class... types) throws IOException { + CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); + this.compiler.getTask(types).call(processor); + return readGeneratedMetadata(this.compiler.getOutputLocation()); + } + + private CandidateComponentsMetadata compile(String... types) throws IOException { + CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); + this.compiler.getTask(types).call(processor); + return readGeneratedMetadata(this.compiler.getOutputLocation()); + } + + private CandidateComponentsMetadata readGeneratedMetadata(File outputLocation) { + try { + File metadataFile = new File(outputLocation, + MetadataStore.METADATA_PATH); + if (metadataFile.isFile()) { + return new PropertiesMarshaller() + .read(new FileInputStream(metadataFile)); + } + else { + return new CandidateComponentsMetadata(); + } + } + catch (IOException e) { + throw new RuntimeException("Failed to read metadata from disk", e); + } + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/metadata/PropertiesMarshallerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/metadata/PropertiesMarshallerTests.java new file mode 100644 index 00000000000..1914fab2767 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/metadata/PropertiesMarshallerTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.metadata; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.context.index.test.Metadata.*; + +/** + * Tests for {@link PropertiesMarshaller}. + * + * @author Stephane Nicoll + */ +public class PropertiesMarshallerTests { + + @Test + public void readWrite() throws IOException { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + metadata.add(createItem("com.foo", "first", "second")); + metadata.add(createItem("com.bar", "first")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PropertiesMarshaller marshaller = new PropertiesMarshaller(); + marshaller.write(metadata, outputStream); + CandidateComponentsMetadata readMetadata = marshaller.read( + new ByteArrayInputStream(outputStream.toByteArray())); + assertThat(readMetadata, hasComponent("com.foo", "first", "second")); + assertThat(readMetadata, hasComponent("com.bar", "first")); + assertThat(readMetadata.getItems(), hasSize(2)); + } + + private static ItemMetadata createItem(String type, String... stereotypes) { + return new ItemMetadata(type, new HashSet<>(Arrays.asList(stereotypes))); + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java new file mode 100644 index 00000000000..156ecf14088 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Abstract {@link Component} that shouldn't be registered. + * + * @author Stephane Nicoll + */ +@Component +public abstract class AbstractController { + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java new file mode 100644 index 00000000000..813228f7cad --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Controller; + +/** + * Sample meta-annotation. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +public @interface MetaController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java new file mode 100644 index 00000000000..9e66cbcc5e8 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Indexed; + +/** + * A test annotation that triggers a dedicated entry in the index. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +@Indexed +public @interface MetaControllerIndexed { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java new file mode 100644 index 00000000000..621c7e0d655 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Test candidate for {@link Component}. + * + * @author Stephane Nicoll + */ +@Component +public class SampleComponent { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java new file mode 100644 index 00000000000..e7593d0e7de --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import org.springframework.stereotype.Controller; + +/** + * Test candidate for {@link Controller}. + * + * @author Stephane Nicoll + */ +@Controller +public class SampleController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java new file mode 100644 index 00000000000..314cb496323 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +/** + * Test candidate for a {@code Controller} defined using a meta-annotation. + * + * @author Stephane Nicoll + */ +@MetaController +public class SampleMetaController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java new file mode 100644 index 00000000000..c620da58ca5 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +/** + * Test candidate for a {@code Controller} that adds both the + * {@code Component} and {@code MetaControllerIndexed} stereotypes. + * + * @author Stephane Nicoll + */ +@MetaControllerIndexed +public class SampleMetaIndexedController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java new file mode 100644 index 00000000000..145e89f923d --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.type.Scope; + +/** + * Candidate with no matching annotation. + * + * @author Stephane Nicoll + */ +@Scope("None") +@Qualifier("None") +public class SampleNone { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java new file mode 100644 index 00000000000..b254ee7624a --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import org.springframework.stereotype.Repository; + +/** + * Test candidate for {@link Repository}. + * + * @author Stephane Nicoll + */ +@Repository +public class SampleRepository { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java new file mode 100644 index 00000000000..2ed7e6025ce --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample; + +import org.springframework.stereotype.Service; + +/** + * Test candidate for {@link Service}. + * + * @author Stephane Nicoll + */ +@Service +public class SampleService { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java new file mode 100644 index 00000000000..54d09f1d748 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.cdi; + +import javax.annotation.ManagedBean; + +/** + * Test candidate for a CDI {@link ManagedBean}. + * + * @author Stephane Nicoll + */ +@ManagedBean +public class SampleManagedBean { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java new file mode 100644 index 00000000000..27364da26de --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.cdi; + +import javax.inject.Named; + +/** + * Test candidate for a CDI {@link Named} bean. + * + * @author Stephane Nicoll + */ +@Named +public class SampleNamed { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java new file mode 100644 index 00000000000..25e01ca3061 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.jpa; + +import javax.persistence.Converter; + +/** + * Test candidate for {@link Converter}. + * + * @author Stephane Nicoll + */ +@Converter +public class SampleConverter { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java new file mode 100644 index 00000000000..53204e472b8 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.jpa; + +import javax.persistence.Embeddable; + +/** + * Test candidate for {@link Embeddable}. + * + * @author Stephane Nicoll + */ +@Embeddable +public class SampleEmbeddable { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java new file mode 100644 index 00000000000..b03bdf7619d --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.jpa; + +import javax.persistence.Entity; + +/** + * Test candidate for {@link Entity}. + * + * @author Stephane Nicoll + */ +@Entity +public class SampleEntity { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java new file mode 100644 index 00000000000..2e922741831 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.jpa; + +import javax.persistence.MappedSuperclass; + +/** + * Test candidate for {@link MappedSuperclass}. + * + * @author Stephane Nicoll + */ +@MappedSuperclass +public abstract class SampleMappedSuperClass { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java new file mode 100644 index 00000000000..0cbf1db3876 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Test candidate for {@code package-info}. + * + * @author Stephane Nicoll + */ +package org.springframework.context.index.sample.jpa; \ No newline at end of file diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java new file mode 100644 index 00000000000..da194359e62 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractRepo implements Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java new file mode 100644 index 00000000000..341b28123de --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +import org.springframework.stereotype.Indexed; + +/** + * A sample interface flagged with {@link Indexed} to indicate that a stereotype + * for all implementations should be added to the index. + * + * @author Stephane Nicoll + */ +@Indexed +public interface Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java new file mode 100644 index 00000000000..83830aaeb38 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public class SampleEntity { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java new file mode 100644 index 00000000000..4d52f68de16 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +/** + * A sample that gets its stereotype via an abstract class. + * + * @author Stephane Nicoll + */ +public class SampleRepo extends AbstractRepo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java new file mode 100644 index 00000000000..755d2a15fe0 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +/** + * A sample that implements both interface used to demonstrate that no + * duplicate stereotypes are generated. + * + * @author Stephane Nicoll + */ +public class SampleSmartRepo + implements SmartRepo, Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java new file mode 100644 index 00000000000..fa932a36b5e --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +/** + * A sample that does not directly implement the {@link Repo} interface. + * + * @author Stephane Nicoll + */ +public class SampleSpecializedRepo implements SpecializedRepo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java new file mode 100644 index 00000000000..b7dc3b0a1b6 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +import org.springframework.stereotype.Indexed; + +/** + * A {@link Repo} that requires an extra stereotype. + * + * @author Stephane Nicoll + */ +@Indexed +public interface SmartRepo extends Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java new file mode 100644 index 00000000000..4c2e31e825a --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public interface SpecializedRepo extends Repo { + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/test/Metadata.java b/spring-context-indexer/src/test/java/org/springframework/context/index/test/Metadata.java new file mode 100644 index 00000000000..e5f52000ad5 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/test/Metadata.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +import org.springframework.context.index.metadata.ItemMetadata; +import org.springframework.context.index.metadata.CandidateComponentsMetadata; + +/** + * Hamcrest {@link org.hamcrest.Matcher Matcher} to help test {@link CandidateComponentsMetadata}. + * + * @author Stephane Nicoll + */ +public class Metadata { + + public static ItemMetadataMatcher hasComponent(Class type, Class... stereotypes) { + return new ItemMetadataMatcher(type.getName(), stereotypes); + } + + public static ItemMetadataMatcher hasComponent(String type, String... stereotypes) { + return new ItemMetadataMatcher(type, stereotypes); + } + + private static class ItemMetadataMatcher extends BaseMatcher { + + private final String type; + + private final List stereotypes; + + private ItemMetadataMatcher(String type, List stereotypes) { + this.type = type; + this.stereotypes = stereotypes; + } + + public ItemMetadataMatcher(String type, String... stereotypes) { + this(type, Arrays.asList(stereotypes)); + } + + public ItemMetadataMatcher(String type, Class... stereotypes) { + this(type, Arrays.stream(stereotypes) + .map(Class::getName).collect(Collectors.toList())); + } + + @Override + public boolean matches(Object value) { + if (!(value instanceof CandidateComponentsMetadata)) { + return false; + } + ItemMetadata itemMetadata = getFirstItemWithType((CandidateComponentsMetadata) value, this.type); + if (itemMetadata == null) { + return false; + } + if (this.type != null && !this.type.equals(itemMetadata.getType())) { + return false; + } + if (this.stereotypes != null) { + for (String stereotype : this.stereotypes) { + if (!itemMetadata.getStereotypes().contains(stereotype)) { + return false; + } + } + if (this.stereotypes.size() != itemMetadata.getStereotypes().size()) { + return false; + } + } + return true; + } + + private ItemMetadata getFirstItemWithType(CandidateComponentsMetadata metadata, String type) { + for (ItemMetadata item : metadata.getItems()) { + if (item.getType().equals(type)) { + return item; + } + } + return null; + } + + @Override + public void describeTo(Description description) { + description.appendText("Candidates with type ").appendValue(this.type); + description.appendText(" and stereotypes ").appendValue(this.stereotypes); + } + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java b/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java new file mode 100644 index 00000000000..ab88bd679d2 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.test; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.processing.Processor; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import org.junit.rules.TemporaryFolder; + +/** + * Wrapper to make the {@link JavaCompiler} easier to use in tests. + * + * @author Stephane Nicoll + */ +public class TestCompiler { + + public static final File ORIGINAL_SOURCE_FOLDER = new File("src/test/java"); + + private final JavaCompiler compiler; + + private final StandardJavaFileManager fileManager; + + private final File outputLocation; + + public TestCompiler(TemporaryFolder temporaryFolder) throws IOException { + this(ToolProvider.getSystemJavaCompiler(), temporaryFolder); + } + + public TestCompiler(JavaCompiler compiler, TemporaryFolder temporaryFolder) + throws IOException { + this.compiler = compiler; + this.fileManager = compiler.getStandardFileManager(null, null, null); + this.outputLocation = temporaryFolder.newFolder(); + Iterable temp = Collections.singletonList(this.outputLocation); + this.fileManager.setLocation(StandardLocation.CLASS_OUTPUT, temp); + this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp); + } + + public TestCompilationTask getTask(Class... types) { + List names = Arrays.stream(types).map(Class::getName) + .collect(Collectors.toList()); + return getTask(names.toArray(new String[names.size()])); + } + + public TestCompilationTask getTask(String... types) { + Iterable javaFileObjects = getJavaFileObjects(types); + return getTask(javaFileObjects); + } + + private TestCompilationTask getTask( + Iterable javaFileObjects) { + return new TestCompilationTask(this.compiler.getTask(null, this.fileManager, null, + null, null, javaFileObjects)); + } + + public File getOutputLocation() { + return this.outputLocation; + } + + private Iterable getJavaFileObjects(String... types) { + File[] files = new File[types.length]; + for (int i = 0; i < types.length; i++) { + files[i] = getFile(types[i]); + } + return this.fileManager.getJavaFileObjects(files); + } + + private File getFile(String type) { + return new File(getSourceFolder(), sourcePathFor(type)); + } + + private static String sourcePathFor(String type) { + return type.replace(".", "/") + ".java"; + } + + private File getSourceFolder() { + return ORIGINAL_SOURCE_FOLDER; + } + + /** + * A compilation task. + */ + public static class TestCompilationTask { + + private final JavaCompiler.CompilationTask task; + + public TestCompilationTask(JavaCompiler.CompilationTask task) { + this.task = task; + } + + public void call(Processor... processors) { + this.task.setProcessors(Arrays.asList(processors)); + if (!this.task.call()) { + throw new IllegalStateException("Compilation failed"); + } + } + + } + +} \ No newline at end of file diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index aed66e9b1ad..967bc68efc1 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -18,6 +18,7 @@ package org.springframework.context.annotation; import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; @@ -28,9 +29,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.index.CandidateComponentsIndex; +import org.springframework.context.index.CandidateComponentsIndexLoader; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; @@ -43,17 +48,24 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.TypeFilter; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Indexed; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * A component provider that scans the classpath from a base package. It then - * applies exclude and include filters to the resulting classes to find candidates. + * A component provider that provides candidate components from a base package. Can + * use {@link CandidateComponentsIndex the index} if it is available of scans the + * classpath otherwise. Candidate components are identified by applying exclude and + * include filters. {@link AnnotationTypeFilter}, {@link AssignableTypeFilter} include + * filters on an annotation/super-class that are annotated with {@link Indexed} are + * supported: if any other include filter is specified, the index is ignored and + * classpath scanning is used instead. * *

This implementation is based on Spring's * {@link org.springframework.core.type.classreading.MetadataReader MetadataReader} @@ -63,10 +75,12 @@ import org.springframework.util.ClassUtils; * @author Juergen Hoeller * @author Ramnivas Laddad * @author Chris Beams + * @author Stephane Nicoll * @since 2.5 * @see org.springframework.core.type.classreading.MetadataReaderFactory * @see org.springframework.core.type.AnnotationMetadata * @see ScannedGenericBeanDefinition + * @see CandidateComponentsIndex */ public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { @@ -81,6 +95,8 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); + private CandidateComponentsIndex componentsIndex; + private String resourcePattern = DEFAULT_RESOURCE_PATTERN; private final List includeFilters = new LinkedList<>(); @@ -132,6 +148,7 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC public void setResourceLoader(ResourceLoader resourceLoader) { this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(resourceLoader.getClassLoader()); } /** @@ -263,6 +280,58 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC * @return a corresponding Set of autodetected bean definitions */ public Set findCandidateComponents(String basePackage) { + if (isIndexSupported()) { + return addCandidateComponentsFromIndex(basePackage); + } + else { + return scanCandidateComponents(basePackage); + } + } + + protected Set addCandidateComponentsFromIndex(String basePackage) { + Set candidates = new LinkedHashSet<>(); + try { + Set types = new HashSet<>(); + for (TypeFilter filter : this.includeFilters) { + String stereotype = extractStereotype(filter); + if (stereotype == null) { + throw new IllegalArgumentException("Failed to extract stereotype from "+ filter); + } + types.addAll(this.componentsIndex.getCandidateTypes(basePackage, stereotype)); + } + boolean traceEnabled = logger.isTraceEnabled(); + boolean debugEnabled = logger.isDebugEnabled(); + for (String type : types) { + MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(type); + if (isCandidateComponent(metadataReader)) { + AnnotatedGenericBeanDefinition sbd = new AnnotatedGenericBeanDefinition( + metadataReader.getAnnotationMetadata()); + if (isCandidateComponent(sbd)) { + if (debugEnabled) { + logger.debug("Using candidate component class from index: " + type); + } + candidates.add(sbd); + } + else { + if (debugEnabled) { + logger.debug("Ignored because not a concrete top-level class: " + type); + } + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because matching an exclude filter: " + type); + } + } + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); + } + return candidates; + } + + protected Set scanCandidateComponents(String basePackage) { Set candidates = new LinkedHashSet<>(); try { String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + @@ -374,6 +443,57 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC return (beanDefinition.getMetadata().isConcrete() && beanDefinition.getMetadata().isIndependent()); } + /** + * Determine if the index can be used by this instance. + * @return {@code true} if the index is available and the configuration of this + * instance is supported by it, {@code false otherwise}. + */ + protected boolean isIndexSupported() { + if (this.componentsIndex == null) { + return false; + } + for (TypeFilter includeFilter : this.includeFilters) { + if (!isIndexSupportsIncludeFilter(includeFilter)) { + return false; + } + } + return true; + } + + /** + * Determine if the specified include {@link TypeFilter} is supported by the index. + * @param filter the filter to check + * @return whether the index supports this include filter + * @see #extractStereotype(TypeFilter) + */ + protected boolean isIndexSupportsIncludeFilter(TypeFilter filter) { + if (filter instanceof AnnotationTypeFilter) { + Class annotation = ((AnnotationTypeFilter) filter).getAnnotationType(); + return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotation) + || annotation.getName().startsWith("javax.")); + } + if (filter instanceof AssignableTypeFilter) { + Class target = ((AssignableTypeFilter) filter).getTargetType(); + return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target); + } + return false; + } + + /** + * Extract the stereotype to use for the specified compatible filter. + * @param filter the filter to handle + * @return the stereotype in the index matching this filter + * @see #isIndexSupportsIncludeFilter(TypeFilter) + */ + protected String extractStereotype(TypeFilter filter) { + if (filter instanceof AnnotationTypeFilter) { + return ((AnnotationTypeFilter) filter).getAnnotationType().getName(); + } + if (filter instanceof AssignableTypeFilter) { + return ((AssignableTypeFilter) filter).getTargetType().getName(); + } + return null; + } /** * Clear the underlying metadata cache, removing all cached class metadata. diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java new file mode 100644 index 00000000000..16dc5f6d717 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Provide access to the candidates that are defined in {@code META-INF/spring.components}. + *

+ * An arbitrary number of stereotypes can be registered (and queried) on the index: a + * typical example is the fully qualified name of an annotation that flags the class for + * a certain use case. The following call returns all the {@code @Component} + * candidate types for the {@code com.example} package (and its sub-packages): + *

+ * Set<String> candidates = index.getCandidateTypes(
+ *         "com.example", "org.springframework.stereotype.Component");
+ * 
+ * + * The {@code type} is usually the fully qualified name of a class, though this is + * not a rule. Similarly, the {@code stereotype} is usually the fully qualified name of + * a target type but it can be any marker really. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class CandidateComponentsIndex { + + private final MultiValueMap index; + + CandidateComponentsIndex(List content) { + this.index = parseIndex(content); + } + + /** + * Return the candidate types that are associated with the specified stereotype. + * @param basePackage the package to check for candidates + * @param stereotype the stereotype to use + * @return the candidate types associated with the specified {@code stereotype} + * or an empty set if none has been found for the specified {@code basePackage} + */ + public Set getCandidateTypes(String basePackage, String stereotype) { + List candidates = this.index.get(stereotype); + if (candidates != null) { + return candidates.parallelStream() + .filter(t -> t.startsWith(basePackage)) + .collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + + private static MultiValueMap parseIndex(List content) { + MultiValueMap index = new LinkedMultiValueMap<>(); + for (Properties entry : content) { + for (Map.Entry entries : entry.entrySet()) { + String type = (String) entries.getKey(); + String[] stereotypes = ((String) entries.getValue()).split(","); + for (String stereotype : stereotypes) { + index.add(stereotype, type); + } + } + } + return index; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java new file mode 100644 index 00000000000..103df5f9c25 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.SpringProperties; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Candidate components index loading mechanism for internal use within the framework. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class CandidateComponentsIndexLoader { + + + private static final Log logger = LogFactory.getLog(CandidateComponentsIndexLoader.class); + + /** + * System property that instructs Spring to ignore the index, i.e. + * to always return {@code null} from {@link #loadIndex(ClassLoader)}. + *

The default is "false", allowing for regular use of the index. Switching this + * flag to {@code true} fulfills a corner case scenario when an index is partially + * available for some libraries (or use cases) but couldn't be built for the whole + * application. In this case, the application context fallbacks to a regular + * classpath arrangement (i.e. as no index was present at all). + */ + public static final String IGNORE_INDEX = "spring.index.ignore"; + + private static final boolean shouldIgnoreIndex = + SpringProperties.getFlag(IGNORE_INDEX); + + + /** + * The location to look for components. + *

Can be present in multiple JAR files. + */ + public static final String COMPONENTS_RESOURCE_LOCATION = "META-INF/spring.components"; + + private static final ConcurrentMap cache + = new ConcurrentReferenceHashMap<>(); + + + /** + * Load and instantiate the {@link CandidateComponentsIndex} from + * {@value #COMPONENTS_RESOURCE_LOCATION}, using the given class loader. If no + * index is available, return {@code null}. + * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) + * @return the index to use or {@code null} if no index was found + * @throws IllegalArgumentException if any module index cannot + * be loaded or if an error occurs while creating {@link CandidateComponentsIndex} + */ + public static CandidateComponentsIndex loadIndex(ClassLoader classLoader) { + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = CandidateComponentsIndexLoader.class.getClassLoader(); + } + return cache.computeIfAbsent(classLoaderToUse, CandidateComponentsIndexLoader::doLoadIndex); + } + + private static CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) { + if (shouldIgnoreIndex) { + return null; + } + try { + Enumeration urls = classLoader.getResources(COMPONENTS_RESOURCE_LOCATION); + if (!urls.hasMoreElements()) { + return null; + } + List result = new ArrayList<>(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + result.add(properties); + } + if (logger.isTraceEnabled()) { + logger.trace("Loaded " + result.size() + "] index(es)"); + } + int totalCount = result.stream().mapToInt(Properties::size).sum(); + return (totalCount > 0 ? new CandidateComponentsIndex(result) : null); + } + catch (IOException ex) { + throw new IllegalArgumentException("Unable to load indexes from location [" + + COMPONENTS_RESOURCE_LOCATION + "]", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/index/package-info.java b/spring-context/src/main/java/org/springframework/context/index/package-info.java new file mode 100644 index 00000000000..d0437fdf9a3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support package for reading and managing the components index. + */ +package org.springframework.context.index; \ No newline at end of file diff --git a/spring-context/src/main/java/org/springframework/stereotype/Component.java b/spring-context/src/main/java/org/springframework/stereotype/Component.java index 8b3c75dffc5..a6fd5b7ca56 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Component.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Component.java @@ -42,6 +42,7 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented +@Indexed public @interface Component { /** diff --git a/spring-context/src/main/java/org/springframework/stereotype/Indexed.java b/spring-context/src/main/java/org/springframework/stereotype/Indexed.java new file mode 100644 index 00000000000..17d2512e791 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/Indexed.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicate that the annotated element represents a stereotype for the index. + *

+ * The {@code CandidateComponentsIndex} is an alternative to classpath + * scanning that uses a metadata file generated at compilation time. The + * index allows retrieving the candidate components (i.e. fully qualified + * name) based on a stereotype. This annotation instructs the generator to + * index the element on which the annotated element is present or if it + * implements or extends from the annotated element. The stereotype is the + * fully qualified name of the annotated element. + *

+ * Consider the default {@link Component} annotation that is meta-annotated + * with this annotation. If a component is annotated with {@link Component}, + * an entry for that component will be added to the index using the + * {@code org.springframework.stereotype.Component} stereotype. + *

+ * This annotation is also honored on meta-annotations. Consider this + * custom annotation: + *

+ * package com.example;
+ *
+ * @Target(ElementType.TYPE)
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @Documented
+ * @Indexed
+ * @Service
+ * public @interface PrivilegedService { ... }
+ * 
+ * If this annotation is present on an type, it will be indexed with two + * stereotypes: {@code org.springframework.stereotype.Component} and + * {@code com.example.PrivilegedService}. While {@link Service} isn't directly + * annotated with {@code Indexed}, it is meta-annotated with {@link Component}. + *

+ * It is also possible to index all implementations of a certain interface or + * all the sub-classes of a given class by adding {@code @Indexed} on it. + * Consider this base interface: + *

+ * package com.example;
+ *
+ * @Indexed
+ * public interface AdminService { ... }
+ * 
+ * Now, consider an implementation of this {@code AdminService} somewhere: + *
+ * package com.example.foo;
+ *
+ * import com.example.AdminService;
+ *
+ * public class ConfigurationAdminService implements AdminService { ... }
+ * 
+ * Because this class implements an interface that is indexed, it will be + * automatically included with the {@code com.example.AdminService} stereotype. + * If there are more {@code @Indexed} interfaces and/or super classes in the + * hierarchy, the class will map to all their stereotypes. + * + * @author Stephane Nicoll + * @since 4.3.3 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Indexed { +} diff --git a/spring-context/src/test/java/example/scannable/FooService.java b/spring-context/src/test/java/example/scannable/FooService.java index df48735445f..3f259040ca6 100644 --- a/spring-context/src/test/java/example/scannable/FooService.java +++ b/spring-context/src/test/java/example/scannable/FooService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,13 @@ package example.scannable; import java.util.concurrent.Future; import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Indexed; /** * @author Mark Fisher * @author Juergen Hoeller */ +@Indexed public interface FooService { String foo(int id); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java index b8c33f77832..6537777beb8 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,20 +24,28 @@ import java.util.regex.Pattern; import example.profilescan.DevComponent; import example.profilescan.ProfileAnnotatedComponent; import example.profilescan.ProfileMetaAnnotatedComponent; +import example.scannable.AutowiredQualifierFooService; +import example.scannable.CustomStereotype; +import example.scannable.DefaultNamedComponent; import example.scannable.FooDao; import example.scannable.FooService; import example.scannable.FooServiceImpl; import example.scannable.MessageBean; import example.scannable.NamedComponent; import example.scannable.NamedStubDao; +import example.scannable.ScopedProxyTestBean; import example.scannable.ServiceInvocationCounter; import example.scannable.StubFooDao; import org.aspectj.lang.annotation.Aspect; import org.junit.Test; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.index.CandidateComponentsTestClassLoader; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.RegexPatternTypeFilter; @@ -53,6 +61,7 @@ import static org.junit.Assert.*; * @author Mark Fisher * @author Juergen Hoeller * @author Chris Beams + * @author Stephane Nicoll */ public class ClassPathScanningCandidateComponentProviderTests { @@ -60,34 +69,192 @@ public class ClassPathScanningCandidateComponentProviderTests { private static final String TEST_PROFILE_PACKAGE = "example.profilescan"; private static final String TEST_DEFAULT_PROFILE_NAME = "testDefault"; + private static final ClassLoader TEST_BASE_CLASSLOADER = CandidateComponentsTestClassLoader.index( + ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), + new ClassPathResource("spring.components", NamedComponent.class)); + + + @Test + public void defaultsWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testDefault(provider, ScannedGenericBeanDefinition.class); + } @Test - public void testWithDefaults() { + public void defaultsWithIndex() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testDefault(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testDefault(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertEquals(6, candidates.size()); + assertTrue(containsBeanClass(candidates, DefaultNamedComponent.class)); assertTrue(containsBeanClass(candidates, NamedComponent.class)); assertTrue(containsBeanClass(candidates, FooServiceImpl.class)); assertTrue(containsBeanClass(candidates, StubFooDao.class)); assertTrue(containsBeanClass(candidates, NamedStubDao.class)); assertTrue(containsBeanClass(candidates, ServiceInvocationCounter.class)); + assertEquals(6, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); } @Test - public void testWithBogusBasePackage() { + public void bogusPackageWithScan() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); Set candidates = provider.findCandidateComponents("bogus"); assertEquals(0, candidates.size()); } @Test - public void testWithPackageExcludeFilter() { + public void bogusPackageWithIndex() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); - provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*"))); - Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + Set candidates = provider.findCandidateComponents("bogus"); assertEquals(0, candidates.size()); } + @Test + public void customFiltersFollowedByResetUseIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.resetFilters(true); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertBeanDefinitionType(candidates, AnnotatedGenericBeanDefinition.class); + } + + @Test + public void customAnnotationTypeIncludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomAnnotationTypeIncludeFilter(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void customAnnotationTypeIncludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomAnnotationTypeIncludeFilter(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + testDefault(provider, expectedBeanDefinitionType); + } + + @Test + public void customAssignableTypeIncludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomAssignableTypeIncludeFilter(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void customAssignableTypeIncludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomAssignableTypeIncludeFilter(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testCustomAssignableTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + provider.addIncludeFilter(new AssignableTypeFilter(FooService.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + // Interfaces/Abstract class are filtered out automatically. + assertTrue(containsBeanClass(candidates, AutowiredQualifierFooService.class)); + assertTrue(containsBeanClass(candidates, FooServiceImpl.class)); + assertTrue(containsBeanClass(candidates, ScopedProxyTestBean.class)); + assertEquals(3, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); + } + + @Test + public void customSupportedIncludeAndExcludedFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomSupportedIncludeAndExcludeFilter(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void customSupportedIncludeAndExcludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomSupportedIncludeAndExcludeFilter(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testCustomSupportedIncludeAndExcludeFilter(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, NamedComponent.class)); + assertTrue(containsBeanClass(candidates, ServiceInvocationCounter.class)); + assertEquals(2, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); + } + + @Test + public void customSupportIncludeFilterWithNonIndexedTypeUseScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + // This annotation type is not directly annotated with Indexed so we can use + // the index to find candidates + provider.addIncludeFilter(new AnnotationTypeFilter(CustomStereotype.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, DefaultNamedComponent.class)); + assertEquals(1, candidates.size()); + assertBeanDefinitionType(candidates, ScannedGenericBeanDefinition.class); + } + + @Test + public void customNotSupportedIncludeFilterUseScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addIncludeFilter(new AssignableTypeFilter(FooDao.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, StubFooDao.class)); + assertEquals(1, candidates.size()); + assertBeanDefinitionType(candidates, ScannedGenericBeanDefinition.class); + } + + @Test + public void excludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*Named.*"))); + testExclude(provider, ScannedGenericBeanDefinition.class); + } + + @Test + public void excludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*Named.*"))); + testExclude(provider, AnnotatedGenericBeanDefinition.class); + } + + private void testExclude(ClassPathScanningCandidateComponentProvider provider, + Class expectedBeanDefinitionType) { + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertTrue(containsBeanClass(candidates, FooServiceImpl.class)); + assertTrue(containsBeanClass(candidates, StubFooDao.class)); + assertTrue(containsBeanClass(candidates, ServiceInvocationCounter.class)); + assertEquals(3, candidates.size()); + assertBeanDefinitionType(candidates, expectedBeanDefinitionType); + } + @Test public void testWithNoFilters() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); @@ -306,14 +473,20 @@ public class ClassPathScanningCandidateComponentProviderTests { private boolean containsBeanClass(Set candidates, Class beanClass) { for (BeanDefinition candidate : candidates) { - ScannedGenericBeanDefinition definition = (ScannedGenericBeanDefinition) candidate; - if (beanClass.getName().equals(definition.getBeanClassName())) { + if (beanClass.getName().equals(candidate.getBeanClassName())) { return true; } } return false; } + private void assertBeanDefinitionType(Set candidates, + Class expectedType) { + candidates.forEach(c -> { + assertThat(c, is(instanceOf(expectedType))); + }); + } + @Profile(TEST_DEFAULT_PROFILE_NAME) @Component(DefaultProfileAnnotatedComponent.BEAN_NAME) diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java new file mode 100644 index 00000000000..674c3c94811 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.io.IOException; +import java.util.Set; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.io.ClassPathResource; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link CandidateComponentsIndexLoader}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexLoaderTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void validateIndexIsDisabledByDefault() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex(null); + assertThat("No spring.components should be available at the default location", index, is(nullValue())); + } + + @Test + public void loadIndexSeveralMatches() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "foo"); + assertThat(components, containsInAnyOrder( + "org.springframework.context.index.Sample1", + "org.springframework.context.index.Sample2")); + } + + @Test + public void loadIndexSingleMatch() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "biz"); + assertThat(components, containsInAnyOrder( + "org.springframework.context.index.Sample3")); + } + + @Test + public void loadIndexNoMatch() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "none"); + assertThat(components, hasSize(0)); + } + + @Test + public void loadIndexNoPackage() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("com.example", "foo"); + assertThat(components, hasSize(0)); + } + + @Test + public void loadIndexNoSpringComponentsResource() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader())); + assertThat(index, is(nullValue())); + } + + @Test + public void loadIndexNoEntry() throws IOException { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("empty-spring.components", getClass()))); + assertThat(index, is(nullValue())); + } + + @Test + public void loadIndexWithException() throws IOException { + final IOException cause = new IOException("test exception"); + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Unable to load indexes"); + this.thrown.expectCause(is(cause)); + CandidateComponentsIndexLoader.loadIndex(new CandidateComponentsTestClassLoader( + getClass().getClassLoader(), cause)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java new file mode 100644 index 00000000000..b153dc65aff --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; +import java.util.Set; + +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link CandidateComponentsIndex}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexTests { + + @Test + public void getCandidateTypes() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service", "service"); + assertThat(actual, containsInAnyOrder("com.example.service.One", + "com.example.service.sub.Two", "com.example.service.Three")); + } + + @Test + public void getCandidateTypesSubPackage() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service.sub", "service"); + assertThat(actual, containsInAnyOrder("com.example.service.sub.Two")); + } + + @Test + public void getCandidateTypesSubPackageNoMatch() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service.none", "service"); + assertThat(actual, hasSize(0)); + } + + @Test + public void getCandidateTypesNoMatch() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service", "entity"); + assertThat(actual, hasSize(0)); + } + + @Test + public void mergeCandidateStereotypes() { + CandidateComponentsIndex index = new CandidateComponentsIndex(Arrays.asList( + createProperties("com.example.Foo", "service"), + createProperties("com.example.Foo", "entity"))); + assertThat(index.getCandidateTypes("com.example", "service"), + contains("com.example.Foo")); + assertThat(index.getCandidateTypes("com.example", "entity"), + contains("com.example.Foo")); + } + + private static Properties createProperties(String key, String stereotypes) { + Properties properties = new Properties(); + properties.put(key, String.join(",", stereotypes)); + return properties; + } + + private static Properties createSampleProperties() { + Properties properties = new Properties(); + properties.put("com.example.service.One", "service"); + properties.put("com.example.service.sub.Two", "service"); + properties.put("com.example.service.Three", "service"); + properties.put("com.example.domain.Four", "entity"); + return properties; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsTestClassLoader.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsTestClassLoader.java new file mode 100644 index 00000000000..10697608d09 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsTestClassLoader.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.io.Resource; + +/** + * A test {@link ClassLoader} that can be used in testing context to control the + * {@code spring.components} resource that should be loaded. Can also simulate a failure + * by throwing a configurable {@link IOException}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsTestClassLoader extends ClassLoader { + + /** + * Create a test {@link ClassLoader} that disable the use of the index, even + * if resources are present at the standard location. + * @param classLoader the classloader to use for all other operations + * @return a test {@link ClassLoader} that has no index + * @see CandidateComponentsIndexLoader#COMPONENTS_RESOURCE_LOCATION + */ + public static ClassLoader disableIndex(ClassLoader classLoader) { + return new CandidateComponentsTestClassLoader(classLoader, + Collections.enumeration(Collections.emptyList())); + } + + /** + * Create a test {@link ClassLoader} that creates an index with the + * specifed {@link Resource} instances + * @param classLoader the classloader to use for all other operations + * @return a test {@link ClassLoader} with an index built based on the + * specified resources. + */ + public static ClassLoader index(ClassLoader classLoader, Resource... resources) { + return new CandidateComponentsTestClassLoader(classLoader, + Collections.enumeration(Stream.of(resources).map(r -> { + try { + return r.getURL(); + } + catch (Exception ex) { + throw new IllegalArgumentException("Invalid resource " + r, ex); + } + }).collect(Collectors.toList()))); + } + + + private final Enumeration resourceUrls; + + private final IOException cause; + + public CandidateComponentsTestClassLoader(ClassLoader classLoader, Enumeration resourceUrls) { + super(classLoader); + this.resourceUrls = resourceUrls; + this.cause = null; + } + + public CandidateComponentsTestClassLoader(ClassLoader parent, IOException cause) { + super(parent); + this.resourceUrls = null; + this.cause = cause; + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (CandidateComponentsIndexLoader.COMPONENTS_RESOURCE_LOCATION.equals(name)) { + if (this.resourceUrls != null) { + return this.resourceUrls; + } + throw this.cause; + } + return super.getResources(name); + } + +} diff --git a/spring-context/src/test/resources/example/scannable/spring.components b/spring-context/src/test/resources/example/scannable/spring.components new file mode 100644 index 00000000000..62de4a95cdc --- /dev/null +++ b/spring-context/src/test/resources/example/scannable/spring.components @@ -0,0 +1,9 @@ +example.scannable.AutowiredQualifierFooService=example.scannable.FooService +example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component +example.scannable.NamedComponent=org.springframework.stereotype.Component +example.scannable.FooService=example.scannable.FooService +example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService +example.scannable.ScopedProxyTestBean=example.scannable.FooService +example.scannable.StubFooDao=org.springframework.stereotype.Component +example.scannable.NamedStubDao=org.springframework.stereotype.Component +example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components b/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components new file mode 100644 index 00000000000..405817f874c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components @@ -0,0 +1,3 @@ +# +# Empty file to validate that if there is no entry we get a "null" index. +# \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/index/spring.components b/spring-context/src/test/resources/org/springframework/context/index/spring.components new file mode 100644 index 00000000000..c03f2f061af --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/index/spring.components @@ -0,0 +1,3 @@ +org.springframework.context.index.Sample1=foo +org.springframework.context.index.Sample2=bar,foo +org.springframework.context.index.Sample3=biz \ No newline at end of file diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java index 2b2faa4d05f..3e31aca1615 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,14 @@ public class AnnotationTypeFilter extends AbstractTypeHierarchyTraversingFilter this.considerMetaAnnotations = considerMetaAnnotations; } + /** + * Return the {@link Annotation} that this instance is using to filter + * candidates. + * @since 5.0 + */ + public final Class getAnnotationType() { + return this.annotationType; + } @Override protected boolean matchSelf(MetadataReader metadataReader) { diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java index 63653046278..d67056ff791 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,13 @@ public class AssignableTypeFilter extends AbstractTypeHierarchyTraversingFilter this.targetType = targetType; } + /** + * Return the {@code type} that this instance is using to filter candidates. + * @since 5.0 + */ + public final Class getTargetType() { + return this.targetType; + } @Override protected boolean matchClassName(String className) { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 8230dd59c2e..2c236d2ebef 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -39,6 +39,7 @@ import org.junit.rules.ExpectedException; import org.springframework.core.annotation.AnnotationUtilsTests.WebController; import org.springframework.core.annotation.AnnotationUtilsTests.WebMapping; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Indexed; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -76,19 +77,19 @@ public class AnnotatedElementUtilsTests { @Test public void getMetaAnnotationTypesOnClassWithMetaDepth1() { Set names = getMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class); - assertEquals(names(Transactional.class, Component.class), names); + assertEquals(names(Transactional.class, Component.class, Indexed.class), names); names = getMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class.getName()); - assertEquals(names(Transactional.class, Component.class), names); + assertEquals(names(Transactional.class, Component.class, Indexed.class), names); } @Test public void getMetaAnnotationTypesOnClassWithMetaDepth2() { Set names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class); - assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class), names); + assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class, Indexed.class), names); names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName()); - assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class), names); + assertEquals(names(TransactionalComponent.class, Transactional.class, Component.class, Indexed.class), names); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 9a91be3af3a..11d8839b5ee 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -161,7 +161,7 @@ public class AnnotationMetadataTests { assertThat(metadata.hasAnnotation(Documented.class.getName()), is(true)); assertThat(metadata.hasAnnotation(Scope.class.getName()), is(false)); assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(false)); - assertThat(metadata.getAnnotationTypes().size(), is(3)); + assertThat(metadata.getAnnotationTypes().size(), is(4)); } /** diff --git a/spring-core/src/test/java/org/springframework/stereotype/Component.java b/spring-core/src/test/java/org/springframework/stereotype/Component.java index e0b637f815c..0df4525c669 100644 --- a/spring-core/src/test/java/org/springframework/stereotype/Component.java +++ b/spring-core/src/test/java/org/springframework/stereotype/Component.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented +@Indexed public @interface Component { /** diff --git a/spring-core/src/test/java/org/springframework/stereotype/Indexed.java b/spring-core/src/test/java/org/springframework/stereotype/Indexed.java new file mode 100644 index 00000000000..386e347e740 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/stereotype/Indexed.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Copy of the standard {@code Indexed} annotation for testing purposes. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Indexed { +} diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java index 7dbfd32155f..58217357011 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java @@ -40,6 +40,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.index.CandidateComponentsIndex; +import org.springframework.context.index.CandidateComponentsIndexLoader; import org.springframework.context.weaving.LoadTimeWeaverAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -76,6 +78,7 @@ import org.springframework.util.ResourceUtils; *

NOTE: Spring's JPA support requires JPA 2.1 or higher, as of Spring 5.0. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.0 * @see #setPersistenceXmlLocations * @see #setDataSourceLookup @@ -108,7 +111,7 @@ public class DefaultPersistenceUnitManager public final static String ORIGINAL_DEFAULT_PERSISTENCE_UNIT_NAME = "default"; - private static final Set entityTypeFilters; + private static final Set entityTypeFilters; static { entityTypeFilters = new LinkedHashSet<>(4); @@ -147,6 +150,8 @@ public class DefaultPersistenceUnitManager private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + private CandidateComponentsIndex componentsIndex; + private final Set persistenceUnitInfoNames = new HashSet<>(); private final Map persistenceUnitInfos = new HashMap<>(); @@ -406,6 +411,7 @@ public class DefaultPersistenceUnitManager @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(resourceLoader.getClassLoader()); } @@ -510,33 +516,11 @@ public class DefaultPersistenceUnitManager if (this.packagesToScan != null) { for (String pkg : this.packagesToScan) { - try { - String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + - ClassUtils.convertClassNameToResourcePath(pkg) + CLASS_RESOURCE_PATTERN; - Resource[] resources = this.resourcePatternResolver.getResources(pattern); - MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); - for (Resource resource : resources) { - if (resource.isReadable()) { - MetadataReader reader = readerFactory.getMetadataReader(resource); - String className = reader.getClassMetadata().getClassName(); - if (matchesFilter(reader, readerFactory)) { - scannedUnit.addManagedClassName(className); - if (scannedUnit.getPersistenceUnitRootUrl() == null) { - URL url = resource.getURL(); - if (ResourceUtils.isJarURL(url)) { - scannedUnit.setPersistenceUnitRootUrl(ResourceUtils.extractJarFileURL(url)); - } - } - } - else if (className.endsWith(PACKAGE_INFO_SUFFIX)) { - scannedUnit.addManagedPackage( - className.substring(0, className.length() - PACKAGE_INFO_SUFFIX.length())); - } - } - } + if (this.componentsIndex != null) { + addPackageFromIndex(scannedUnit, pkg); } - catch (IOException ex) { - throw new PersistenceException("Failed to scan classpath for unlisted entity classes", ex); + else { + scanPackage(scannedUnit, pkg); } } } @@ -565,6 +549,48 @@ public class DefaultPersistenceUnitManager return scannedUnit; } + private void addPackageFromIndex(SpringPersistenceUnitInfo scannedUnit, String pkg) { + Set candidates = new HashSet<>(); + for (AnnotationTypeFilter filter : entityTypeFilters) { + candidates.addAll(this.componentsIndex + .getCandidateTypes(pkg, filter.getAnnotationType().getName())); + } + candidates.forEach(scannedUnit::addManagedClassName); + Set managedPackages = this.componentsIndex.getCandidateTypes(pkg, "package-info"); + managedPackages.forEach(scannedUnit::addManagedPackage); + } + + private void scanPackage(SpringPersistenceUnitInfo scannedUnit, String pkg) { + try { + String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(pkg) + CLASS_RESOURCE_PATTERN; + Resource[] resources = this.resourcePatternResolver.getResources(pattern); + MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); + for (Resource resource : resources) { + if (resource.isReadable()) { + MetadataReader reader = readerFactory.getMetadataReader(resource); + String className = reader.getClassMetadata().getClassName(); + if (matchesFilter(reader, readerFactory)) { + scannedUnit.addManagedClassName(className); + if (scannedUnit.getPersistenceUnitRootUrl() == null) { + URL url = resource.getURL(); + if (ResourceUtils.isJarURL(url)) { + scannedUnit.setPersistenceUnitRootUrl(ResourceUtils.extractJarFileURL(url)); + } + } + } + else if (className.endsWith(PACKAGE_INFO_SUFFIX)) { + scannedUnit.addManagedPackage( + className.substring(0, className.length() - PACKAGE_INFO_SUFFIX.length())); + } + } + } + } + catch (IOException ex) { + throw new PersistenceException("Failed to scan classpath for unlisted entity classes", ex); + } + } + /** * Check whether any of the configured entity type filters matches * the current class descriptor contained in the metadata reader. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java new file mode 100644 index 00000000000..3b5f0552919 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.jpa.persistenceunit; + +import org.junit.Test; + +import org.springframework.context.index.CandidateComponentsTestClassLoader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.orm.jpa.domain.Person; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link DefaultPersistenceUnitManager}. + * + * @author Stephane Nicoll + */ +public class DefaultPersistenceUnitManagerTests { + + private final DefaultPersistenceUnitManager manager = new DefaultPersistenceUnitManager(); + + @Test + public void defaultDomainWithScan() { + this.manager.setPackagesToScan("org.springframework.orm.jpa.domain"); + this.manager.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testDefaultDomain(); + } + + @Test + public void defaultDomainWithIndex() { + this.manager.setPackagesToScan("org.springframework.orm.jpa.domain"); + this.manager.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", Person.class)))); + testDefaultDomain(); + } + + private void testDefaultDomain() { + SpringPersistenceUnitInfo puInfo = buildDefaultPersistenceUnitInfo(); + assertThat(puInfo.getManagedClassNames(), containsInAnyOrder( + "org.springframework.orm.jpa.domain.Person", + "org.springframework.orm.jpa.domain.DriversLicense")); + } + + private SpringPersistenceUnitInfo buildDefaultPersistenceUnitInfo() { + this.manager.preparePersistenceUnitInfos(); + return (SpringPersistenceUnitInfo) this.manager.obtainDefaultPersistenceUnitInfo(); + } + +} diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/domain/spring.components b/spring-orm/src/test/resources/org/springframework/orm/jpa/domain/spring.components new file mode 100644 index 00000000000..4b6de536dfa --- /dev/null +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/domain/spring.components @@ -0,0 +1,2 @@ +org.springframework.orm.jpa.domain.Person=javax.persistence.Entity +org.springframework.orm.jpa.domain.DriversLicense=javax.persistence.Entity \ No newline at end of file