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