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