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.xml; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.ThrowableUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022import static org.apache.juneau.xml.annotation.XmlFormat.*; 023 024import java.util.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.commons.lang.*; 028import org.apache.juneau.commons.reflect.*; 029import org.apache.juneau.xml.annotation.*; 030 031/** 032 * Metadata on beans specific to the XML serializers and parsers pulled from the {@link Xml @Xml} annotation on the 033 * class. 034 * 035 * <h5 class='section'>See Also:</h5><ul> 036 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/XmlBasics">XML Basics</a> 037 * </ul> 038 */ 039public class XmlBeanMeta extends ExtendedBeanMeta { 040 041 private static class XmlBeanMetaBuilder { 042 Map<String,BeanPropertyMeta> attrs = map(), elements = map(), collapsedProperties = map(); 043 BeanPropertyMeta attrsProperty, contentProperty; 044 XmlFormat contentFormat = DEFAULT; 045 046 XmlBeanMetaBuilder(BeanMeta<?> beanMeta, XmlMetaProvider mp) { 047 var c = beanMeta.getClassMeta().inner(); 048 var ci = beanMeta.getClassMeta(); 049 var defaultFormat = Value.<XmlFormat>empty(); 050 051 beanMeta.getClassMeta().getBeanContext().getAnnotationProvider().find(Xml.class, ci).stream().map(AnnotationInfo::inner).forEach(x -> { 052 var xf = x.format(); 053 if (xf == ATTRS) 054 defaultFormat.set(XmlFormat.ATTR); 055 else if (xf.isOneOf(ELEMENTS, DEFAULT)) 056 defaultFormat.set(ELEMENT); 057 else if (xf == VOID) { 058 contentFormat = VOID; 059 defaultFormat.set(VOID); 060 } else 061 throw bex(c, "Invalid format specified in @Xml annotation on bean: {0}. Must be one of the following: DEFAULT,ATTRS,ELEMENTS,VOID", x.format()); 062 }); 063 064 beanMeta.getProperties().values().forEach(p -> { 065 var xf = mp.getXmlBeanPropertyMeta(p).getXmlFormat(); 066 var pcm = p.getClassMeta(); 067 if (xf == ATTR) { 068 attrs.put(p.getName(), p); 069 } else if (xf == ELEMENT) { 070 elements.put(p.getName(), p); 071 } else if (xf == COLLAPSED) { 072 collapsedProperties.put(p.getName(), p); 073 } else if (xf == DEFAULT) { 074 if (defaultFormat.get() == ATTR) 075 attrs.put(p.getName(), p); 076 else 077 elements.put(p.getName(), p); 078 } else if (xf == ATTRS) { 079 if (nn(attrsProperty)) 080 throw bex(c, "Multiple instances of ATTRS properties defined on class. Only one property can be designated as such."); 081 if (! pcm.isMapOrBean()) 082 throw bex(c, "Invalid type for ATTRS property. Only properties of type Map and bean can be used."); 083 attrsProperty = p; 084 } else if (xf.isOneOf(ELEMENTS, MIXED, MIXED_PWS, TEXT, TEXT_PWS, XMLTEXT)) { 085 if (xf.isOneOf(ELEMENTS, MIXED, MIXED_PWS) && ! pcm.isCollectionOrArray()) 086 throw bex(c, "Invalid type for {0} property. Only properties of type Collection and array can be used.", xf); 087 if (nn(contentProperty)) { 088 if (xf == contentFormat) 089 throw bex(c, "Multiple instances of {0} properties defined on class. Only one property can be designated as such.", xf); 090 throw bex(c, "{0} and {1} properties found on the same bean. Only one property can be designated as such.", contentFormat, xf); 091 } 092 contentProperty = p; 093 contentFormat = xf; 094 } 095 // Look for any properties that are collections with @Xml.childName specified. 096 String n = mp.getXmlBeanPropertyMeta(p).getChildName(); 097 if (nn(n)) { 098 if (collapsedProperties.containsKey(n) && collapsedProperties.get(n) != p) 099 throw bex(c, "Multiple properties found with the child name ''{0}''.", n); 100 collapsedProperties.put(n, p); 101 } 102 }); 103 } 104 } 105 106 // XML related fields 107 private final Map<String,BeanPropertyMeta> attrs; // Map of bean properties that are represented as XML attributes. 108 private final Map<String,BeanPropertyMeta> elements; // Map of bean properties that are represented as XML elements. 109 private final BeanPropertyMeta attrsProperty; // Bean property that contain XML attribute key/value pairs for this bean. 110 private final Map<String,BeanPropertyMeta> collapsedProperties; // Properties defined with @Xml.childName annotation. 111 private final BeanPropertyMeta contentProperty; 112 113 private final XmlFormat contentFormat; 114 115 /** 116 * Constructor. 117 * 118 * @param beanMeta The metadata on the bean that this metadata applies to. 119 * @param mp XML metadata provider (for finding information about other artifacts). 120 */ 121 public XmlBeanMeta(BeanMeta<?> beanMeta, XmlMetaProvider mp) { 122 super(beanMeta); 123 124 var c = beanMeta.getClassMeta().inner(); 125 var b = new XmlBeanMetaBuilder(beanMeta, mp); 126 127 attrs = u(b.attrs); 128 elements = u(b.elements); 129 attrsProperty = b.attrsProperty; 130 collapsedProperties = u(b.collapsedProperties); 131 contentProperty = b.contentProperty; 132 contentFormat = b.contentFormat; 133 134 // Do some validation. 135 if (nn(contentProperty) || contentFormat == XmlFormat.VOID) { 136 if (! elements.isEmpty()) 137 throw bex(c, "{0} and ELEMENT properties found on the same bean. These cannot be mixed.", contentFormat); 138 if (! collapsedProperties.isEmpty()) 139 throw bex(c, "{0} and COLLAPSED properties found on the same bean. These cannot be mixed.", contentFormat); 140 } 141 142 if (! collapsedProperties.isEmpty()) { 143 if (! Collections.disjoint(elements.keySet(), collapsedProperties.keySet())) 144 throw bex(c, "Child element name conflicts found with another property."); 145 } 146 } 147 148 /** 149 * The list of properties that should be rendered as XML attributes. 150 * 151 * @return Map of property names to property metadata. 152 */ 153 public Map<String,BeanPropertyMeta> getAttrProperties() { return attrs; } 154 155 /** 156 * Returns the format of the inner XML content of this bean. 157 * 158 * <p> 159 * Can be one of the following: 160 * <ul> 161 * <li>{@link XmlFormat#ELEMENTS} 162 * <li>{@link XmlFormat#MIXED} 163 * <li>{@link XmlFormat#MIXED_PWS} 164 * <li>{@link XmlFormat#TEXT} 165 * <li>{@link XmlFormat#TEXT_PWS} 166 * <li>{@link XmlFormat#XMLTEXT} 167 * <li>{@link XmlFormat#VOID} 168 * <li><jk>null</jk> 169 * </ul> 170 * 171 * @return The format of the inner XML content of this bean. 172 */ 173 public XmlFormat getContentFormat() { return contentFormat; } 174 175 /** 176 * The property that represents the inner XML content of this bean. 177 * 178 * @return The bean property metadata, or <jk>null</jk> if there is no such method. 179 */ 180 public BeanPropertyMeta getContentProperty() { return contentProperty; } 181 182 /** 183 * The list of names of properties that should be rendered as XML attributes. 184 * 185 * @return Set of property names. 186 */ 187 protected Set<String> getAttrPropertyNames() { return attrs.keySet(); } 188 189 /** 190 * The property that returns a map of XML attributes as key/value pairs. 191 * 192 * @return The bean property metadata, or <jk>null</jk> if there is no such method. 193 */ 194 protected BeanPropertyMeta getAttrsProperty() { return attrsProperty; } 195 196 /** 197 * The name of the property that returns a map of XML attributes as key/value pairs. 198 * 199 * @return The bean property name, or <jk>null</jk> if there is no such method. 200 */ 201 protected String getAttrsPropertyName() { return attrsProperty == null ? null : attrsProperty.getName(); } 202 203 /** 204 * The list of properties that should be rendered as collapsed child elements. 205 * <br>See {@link Xml#childName() @Xml(childName)} 206 * 207 * @return Map of property names to property metadata. 208 */ 209 protected Map<String,BeanPropertyMeta> getCollapsedProperties() { return collapsedProperties; } 210 211 /** 212 * The list of names of properties that should be rendered as collapsed child elements. 213 * 214 * @return Set of property names. 215 */ 216 protected Set<String> getCollapsedPropertyNames() { return collapsedProperties.keySet(); } 217 218 /** 219 * The name of the property that represents the inner XML content of this bean. 220 * 221 * @return The bean property name, or <jk>null</jk> if there is no such method. 222 */ 223 protected String getContentPropertyName() { return contentProperty == null ? null : contentProperty.getName(); } 224 225 /** 226 * The list of properties that should be rendered as child elements. 227 * 228 * @return Map of property names to property metadata. 229 */ 230 protected Map<String,BeanPropertyMeta> getElementProperties() { return elements; } 231 232 /** 233 * The list of names of properties that should be rendered as child elements. 234 * 235 * @return Set of property names. 236 */ 237 protected Set<String> getElementPropertyNames() { return elements.keySet(); } 238 239 /** 240 * Returns bean property meta with the specified name. 241 * 242 * <p> 243 * This is identical to calling {@link BeanMeta#getPropertyMeta(String)} except it first retrieves the bean property 244 * meta based on the child name (e.g. a property whose name is "people", but whose child name is "person"). 245 * 246 * @param fieldName The bean property name. 247 * @return The property metadata. 248 */ 249 protected BeanPropertyMeta getPropertyMeta(String fieldName) { 250 if (nn(collapsedProperties)) { 251 BeanPropertyMeta bpm = collapsedProperties.get(fieldName); 252 if (bpm == null) 253 bpm = collapsedProperties.get("*"); 254 if (nn(bpm)) 255 return bpm; 256 } 257 return getBeanMeta().getPropertyMeta(fieldName); 258 } 259}