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