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