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.getBeanDictionaryClasses())
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}