View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau;
18  
19  import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
20  import static org.apache.juneau.commons.utils.AssertionUtils.*;
21  import static org.apache.juneau.commons.utils.CollectionUtils.*;
22  import static org.apache.juneau.commons.utils.ThrowableUtils.*;
23  import static org.apache.juneau.commons.utils.Utils.*;
24  
25  import java.lang.reflect.*;
26  import java.util.*;
27  import java.util.concurrent.*;
28  
29  import org.apache.juneau.annotation.*;
30  import org.apache.juneau.commons.collections.*;
31  import org.apache.juneau.commons.reflect.*;
32  import org.apache.juneau.commons.utils.*;
33  import org.apache.juneau.cp.*;
34  
35  /**
36   * A lookup table for resolving bean types by name.
37   *
38   * <p>
39   * In a nutshell, provides a simple mapping of bean class objects to identifying names.
40   *
41   * <p>
42   * Class names are defined through the {@link Bean#typeName() @Bean(typeName)} annotation.
43   *
44   * <p>
45   * The dictionary is used by the framework in the following ways:
46   * <ul>
47   * 	<li>If a class type cannot be inferred through reflection during parsing, then a helper <js>"_type"</js> is added
48   * 		to the serialized output using the name defined for that class in this dictionary.  This helps determine the
49   * 		real class at parse time.
50   * 	<li>The dictionary name is used as element names when serialized to XML.
51   * </ul>
52   *
53   */
54  public class BeanRegistry {
55  
56  	private final Map<String,ClassMeta<?>> map;
57  	private final Map<Class<?>,String> reverseMap;
58  	private final BeanContext bc;
59  	private final AnnotationProvider ap;
60  	private final boolean isEmpty;
61  
62  	BeanRegistry(BeanContext bc, BeanRegistry parent, List<ClassInfo> classes) {
63  		assertArgNotNull("bc", bc);
64  		assertArgNotNull("classes", classes);
65  		this.bc = bc;
66  		this.ap = bc.getAnnotationProvider();
67  		this.map = new ConcurrentHashMap<>();
68  		this.reverseMap = new ConcurrentHashMap<>();
69  		bc.getBeanDictionary().forEach(this::addClass);
70  		if (nn(parent))
71  			parent.map.forEach(this::addToMap);
72  		classes.forEach(this::addClass);
73  		isEmpty = map.isEmpty();
74  	}
75  
76  	/**
77  	 * Gets the class metadata for the specified bean type name.
78  	 *
79  	 * @param typeName
80  	 * 	The bean type name as defined by {@link Bean#typeName() @Bean(typeName)}.
81  	 * 	Can include multi-dimensional array type names (e.g. <js>"X^^"</js>).
82  	 * @return The class metadata for the bean.
83  	 */
84  	public ClassMeta<?> getClassMeta(String typeName) {
85  		if (isEmpty || typeName == null)
86  			return null;
87  		var cm = map.get(typeName);
88  		if (nn(cm))
89  			return cm;
90  		if (typeName.charAt(typeName.length() - 1) == '^') {
91  			cm = getClassMeta(typeName.substring(0, typeName.length() - 1));
92  			if (nn(cm)) {
93  				cm = bc.getClassMeta(Array.newInstance(cm.inner(), 1).getClass());
94  				map.put(typeName, cm);
95  			}
96  			return cm;
97  		}
98  		return null;
99  	}
100 
101 	/**
102 	 * Given the specified class, return the dictionary name for it.
103 	 *
104 	 * @param c The class to lookup in this registry.
105 	 * @return The dictionary name for the specified class in this registry, or <jk>null</jk> if not found.
106 	 */
107 	public String getTypeName(ClassMeta<?> c) {
108 		return isEmpty ? null : reverseMap.get(c.inner());
109 	}
110 
111 	/**
112 	 * Returns <jk>true</jk> if this dictionary has an entry for the specified type name.
113 	 *
114 	 * @param typeName The bean type name.
115 	 * @return <jk>true</jk> if this dictionary has an entry for the specified type name.
116 	 */
117 	public boolean hasName(String typeName) {
118 		return nn(getClassMeta(typeName));
119 	}
120 
121 	protected FluentMap<String,Object> properties() {
122 		var m = filteredBeanPropertyMap();
123 		map.forEach((k, v) -> m.a(k, v.toString(true)));
124 		return m;
125 	}
126 
127 	@Override
128 	public String toString() {
129 		return r(properties());
130 	}
131 
132 	private void addClass(ClassInfo ci) {
133 		try {
134 			if (nn(ci) && nn(ci.inner())) {
135 				if (ci.isChildOf(Collection.class)) {
136 					Collection<?> cc = BeanCreator.of(Collection.class).type(ci).run();
137 					cc.forEach(x -> {
138 						if (x instanceof Class<?> x2)
139 							addClass(info(x2));
140 						else
141 							throw bex("Collection class ''{0}'' passed to BeanRegistry does not contain Class objects.", ci.getName());
142 					});
143 				} else if (ci.isChildOf(Map.class)) {
144 					Map<?,?> m = BeanCreator.of(Map.class).type(ci).run();
145 					m.forEach((k, v) -> {
146 						var typeName = s(k);
147 						var val = (ClassMeta<?>)null;
148 						if (v instanceof Type v2)
149 							val = bc.getClassMeta(v2);
150 						else if (isArray(v))
151 							val = getTypedClassMeta(v);
152 						else
153 							throw bex("Class ''{0}'' was passed to BeanRegistry but value of type ''{1}'' found in map is not a Type object.", ci.getName(), cn(v));
154 						addToMap(typeName, val);
155 					});
156 				} else {
157 					// @formatter:off
158 					var typeName = ap.find(Bean.class, ci)
159 						.stream()
160 						.map(x -> x.inner().typeName())
161 						.filter(Utils::ne)
162 						.findFirst()
163 						.orElseThrow(() -> bex("Class ''{0}'' was passed to BeanRegistry but it doesn't have a @Bean(typeName) annotation defined.", ci.getName()));
164 					// @formatter:on
165 					addToMap(typeName, bc.getClassMeta(ci.inner()));
166 				}
167 			}
168 		} catch (BeanRuntimeException e) {
169 			throw e;
170 		} catch (Exception e) {
171 			throw bex(e);
172 		}
173 	}
174 
175 	private void addToMap(String typeName, ClassMeta<?> cm) {
176 		map.put(typeName, cm);
177 		reverseMap.put(cm.inner(), typeName);
178 	}
179 
180 	private ClassMeta<?> getTypedClassMeta(Object array) {
181 		var len = Array.getLength(array);
182 		if (len == 0)
183 			throw bex("Map entry had an empty array value.");
184 		var type = (Type)Array.get(array, 0);
185 		var args = new Type[len - 1];
186 		for (var i = 1; i < len; i++)
187 			args[i - 1] = (Type)Array.get(array, i);
188 		return bc.getClassMeta(type, args);
189 	}
190 }