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.core;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.concurrent.ConcurrentHashMap;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027
028import org.springframework.util.Assert;
029import org.springframework.util.ObjectUtils;
030import org.springframework.util.StringUtils;
031import org.springframework.util.StringValueResolver;
032
033/**
034 * Simple implementation of the {@link AliasRegistry} interface.
035 * <p>Serves as base class for
036 * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry}
037 * implementations.
038 *
039 * @author Juergen Hoeller
040 * @author Qimiao Chen
041 * @since 2.5.2
042 */
043public class SimpleAliasRegistry implements AliasRegistry {
044
045        /** Logger available to subclasses. */
046        protected final Log logger = LogFactory.getLog(getClass());
047
048        /** Map from alias to canonical name. */
049        private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);
050
051
052        @Override
053        public void registerAlias(String name, String alias) {
054                Assert.hasText(name, "'name' must not be empty");
055                Assert.hasText(alias, "'alias' must not be empty");
056                synchronized (this.aliasMap) {
057                        if (alias.equals(name)) {
058                                this.aliasMap.remove(alias);
059                                if (logger.isDebugEnabled()) {
060                                        logger.debug("Alias definition '" + alias + "' ignored since it points to same name");
061                                }
062                        }
063                        else {
064                                String registeredName = this.aliasMap.get(alias);
065                                if (registeredName != null) {
066                                        if (registeredName.equals(name)) {
067                                                // An existing alias - no need to re-register
068                                                return;
069                                        }
070                                        if (!allowAliasOverriding()) {
071                                                throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +
072                                                                name + "': It is already registered for name '" + registeredName + "'.");
073                                        }
074                                        if (logger.isDebugEnabled()) {
075                                                logger.debug("Overriding alias '" + alias + "' definition for registered name '" +
076                                                                registeredName + "' with new target name '" + name + "'");
077                                        }
078                                }
079                                checkForAliasCircle(name, alias);
080                                this.aliasMap.put(alias, name);
081                                if (logger.isTraceEnabled()) {
082                                        logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'");
083                                }
084                        }
085                }
086        }
087
088        /**
089         * Determine whether alias overriding is allowed.
090         * <p>Default is {@code true}.
091         */
092        protected boolean allowAliasOverriding() {
093                return true;
094        }
095
096        /**
097         * Determine whether the given name has the given alias registered.
098         * @param name the name to check
099         * @param alias the alias to look for
100         * @since 4.2.1
101         */
102        public boolean hasAlias(String name, String alias) {
103                String registeredName = this.aliasMap.get(alias);
104                return ObjectUtils.nullSafeEquals(registeredName, name) || (registeredName != null
105                                && hasAlias(name, registeredName));
106        }
107
108        @Override
109        public void removeAlias(String alias) {
110                synchronized (this.aliasMap) {
111                        String name = this.aliasMap.remove(alias);
112                        if (name == null) {
113                                throw new IllegalStateException("No alias '" + alias + "' registered");
114                        }
115                }
116        }
117
118        @Override
119        public boolean isAlias(String name) {
120                return this.aliasMap.containsKey(name);
121        }
122
123        @Override
124        public String[] getAliases(String name) {
125                List<String> result = new ArrayList<>();
126                synchronized (this.aliasMap) {
127                        retrieveAliases(name, result);
128                }
129                return StringUtils.toStringArray(result);
130        }
131
132        /**
133         * Transitively retrieve all aliases for the given name.
134         * @param name the target name to find aliases for
135         * @param result the resulting aliases list
136         */
137        private void retrieveAliases(String name, List<String> result) {
138                this.aliasMap.forEach((alias, registeredName) -> {
139                        if (registeredName.equals(name)) {
140                                result.add(alias);
141                                retrieveAliases(alias, result);
142                        }
143                });
144        }
145
146        /**
147         * Resolve all alias target names and aliases registered in this
148         * registry, applying the given {@link StringValueResolver} to them.
149         * <p>The value resolver may for example resolve placeholders
150         * in target bean names and even in alias names.
151         * @param valueResolver the StringValueResolver to apply
152         */
153        public void resolveAliases(StringValueResolver valueResolver) {
154                Assert.notNull(valueResolver, "StringValueResolver must not be null");
155                synchronized (this.aliasMap) {
156                        Map<String, String> aliasCopy = new HashMap<>(this.aliasMap);
157                        aliasCopy.forEach((alias, registeredName) -> {
158                                String resolvedAlias = valueResolver.resolveStringValue(alias);
159                                String resolvedName = valueResolver.resolveStringValue(registeredName);
160                                if (resolvedAlias == null || resolvedName == null || resolvedAlias.equals(resolvedName)) {
161                                        this.aliasMap.remove(alias);
162                                }
163                                else if (!resolvedAlias.equals(alias)) {
164                                        String existingName = this.aliasMap.get(resolvedAlias);
165                                        if (existingName != null) {
166                                                if (existingName.equals(resolvedName)) {
167                                                        // Pointing to existing alias - just remove placeholder
168                                                        this.aliasMap.remove(alias);
169                                                        return;
170                                                }
171                                                throw new IllegalStateException(
172                                                                "Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias +
173                                                                "') for name '" + resolvedName + "': It is already registered for name '" +
174                                                                registeredName + "'.");
175                                        }
176                                        checkForAliasCircle(resolvedName, resolvedAlias);
177                                        this.aliasMap.remove(alias);
178                                        this.aliasMap.put(resolvedAlias, resolvedName);
179                                }
180                                else if (!registeredName.equals(resolvedName)) {
181                                        this.aliasMap.put(alias, resolvedName);
182                                }
183                        });
184                }
185        }
186
187        /**
188         * Check whether the given name points back to the given alias as an alias
189         * in the other direction already, catching a circular reference upfront
190         * and throwing a corresponding IllegalStateException.
191         * @param name the candidate name
192         * @param alias the candidate alias
193         * @see #registerAlias
194         * @see #hasAlias
195         */
196        protected void checkForAliasCircle(String name, String alias) {
197                if (hasAlias(alias, name)) {
198                        throw new IllegalStateException("Cannot register alias '" + alias +
199                                        "' for name '" + name + "': Circular reference - '" +
200                                        name + "' is a direct or indirect alias for '" + alias + "' already");
201                }
202        }
203
204        /**
205         * Determine the raw name, resolving aliases to canonical names.
206         * @param name the user-specified name
207         * @return the transformed name
208         */
209        public String canonicalName(String name) {
210                String canonicalName = name;
211                // Handle aliasing...
212                String resolvedName;
213                do {
214                        resolvedName = this.aliasMap.get(canonicalName);
215                        if (resolvedName != null) {
216                                canonicalName = resolvedName;
217                        }
218                }
219                while (resolvedName != null);
220                return canonicalName;
221        }
222
223}