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