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.marshall;
018
019import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
020import static org.apache.juneau.commons.utils.ClassUtils.*;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.Shorts.*;
023
024import java.lang.reflect.*;
025import java.util.*;
026import java.util.concurrent.*;
027
028import org.apache.juneau.commons.bean.*;
029import org.apache.juneau.commons.collections.*;
030import org.apache.juneau.commons.inject.*;
031import org.apache.juneau.commons.reflect.*;
032import org.apache.juneau.commons.utils.*;
033
034/**
035 * A lookup table for resolving bean types by name.
036 *
037 * <p>
038 * In a nutshell, provides a simple mapping of bean class objects to identifying names.
039 *
040 * <p>
041 * Class names are defined through the {@link Marshalled#typeName() @Marshalled(typeName)} annotation.
042 *
043 * <p>
044 * The dictionary is used by the framework in the following ways:
045 * <ul>
046 *    <li>If a class type cannot be inferred through reflection during parsing, then a helper <js>"_type"</js> is added
047 *       to the serialized output using the name defined for that class in this dictionary.  This helps determine the
048 *       real class at parse time.
049 *    <li>The dictionary name is used as element names when serialized to XML.
050 * </ul>
051 *
052 */
053@SuppressWarnings({
054   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
055   "java:S1452" // Wildcard required - Class<?> for bean dictionary types
056})
057public class BeanRegistry implements BeanRegistryLookup {
058
059   private final Map<String,ClassMeta<?>> map;
060   private final Map<Class<?>,String> reverseMap;
061   private final MarshallingContext bc;
062   private final AnnotationProvider ap;
063   private final boolean isEmpty;
064
065   BeanRegistry(MarshallingContext bc, BeanRegistry parent, List<ClassInfo> classes) {
066      reqnn("bc", bc);
067      reqnn("classes", classes);
068      this.bc = bc;
069      this.ap = bc.getAnnotationProvider();
070      this.map = new ConcurrentHashMap<>();
071      this.reverseMap = new ConcurrentHashMap<>();
072      bc.getBeanDictionary().forEach(this::addClass);
073      if (nn(parent))
074         parent.map.forEach(this::addToMap);
075      classes.forEach(this::addClass);
076      isEmpty = map.isEmpty();
077   }
078
079   /**
080    * Gets the class metadata for the specified bean type name.
081    *
082    * @param typeName
083    *    The bean type name as defined by {@link Marshalled#typeName() @Marshalled(typeName)}.
084    *    Can include multi-dimensional array type names (e.g. <js>"X^^"</js>).
085    *    <br>Can be <jk>null</jk> (returns <jk>null</jk>).
086    * @return The class metadata for the bean, or <jk>null</jk> if the registry is empty, the type name is <jk>null</jk>, or no matching entry was found.
087    */
088   public ClassMeta<?> getClassMeta(String typeName) {
089      if (isEmpty || typeName == null)
090         return null;
091      var cm = map.get(typeName);
092      if (nn(cm))
093         return cm;
094      if (typeName.charAt(typeName.length() - 1) == '^') {
095         cm = getClassMeta(typeName.substring(0, typeName.length() - 1));
096         if (nn(cm)) {
097            cm = bc.getClassMeta(Array.newInstance(cm.inner(), 1).getClass());
098            map.put(typeName, cm);
099         }
100         return cm;
101      }
102      return null;
103   }
104
105   /**
106    * Given the specified class, return the dictionary name for it.
107    *
108    * @param c The class to lookup in this registry. Can be <jk>null</jk> (returns <jk>null</jk>).
109    * @return The dictionary name for the specified class in this registry, or <jk>null</jk> if not found.
110    */
111   public String getTypeName(ClassMeta<?> c) {
112      return isEmpty || c == null ? null : reverseMap.get(c.inner());
113   }
114
115   /**
116    * Given the specified raw class, return the dictionary name for it.
117    *
118    * <p>
119    * Variant of {@link #getTypeName(ClassMeta)} that takes the raw {@link Class} directly so callers in the
120    * bean-modeling layer (which is being decoupled from {@link ClassMeta}) can perform a polymorphic-dispatch
121    * lookup without holding a {@link ClassMeta} reference.
122    *
123    * @param c The class to lookup in this registry. Can be <jk>null</jk> (returns <jk>null</jk>).
124    * @return The dictionary name for the specified class in this registry, or <jk>null</jk> if not found.
125    */
126   @Override
127   public String getTypeName(Class<?> c) {
128      return isEmpty || c == null ? null : reverseMap.get(c);
129   }
130
131   /**
132    * Returns <jk>true</jk> if this dictionary has an entry for the specified type name.
133    *
134    * @param typeName The bean type name. Can be <jk>null</jk> (returns <jk>false</jk>).
135    * @return <jk>true</jk> if this dictionary has an entry for the specified type name.
136    */
137   @Override
138   public boolean hasName(String typeName) {
139      return nn(getClassMeta(typeName));
140   }
141
142   protected FluentMap<String,Object> properties() {
143      var m = filteredBeanPropertyMap();
144      map.forEach((k, v) -> m.a(k, v.toString(true)));
145      return m;
146   }
147
148   @Override
149   public String toString() {
150      return r(properties());
151   }
152
153   @SuppressWarnings({
154      "java:S3776" // Cognitive complexity acceptable for class registration with multiple conditions
155   })
156   private void addClass(ClassInfo ci) {
157      try {
158         if (nn(ci) && nn(ci.inner())) {
159            if (ci.isAssignableTo(Collection.class)) {
160               Collection<?> cc = BeanInstantiator.of(Collection.class).type(ci).preferZeroArgConstructor().run();
161               cc.forEach(x -> {
162                  if (x instanceof Class<?> x2)
163                     addClass(info(x2));
164                  else
165                     throw brex("Collection class '%s' passed to BeanRegistry does not contain Class objects.", ci.getName());
166               });
167            } else if (ci.isAssignableTo(Map.class)) {
168               Map<?,?> m = BeanInstantiator.of(Map.class).type(ci).preferZeroArgConstructor().run();
169               m.forEach((k, v) -> {
170                  var typeName = s(k);
171                  ClassMeta<?> val = null;
172                  if (v instanceof Type v2)
173                     val = bc.getClassMeta(v2);
174                  else if (isArray(v))
175                     val = getTypedClassMeta(v);
176                  else
177                     throw brex("Class '%s' was passed to BeanRegistry but value of type '%s' found in map is not a Type object.", ci.getName(), cn(v));
178                  addToMap(typeName, val);
179               });
180            } else {
181               // @formatter:off
182               var typeName = ap.find(Marshalled.class, ci)
183                  .stream()
184                  .map(x -> x.inner().typeName())
185                  .filter(Shorts::ine)
186                  .findFirst()
187                  .orElseThrow(() -> brex("Class '%s' was passed to BeanRegistry but it doesn't have a @Marshalled(typeName) annotation defined.", ci.getName()));
188               // @formatter:on
189               addToMap(typeName, bc.getClassMeta(ci.inner()));
190            }
191         }
192      } catch (BeanRuntimeException e) {
193         throw e;
194      } catch (Exception e) {
195         throw brex(e);
196      }
197   }
198
199   private void addToMap(String typeName, ClassMeta<?> cm) {
200      map.put(typeName, cm);
201      reverseMap.put(cm.inner(), typeName);
202   }
203
204   private ClassMeta<?> getTypedClassMeta(Object array) {
205      var len = Array.getLength(array);
206      if (len == 0)
207         throw brex("Map entry had an empty array value.");
208      var type = (Type)Array.get(array, 0);
209      var args = new Type[len - 1];
210      for (var i = 1; i < len; i++)
211         args[i - 1] = (Type)Array.get(array, i);
212      return bc.getClassMeta(type, args);
213   }
214}