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