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}