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