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.*;
023import org.apache.juneau.reflect.*;
024
025/**
026 * A lookup table for resolving bean types by name.
027 *
028 * <p>
029 * In a nutshell, provides a simple mapping of bean class objects to identifying names.
030 *
031 * <p>
032 * Class names are defined through the {@link Bean#typeName() @Bean(typeName)} annotation.
033 *
034 * <p>
035 * The dictionary is used by the framework in the following ways:
036 * <ul>
037 *    <li>If a class type cannot be inferred through reflection during parsing, then a helper <js>"_type"</js> is added
038 *       to the serialized output using the name defined for that class in this dictionary.  This helps determine the
039 *       real class at parse time.
040 *    <li>The dictionary name is used as element names when serialized to XML.
041 * </ul>
042 */
043public class BeanRegistry {
044
045   private final Map<String,ClassMeta<?>> map;
046   private final Map<Class<?>,String> reverseMap;
047   private final BeanContext beanContext;
048   private final boolean isEmpty;
049
050   BeanRegistry(BeanContext beanContext, BeanRegistry parent, Class<?>...classes) {
051      this.beanContext = beanContext;
052      this.map = new ConcurrentHashMap<>();
053      this.reverseMap = new ConcurrentHashMap<>();
054      for (Class<?> c : beanContext.getBeanDictionaryClasses())
055         addClass(c);
056      if (parent != null)
057         for (Map.Entry<String,ClassMeta<?>> e : parent.map.entrySet())
058            addToMap(e.getKey(), e.getValue());
059      for (Class<?> c : classes)
060         addClass(c);
061      isEmpty = map.isEmpty();
062   }
063
064   private void addClass(Class<?> c) {
065      try {
066         if (c != null) {
067            ClassInfo ci = ClassInfo.of(c);
068            if (ci.isChildOf(Collection.class)) {
069               @SuppressWarnings("rawtypes")
070               Collection cc = castOrCreate(Collection.class, c);
071               for (Object o : cc) {
072                  if (o instanceof Class)
073                     addClass((Class<?>)o);
074                  else
075                     throw new BeanRuntimeException("Collection class ''{0}'' passed to BeanRegistry does not contain Class objects.", c.getName());
076               }
077            } else if (ci.isChildOf(Map.class)) {
078               Map<?,?> m = castOrCreate(Map.class, c);
079               for (Map.Entry<?,?> e : m.entrySet()) {
080                  String typeName = stringify(e.getKey());
081                  Object v = e.getValue();
082                  ClassMeta<?> val = null;
083                  if (v instanceof Type)
084                     val = beanContext.getClassMeta((Type)v);
085                  else if (v.getClass().isArray())
086                     val = getTypedClassMeta(v);
087                  else
088                     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());
089                  addToMap(typeName, val);
090               }
091            } else {
092               Bean b = c.getAnnotation(Bean.class);
093               if (b == null || b.typeName().isEmpty())
094                  throw new BeanRuntimeException("Class ''{0}'' was passed to BeanRegistry but it doesn't have a @Bean(typeName) annotation defined.", c.getName());
095               addToMap(b.typeName(), beanContext.getClassMeta(c));
096            }
097         }
098      } catch (BeanRuntimeException e) {
099         throw e;
100      } catch (Exception e) {
101         throw new BeanRuntimeException(e);
102      }
103   }
104
105   private ClassMeta<?> getTypedClassMeta(Object array) {
106      int len = Array.getLength(array);
107      if (len == 0)
108         throw new BeanRuntimeException("Map entry had an empty array value.");
109      Type type = (Type)Array.get(array, 0);
110      Type[] args = new Type[len-1];
111      for (int i = 1; i < len; i++)
112         args[i-1] = (Type)Array.get(array, i);
113      return beanContext.getClassMeta(type, args);
114   }
115
116   private void addToMap(String typeName, ClassMeta<?> cm) {
117      map.put(typeName, cm);
118      reverseMap.put(cm.innerClass, typeName);
119   }
120
121   /**
122    * Gets the class metadata for the specified bean type name.
123    *
124    * @param typeName
125    *    The bean type name as defined by {@link Bean#typeName() @Bean(typeName)}.
126    *    Can include multi-dimensional array type names (e.g. <js>"X^^"</js>).
127    * @return The class metadata for the bean.
128    */
129   public ClassMeta<?> getClassMeta(String typeName) {
130      if (isEmpty)
131         return null;
132      if (typeName == null)
133         return null;
134      ClassMeta<?> cm = map.get(typeName);
135      if (cm != null)
136         return cm;
137      if (typeName.charAt(typeName.length()-1) == '^') {
138         cm = getClassMeta(typeName.substring(0, typeName.length()-1));
139         if (cm != null) {
140            cm = beanContext.getClassMeta(Array.newInstance(cm.innerClass, 1).getClass());
141            map.put(typeName, cm);
142         }
143         return cm;
144      }
145      return null;
146   }
147
148   /**
149    * Given the specified class, return the dictionary name for it.
150    *
151    * @param c The class to lookup in this registry.
152    * @return The dictionary name for the specified class in this registry, or <jk>null</jk> if not found.
153    */
154   public String getTypeName(ClassMeta<?> c) {
155      if (isEmpty)
156         return null;
157      return reverseMap.get(c.innerClass);
158   }
159
160   /**
161    * Returns <jk>true</jk> if this dictionary has an entry for the specified type name.
162    *
163    * @param typeName The bean type name.
164    * @return <jk>true</jk> if this dictionary has an entry for the specified type name.
165    */
166   public boolean hasName(String typeName) {
167      return getClassMeta(typeName) != null;
168   }
169
170   @Override
171   public String toString() {
172      StringBuilder sb = new StringBuilder();
173      sb.append('{');
174      for (Map.Entry<String,ClassMeta<?>> e : map.entrySet())
175         sb.append(e.getKey()).append(":").append(e.getValue().toString(true)).append(", ");
176      sb.append('}');
177      return sb.toString();
178   }
179}