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