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}