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   public 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   public 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   public 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}