001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.context.index;
018
019import java.util.Collections;
020import java.util.List;
021import java.util.Properties;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import org.springframework.util.AntPathMatcher;
026import org.springframework.util.ClassUtils;
027import org.springframework.util.LinkedMultiValueMap;
028import org.springframework.util.MultiValueMap;
029
030/**
031 * Provide access to the candidates that are defined in {@code META-INF/spring.components}.
032 *
033 * <p>An arbitrary number of stereotypes can be registered (and queried) on the index: a
034 * typical example is the fully qualified name of an annotation that flags the class for
035 * a certain use case. The following call returns all the {@code @Component}
036 * <b>candidate</b> types for the {@code com.example} package (and its sub-packages):
037 * <pre class="code">
038 * Set&lt;String&gt; candidates = index.getCandidateTypes(
039 *         "com.example", "org.springframework.stereotype.Component");
040 * </pre>
041 *
042 * <p>The {@code type} is usually the fully qualified name of a class, though this is
043 * not a rule. Similarly, the {@code stereotype} is usually the fully qualified name of
044 * a target type but it can be any marker really.
045 *
046 * @author Stephane Nicoll
047 * @since 5.0
048 */
049public class CandidateComponentsIndex {
050
051        private static final AntPathMatcher pathMatcher = new AntPathMatcher(".");
052
053        private final MultiValueMap<String, Entry> index;
054
055
056        CandidateComponentsIndex(List<Properties> content) {
057                this.index = parseIndex(content);
058        }
059
060        private static MultiValueMap<String, Entry> parseIndex(List<Properties> content) {
061                MultiValueMap<String, Entry> index = new LinkedMultiValueMap<>();
062                for (Properties entry : content) {
063                        entry.forEach((type, values) -> {
064                                String[] stereotypes = ((String) values).split(",");
065                                for (String stereotype : stereotypes) {
066                                        index.add(stereotype, new Entry((String) type));
067                                }
068                        });
069                }
070                return index;
071        }
072
073
074        /**
075         * Return the candidate types that are associated with the specified stereotype.
076         * @param basePackage the package to check for candidates
077         * @param stereotype the stereotype to use
078         * @return the candidate types associated with the specified {@code stereotype}
079         * or an empty set if none has been found for the specified {@code basePackage}
080         */
081        public Set<String> getCandidateTypes(String basePackage, String stereotype) {
082                List<Entry> candidates = this.index.get(stereotype);
083                if (candidates != null) {
084                        return candidates.parallelStream()
085                                        .filter(t -> t.match(basePackage))
086                                        .map(t -> t.type)
087                                        .collect(Collectors.toSet());
088                }
089                return Collections.emptySet();
090        }
091
092
093        private static class Entry {
094
095                private final String type;
096
097                private final String packageName;
098
099                Entry(String type) {
100                        this.type = type;
101                        this.packageName = ClassUtils.getPackageName(type);
102                }
103
104                public boolean match(String basePackage) {
105                        if (pathMatcher.isPattern(basePackage)) {
106                                return pathMatcher.match(basePackage, this.packageName);
107                        }
108                        else {
109                                return this.type.startsWith(basePackage);
110                        }
111                }
112        }
113
114}