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.commons.reflect.ReflectionUtils.*;
020import static org.apache.juneau.commons.utils.AssertionUtils.*;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
024
025import java.lang.reflect.*;
026import java.util.*;
027import java.util.concurrent.*;
028
029import org.apache.juneau.annotation.*;
030import org.apache.juneau.commons.collections.*;
031import org.apache.juneau.commons.reflect.*;
032import org.apache.juneau.commons.utils.*;
033import org.apache.juneau.cp.*;
034
035/**
036 * A lookup table for resolving bean types by name.
037 *
038 * <p>
039 * In a nutshell, provides a simple mapping of bean class objects to identifying names.
040 *
041 * <p>
042 * Class names are defined through the {@link Bean#typeName() @Bean(typeName)} annotation.
043 *
044 * <p>
045 * The dictionary is used by the framework in the following ways:
046 * <ul>
047 *    <li>If a class type cannot be inferred through reflection during parsing, then a helper <js>"_type"</js> is added
048 *       to the serialized output using the name defined for that class in this dictionary.  This helps determine the
049 *       real class at parse time.
050 *    <li>The dictionary name is used as element names when serialized to XML.
051 * </ul>
052 *
053 */
054public class BeanRegistry {
055
056   private final Map<String,ClassMeta<?>> map;
057   private final Map<Class<?>,String> reverseMap;
058   private final BeanContext bc;
059   private final AnnotationProvider ap;
060   private final boolean isEmpty;
061
062   BeanRegistry(BeanContext bc, BeanRegistry parent, List<ClassInfo> classes) {
063      assertArgNotNull("bc", bc);
064      assertArgNotNull("classes", classes);
065      this.bc = bc;
066      this.ap = bc.getAnnotationProvider();
067      this.map = new ConcurrentHashMap<>();
068      this.reverseMap = new ConcurrentHashMap<>();
069      bc.getBeanDictionary().forEach(this::addClass);
070      if (nn(parent))
071         parent.map.forEach(this::addToMap);
072      classes.forEach(this::addClass);
073      isEmpty = map.isEmpty();
074   }
075
076   /**
077    * Gets the class metadata for the specified bean type name.
078    *
079    * @param typeName
080    *    The bean type name as defined by {@link Bean#typeName() @Bean(typeName)}.
081    *    Can include multi-dimensional array type names (e.g. <js>"X^^"</js>).
082    * @return The class metadata for the bean.
083    */
084   public ClassMeta<?> getClassMeta(String typeName) {
085      if (isEmpty || typeName == null)
086         return null;
087      var cm = map.get(typeName);
088      if (nn(cm))
089         return cm;
090      if (typeName.charAt(typeName.length() - 1) == '^') {
091         cm = getClassMeta(typeName.substring(0, typeName.length() - 1));
092         if (nn(cm)) {
093            cm = bc.getClassMeta(Array.newInstance(cm.inner(), 1).getClass());
094            map.put(typeName, cm);
095         }
096         return cm;
097      }
098      return null;
099   }
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}