001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau; 014 015import static org.apache.juneau.internal.ClassUtils.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.lang.reflect.*; 019import java.util.*; 020import java.util.concurrent.*; 021 022import org.apache.juneau.annotation.*; 023 024/** 025 * A lookup table for resolving bean types by name. 026 * 027 * <p> 028 * In a nutshell, provides a simple mapping of bean class objects to identifying names. 029 * 030 * <p> 031 * Class names are defined through the {@link Bean#typeName() @Bean.typeName()} annotation. 032 * 033 * <p> 034 * The dictionary is used by the framework in the following ways: 035 * <ul> 036 * <li>If a class type cannot be inferred through reflection during parsing, then a helper <js>"_type"</js> is added 037 * to the serialized output using the name defined for that class in this dictionary. This helps determine the 038 * real class at parse time. 039 * <li>The dictionary name is used as element names when serialized to XML. 040 * </ul> 041 */ 042public class BeanRegistry { 043 044 private final Map<String,ClassMeta<?>> map; 045 private final Map<Class<?>,String> reverseMap; 046 private final BeanContext beanContext; 047 private final boolean isEmpty; 048 049 BeanRegistry(BeanContext beanContext, BeanRegistry parent, Class<?>...classes) { 050 this.beanContext = beanContext; 051 this.map = new ConcurrentHashMap<>(); 052 this.reverseMap = new ConcurrentHashMap<>(); 053 for (Class<?> c : beanContext.beanDictionaryClasses) 054 addClass(c); 055 if (parent != null) 056 for (Map.Entry<String,ClassMeta<?>> e : parent.map.entrySet()) 057 addToMap(e.getKey(), e.getValue()); 058 for (Class<?> c : classes) 059 addClass(c); 060 isEmpty = map.isEmpty(); 061 } 062 063 private void addClass(Class<?> c) { 064 try { 065 if (c != null) { 066 if (isParentClass(Collection.class, c)) { 067 @SuppressWarnings("rawtypes") 068 Collection cc = beanContext.newInstance(Collection.class, c); 069 for (Object o : cc) { 070 if (o instanceof Class) 071 addClass((Class<?>)o); 072 else 073 throw new BeanRuntimeException("Collection class ''{0}'' passed to BeanRegistry does not contain Class objects.", c.getName()); 074 } 075 } else if (isParentClass(Map.class, c)) { 076 Map<?,?> m = beanContext.newInstance(Map.class, c); 077 for (Map.Entry<?,?> e : m.entrySet()) { 078 String typeName = asString(e.getKey()); 079 Object v = e.getValue(); 080 ClassMeta<?> val = null; 081 if (v instanceof Type) 082 val = beanContext.getClassMeta((Type)v); 083 else if (v.getClass().isArray()) 084 val = getTypedClassMeta(v); 085 else 086 throw new BeanRuntimeException("Class ''{0}'' was passed to BeanRegistry but value of type ''{1}'' found in map is not a Type object.", c.getName(), v.getClass().getName()); 087 addToMap(typeName, val); 088 } 089 } else { 090 Bean b = c.getAnnotation(Bean.class); 091 if (b == null || b.typeName().isEmpty()) 092 throw new BeanRuntimeException("Class ''{0}'' was passed to BeanRegistry but it doesn't have a @Bean.typeName() annotation defined.", c.getName()); 093 addToMap(b.typeName(), beanContext.getClassMeta(c)); 094 } 095 } 096 } catch (BeanRuntimeException e) { 097 throw e; 098 } catch (Exception e) { 099 throw new BeanRuntimeException(e); 100 } 101 } 102 103 private ClassMeta<?> getTypedClassMeta(Object array) { 104 int len = Array.getLength(array); 105 if (len == 0) 106 throw new BeanRuntimeException("Map entry had an empty array value."); 107 Type type = (Type)Array.get(array, 0); 108 Type[] args = new Type[len-1]; 109 for (int i = 1; i < len; i++) 110 args[i-1] = (Type)Array.get(array, i); 111 return beanContext.getClassMeta(type, args); 112 } 113 114 private void addToMap(String typeName, ClassMeta<?> cm) { 115 map.put(typeName, cm); 116 reverseMap.put(cm.innerClass, typeName); 117 } 118 119 /** 120 * Gets the class metadata for the specified bean type name. 121 * 122 * @param typeName 123 * The bean type name as defined by {@link Bean#typeName() @Bean.typeName()}. 124 * Can include multi-dimensional array type names (e.g. <js>"X^^"</js>). 125 * @return The class metadata for the bean. 126 */ 127 public ClassMeta<?> getClassMeta(String typeName) { 128 if (isEmpty) 129 return null; 130 if (typeName == null) 131 return null; 132 ClassMeta<?> cm = map.get(typeName); 133 if (cm != null) 134 return cm; 135 if (typeName.charAt(typeName.length()-1) == '^') { 136 cm = getClassMeta(typeName.substring(0, typeName.length()-1)); 137 if (cm != null) { 138 cm = beanContext.getClassMeta(Array.newInstance(cm.innerClass, 1).getClass()); 139 map.put(typeName, cm); 140 } 141 return cm; 142 } 143 return null; 144 } 145 146 /** 147 * Given the specified class, return the dictionary name for it. 148 * 149 * @param c The class to lookup in this registry. 150 * @return The dictionary name for the specified class in this registry, or <jk>null</jk> if not found. 151 */ 152 public String getTypeName(ClassMeta<?> c) { 153 if (isEmpty) 154 return null; 155 return reverseMap.get(c.innerClass); 156 } 157 158 /** 159 * Returns <jk>true</jk> if this dictionary has an entry for the specified type name. 160 * 161 * @param typeName The bean type name. 162 * @return <jk>true</jk> if this dictionary has an entry for the specified type name. 163 */ 164 public boolean hasName(String typeName) { 165 return getClassMeta(typeName) != null; 166 } 167 168 @Override 169 public String toString() { 170 StringBuilder sb = new StringBuilder(); 171 sb.append('{'); 172 for (Map.Entry<String,ClassMeta<?>> e : map.entrySet()) 173 sb.append(e.getKey()).append(":").append(e.getValue().toString(true)).append(", "); 174 sb.append('}'); 175 return sb.toString(); 176 } 177}