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.ArrayUtils.*;
016import static org.apache.juneau.xml.annotation.XmlFormat.*;
017
018import java.io.*;
019import java.util.*;
020import java.util.regex.*;
021
022import javax.xml.*;
023import javax.xml.transform.stream.*;
024import javax.xml.validation.*;
025
026import org.apache.juneau.*;
027import org.apache.juneau.serializer.*;
028import org.apache.juneau.xml.annotation.*;
029import org.w3c.dom.bootstrap.*;
030import org.w3c.dom.ls.*;
031
032/**
033 * Session object that lives for the duration of a single use of {@link XmlSchemaSerializer}.
034 * 
035 * <p>
036 * This class is NOT thread safe.
037 * It is typically discarded after one-time use although it can be reused within the same thread.
038 */
039public class XmlSchemaSerializerSession extends XmlSerializerSession {
040
041   /**
042    * Create a new session using properties specified in the context.
043    * 
044    * @param ctx
045    *    The context creating this session object.
046    *    The context contains all the configuration settings for this object.
047    * @param args
048    *    Runtime arguments.
049    *    These specify session-level information such as locale and URI context.
050    *    It also include session-level properties that override the properties defined on the bean and
051    *    serializer contexts.
052    */
053   protected XmlSchemaSerializerSession(XmlSerializer ctx, SerializerSessionArgs args) {
054      super(ctx, args);
055   }
056
057   @Override /* SerializerSession */
058   protected void doSerialize(SerializerPipe out, Object o) throws Exception {
059      if (enableNamespaces && autoDetectNamespaces)
060         findNsfMappings(o);
061
062      Namespace xs = xsNamespace;
063      Namespace[] allNs = append(new Namespace[]{defaultNamespace}, namespaces);
064
065      Schemas schemas = new Schemas(this, xs, defaultNamespace, allNs);
066      schemas.process(o);
067      schemas.serializeTo(out.getWriter());
068   }
069
070   /**
071    * Returns an XML-Schema validator based on the output returned by {@link #doSerialize(SerializerPipe, Object)};
072    * 
073    * @param out The target writer.
074    * @param o The object to serialize.
075    * @return The new validator.
076    * @throws Exception If a problem was detected in the XML-Schema output produced by this serializer.
077    */
078   public Validator getValidator(SerializerPipe out, Object o) throws Exception {
079      doSerialize(out, o);
080      String xmlSchema = out.getWriter().toString();
081
082      // create a SchemaFactory capable of understanding WXS schemas
083      SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
084
085      if (xmlSchema.indexOf('\u0000') != -1) {
086
087         // Break it up into a map of namespaceURI->schema document
088         final Map<String,String> schemas = new HashMap<>();
089         String[] ss = xmlSchema.split("\u0000");
090         xmlSchema = ss[0];
091         for (String s : ss) {
092            Matcher m = pTargetNs.matcher(s);
093            if (m.find())
094               schemas.put(m.group(1), s);
095         }
096
097         // Create a custom resolver
098         factory.setResourceResolver(
099            new LSResourceResolver() {
100
101               @Override /* LSResourceResolver */
102               public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) {
103
104                  String schema = schemas.get(namespaceURI);
105                  if (schema == null)
106                     throw new FormattedRuntimeException("No schema found for namespaceURI ''{0}''", namespaceURI);
107
108                  try {
109                     DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
110                     DOMImplementationLS domImplementationLS = (DOMImplementationLS)registry.getDOMImplementation("LS 3.0");
111                     LSInput in = domImplementationLS.createLSInput();
112                     in.setCharacterStream(new StringReader(schema));
113                     in.setSystemId(systemId);
114                     return in;
115
116                  } catch (Exception e) {
117                     throw new RuntimeException(e);
118                  }
119               }
120            }
121         );
122      }
123      return factory.newSchema(new StreamSource(new StringReader(xmlSchema))).newValidator();
124   }
125
126   private static Pattern pTargetNs = Pattern.compile("targetNamespace=['\"]([^'\"]+)['\"]");
127
128
129   /* An instance of a global element, global attribute, or XML type to be serialized. */
130   private static class QueueEntry {
131      Namespace ns;
132      String name;
133      ClassMeta<?> cm;
134      QueueEntry(Namespace ns, String name, ClassMeta<?> cm) {
135         this.ns = ns;
136         this.name = name;
137         this.cm = cm;
138      }
139   }
140
141   /* An encapsulation of all schemas present in the metamodel of the serialized object. */
142   final class Schemas extends LinkedHashMap<Namespace,Schema> {
143
144      private static final long serialVersionUID = 1L;
145
146      private Namespace defaultNs;
147      BeanSession session;
148      private LinkedList<QueueEntry>
149         elementQueue = new LinkedList<>(),
150         attributeQueue = new LinkedList<>(),
151         typeQueue = new LinkedList<>();
152
153      Schemas(BeanSession session, Namespace xs, Namespace defaultNs, Namespace[] allNs) throws IOException {
154         this.session = session;
155         this.defaultNs = defaultNs;
156         for (Namespace ns : allNs)
157            put(ns, new Schema(this, xs, ns, defaultNs, allNs));
158      }
159
160      Schema getSchema(Namespace ns) {
161         if (ns == null)
162            ns = defaultNs;
163         Schema s = get(ns);
164         if (s == null)
165            throw new FormattedRuntimeException("No schema defined for namespace ''{0}''", ns);
166         return s;
167      }
168
169      void process(Object o) throws IOException {
170         ClassMeta<?> cm = getClassMetaForObject(o);
171         Namespace ns = defaultNs;
172         if (cm == null)
173            queueElement(ns, "null", object());
174         else {
175            XmlClassMeta xmlMeta = cm.getExtendedMeta(XmlClassMeta.class);
176            if (cm.getDictionaryName() != null && xmlMeta.getNamespace() != null)
177               ns = xmlMeta.getNamespace();
178            queueElement(ns, cm.getDictionaryName(), cm);
179         }
180         processQueue();
181      }
182
183      void processQueue() throws IOException {
184         boolean b;
185         do {
186            b = false;
187            while (! elementQueue.isEmpty()) {
188               QueueEntry q = elementQueue.removeFirst();
189               b |= getSchema(q.ns).processElement(q.name, q.cm);
190            }
191            while (! typeQueue.isEmpty()) {
192               QueueEntry q = typeQueue.removeFirst();
193               b |= getSchema(q.ns).processType(q.name, q.cm);
194            }
195            while (! attributeQueue.isEmpty()) {
196               QueueEntry q = attributeQueue.removeFirst();
197               b |= getSchema(q.ns).processAttribute(q.name, q.cm);
198            }
199         } while (b);
200      }
201
202      void queueElement(Namespace ns, String name, ClassMeta<?> cm) {
203         elementQueue.add(new QueueEntry(ns, name, cm));
204      }
205
206      void queueType(Namespace ns, String name, ClassMeta<?> cm) {
207         if (name == null)
208            name = XmlUtils.encodeElementName(cm);
209         typeQueue.add(new QueueEntry(ns, name, cm));
210      }
211
212      void queueAttribute(Namespace ns, String name, ClassMeta<?> cm) {
213         attributeQueue.add(new QueueEntry(ns, name, cm));
214      }
215
216      void serializeTo(Writer w) throws IOException {
217         boolean b = false;
218         for (Schema s : values()) {
219            if (b)
220               w.append('\u0000');
221            w.append(s.toString());
222            b = true;
223         }
224      }
225   }
226
227   @Override /* SerializerSession */
228   protected boolean isUseWhitespace() {
229      return super.isUseWhitespace();
230   }
231   
232   @Override /* SerializerSession */
233   protected int getMaxIndent() {
234      return super.getMaxIndent();
235   }
236
237   @Override /* SerializerSession */
238   protected boolean isTrimStrings() {
239      return super.isTrimStrings();
240   }
241
242   @Override /* SerializerSession */
243   protected char getQuoteChar() {
244      return super.getQuoteChar();
245   }
246
247   /* An encapsulation of a single schema. */
248   private final class Schema {
249      private StringWriter sw = new StringWriter();
250      private XmlWriter w;
251      private Namespace defaultNs, targetNs;
252      private Schemas schemas;
253      private Set<String>
254         processedTypes = new HashSet<>(),
255         processedAttributes = new HashSet<>(),
256         processedElements = new HashSet<>();
257
258      public Schema(Schemas schemas, Namespace xs, Namespace targetNs, Namespace defaultNs, Namespace[] allNs) throws IOException {
259         this.schemas = schemas;
260         this.defaultNs = defaultNs;
261         this.targetNs = targetNs;
262         w = new XmlWriter(sw, isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), null, true, null);
263         int i = indent;
264         w.oTag(i, "schema");
265         w.attr("xmlns", xs.getUri());
266         w.attr("targetNamespace", targetNs.getUri());
267         w.attr("elementFormDefault", "qualified");
268         if (targetNs != defaultNs)
269            w.attr("attributeFormDefault", "qualified");
270         for (Namespace ns2 : allNs)
271            w.attr("xmlns", ns2.name, ns2.uri);
272         w.append('>').nl(i);
273         for (Namespace ns : allNs) {
274            if (ns != targetNs) {
275               w.oTag(i+1, "import")
276                  .attr("namespace", ns.getUri())
277                  .attr("schemaLocation", ns.getName()+".xsd")
278                  .append("/>").nl(i+1);
279            }
280         }
281      }
282
283      boolean processElement(String name, ClassMeta<?> cm) throws IOException {
284         if (processedElements.contains(name))
285            return false;
286         processedElements.add(name);
287
288         ClassMeta<?> ft = cm.getSerializedClassMeta(schemas.session);
289         if (name == null)
290            name = getElementName(ft);
291         Namespace ns = first(ft.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs);
292         String type = getXmlType(ns, ft);
293
294         w.oTag(indent+1, "element")
295            .attr("name", XmlUtils.encodeElementName(name))
296            .attr("type", type)
297            .append('/').append('>').nl(indent+1);
298
299         schemas.queueType(ns, null, ft);
300         schemas.processQueue();
301         return true;
302      }
303
304      boolean processAttribute(String name, ClassMeta<?> cm) throws IOException {
305         if (processedAttributes.contains(name))
306            return false;
307         processedAttributes.add(name);
308
309         String type = getXmlAttrType(cm);
310
311         w.oTag(indent+1, "attribute")
312            .attr("name", name)
313            .attr("type", type)
314            .append('/').append('>').nl(indent+1);
315
316         return true;
317      }
318
319      boolean processType(String name, ClassMeta<?> cm) throws IOException {
320         if (processedTypes.contains(name))
321            return false;
322         processedTypes.add(name);
323
324         int i = indent + 1;
325
326         cm = cm.getSerializedClassMeta(schemas.session);
327         XmlBeanMeta xbm = cm.isBean() ? cm.getBeanMeta().getExtendedMeta(XmlBeanMeta.class) : null;
328
329         w.oTag(i, "complexType")
330            .attr("name", name);
331
332         // This element can have mixed content if:
333         //    1) It's a generic Object (so it can theoretically be anything)
334         //    2) The bean has a property defined with @XmlFormat.CONTENT.
335         if ((xbm != null && (xbm.getContentFormat() != null && xbm.getContentFormat().isOneOf(TEXT,TEXT_PWS,MIXED,MIXED_PWS,XMLTEXT))) || ! cm.isMapOrBean())
336            w.attr("mixed", "true");
337
338         w.cTag().nl(i);
339
340         if (! (cm.isMapOrBean() || cm.isCollectionOrArray() || (cm.isAbstract() && ! cm.isNumber()) || cm.isObject())) {
341            w.oTag(i+1, "attribute").attr("name", getBeanTypePropertyName(cm)).attr("type", "string").ceTag().nl(i+1);
342
343         } else {
344
345            //----- Bean -----
346            if (cm.isBean()) {
347               BeanMeta<?> bm = cm.getBeanMeta();
348
349               boolean hasChildElements = false;
350
351               for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) {
352                  if (pMeta.canRead()) {
353                     XmlFormat pMetaFormat = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getXmlFormat();
354                     if (pMetaFormat != XmlFormat.ATTR)
355                        hasChildElements = true;
356                  }
357               }
358
359               XmlBeanMeta xbm2 = bm.getExtendedMeta(XmlBeanMeta.class);
360               if (xbm2.getContentProperty() != null && xbm2.getContentFormat() == ELEMENTS) {
361                  w.sTag(i+1, "sequence").nl(i+1);
362                  w.oTag(i+2, "any")
363                     .attr("processContents", "skip")
364                     .attr("minOccurs", 0)
365                     .ceTag().nl(i+2);
366                  w.eTag(i+1, "sequence").nl(i+1);
367
368               } else if (hasChildElements) {
369
370                  boolean hasOtherNsElement = false;
371                  boolean hasCollapsed = false;
372
373                  for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) {
374                     if (pMeta.canRead()) {
375                        XmlBeanPropertyMeta xmlMeta = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class);
376                        if (xmlMeta.getXmlFormat() != ATTR) {
377                           if (xmlMeta.getNamespace() != null) {
378                              ClassMeta<?> ct2 = pMeta.getClassMeta();
379                              Namespace cNs = first(xmlMeta.getNamespace(), ct2.getExtendedMeta(XmlClassMeta.class).getNamespace(), cm.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs);
380                              // Child element is in another namespace.
381                              schemas.queueElement(cNs, pMeta.getName(), ct2);
382                              hasOtherNsElement = true;
383                           }
384                           if (xmlMeta.getXmlFormat() == COLLAPSED)
385                              hasCollapsed = true;
386                        }
387                        
388                     }
389                  }
390
391                  if (hasOtherNsElement || hasCollapsed) {
392                     // If this bean has any child elements in another namespace,
393                     // we need to add an <any> element.
394                     w.oTag(i+1, "choice").attr("maxOccurs", "unbounded").cTag().nl(i+1);
395                     w.oTag(i+2, "any")
396                        .attr("processContents", "skip")
397                        .attr("minOccurs", 0)
398                        .ceTag().nl(i+2);
399                     w.eTag(i+1, "choice").nl(i+1);
400
401                  } else {
402                     w.sTag(i+1, "all").nl(i+1);
403                     for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) {
404                        if (pMeta.canRead()) {
405                           XmlBeanPropertyMeta xmlMeta = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class);
406                           if (xmlMeta.getXmlFormat() != ATTR) {
407                              boolean isCollapsed = xmlMeta.getXmlFormat() == COLLAPSED;
408                              ClassMeta<?> ct2 = pMeta.getClassMeta();
409                              String childName = pMeta.getName();
410                              if (isCollapsed) {
411                                 if (xmlMeta.getChildName() != null)
412                                    childName = xmlMeta.getChildName();
413                                 ct2 = pMeta.getClassMeta().getElementType();
414                              }
415                              Namespace cNs = first(xmlMeta.getNamespace(), ct2.getExtendedMeta(XmlClassMeta.class).getNamespace(), cm.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs);
416                              if (xmlMeta.getNamespace() == null) {
417                                 w.oTag(i+2, "element")
418                                    .attr("name", XmlUtils.encodeElementName(childName), false)
419                                    .attr("type", getXmlType(cNs, ct2))
420                                    .attr("minOccurs", 0);
421
422                                 w.ceTag().nl(i+2);
423                              } else {
424                                 // Child element is in another namespace.
425                                 schemas.queueElement(cNs, pMeta.getName(), ct2);
426                                 hasOtherNsElement = true;
427                              }
428                           }
429                        }
430                     }
431                     w.eTag(i+1, "all").nl(i+1);
432                  }
433
434               }
435
436               for (BeanPropertyMeta pMeta : bm.getExtendedMeta(XmlBeanMeta.class).getAttrProperties().values()) {
437                  if (pMeta.canRead()) {
438                     Namespace pNs = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace();
439                     if (pNs == null)
440                        pNs = defaultNs;
441
442                     // If the bean attribute has a different namespace than the bean, then it needs to
443                     // be added as a top-level entry in the appropriate schema file.
444                     if (pNs != targetNs) {
445                        schemas.queueAttribute(pNs, pMeta.getName(), pMeta.getClassMeta());
446                        w.oTag(i+1, "attribute")
447                           //.attr("name", pMeta.getName(), true)
448                           .attr("ref", pNs.getName() + ':' + pMeta.getName())
449                           .ceTag().nl(i+1);
450                     }
451
452                     // Otherwise, it's just a plain attribute of this bean.
453                     else {
454                        w.oTag(i+1, "attribute")
455                           .attr("name", pMeta.getName(), true)
456                           .attr("type", getXmlAttrType(pMeta.getClassMeta()))
457                           .ceTag().nl(i+1);
458                     }
459                  }
460               }
461
462            //----- Collection -----
463            } else if (cm.isCollectionOrArray()) {
464               ClassMeta<?> elementType = cm.getElementType();
465               if (elementType.isObject()) {
466                  w.sTag(i+1, "sequence").nl(i+1);
467                  w.oTag(i+2, "any")
468                     .attr("processContents", "skip")
469                     .attr("maxOccurs", "unbounded")
470                     .attr("minOccurs", "0")
471                     .ceTag().nl(i+2);
472                  w.eTag(i+1, "sequence").nl(i+1);
473               } else {
474                  Namespace cNs = first(elementType.getExtendedMeta(XmlClassMeta.class).getNamespace(), cm.getExtendedMeta(XmlClassMeta.class).getNamespace(), defaultNs);
475                  schemas.queueType(cNs, null, elementType);
476                  w.sTag(i+1, "sequence").nl(i+1);
477                  w.oTag(i+2, "any")
478                     .attr("processContents", "skip")
479                     .attr("maxOccurs", "unbounded")
480                     .attr("minOccurs", "0")
481                     .ceTag().nl(i+2);
482                  w.eTag(i+1, "sequence").nl(i+1);
483               }
484
485            //----- Map -----
486            } else if (cm.isMap() || cm.isAbstract() || cm.isObject()) {
487               w.sTag(i+1, "sequence").nl(i+1);
488               w.oTag(i+2, "any")
489                  .attr("processContents", "skip")
490                  .attr("maxOccurs", "unbounded")
491                  .attr("minOccurs", "0")
492                  .ceTag().nl(i+2);
493               w.eTag(i+1, "sequence").nl(i+1);
494            }
495
496            w.oTag(i+1, "attribute")
497               .attr("name", getBeanTypePropertyName(null))
498               .attr("type", "string")
499               .ceTag().nl(i+1);
500         }
501
502         w.eTag(i, "complexType").nl(i);
503         schemas.processQueue();
504
505         return true;
506      }
507
508      private String getElementName(ClassMeta<?> cm) {
509         cm = cm.getSerializedClassMeta(schemas.session);
510         String name = cm.getDictionaryName();
511
512         if (name == null) {
513            if (cm.isBoolean())
514               name = "boolean";
515            else if (cm.isNumber())
516               name = "number";
517            else if (cm.isCollectionOrArray())
518               name = "array";
519            else if (! (cm.isMapOrBean() || cm.isCollectionOrArray() || cm.isObject() || cm.isAbstract()))
520               name = "string";
521            else
522               name = "object";
523         }
524         return name;
525      }
526
527      @Override /* Object */
528      public String toString() {
529         try {
530            w.eTag(indent, "schema").nl(indent);
531         } catch (IOException e) {
532            throw new RuntimeException(e); // Shouldn't happen.
533         }
534         return sw.toString();
535      }
536
537      private String getXmlType(Namespace currentNs, ClassMeta<?> cm) {
538         String name = null;
539         cm = cm.getSerializedClassMeta(schemas.session);
540         if (currentNs == targetNs) {
541            if (cm.isPrimitive()) {
542               if (cm.isBoolean())
543                  name = "boolean";
544               else if (cm.isNumber()) {
545                  if (cm.isDecimal())
546                     name = "decimal";
547                  else
548                     name = "integer";
549               }
550            }
551         }
552         if (name == null) {
553            name = XmlUtils.encodeElementName(cm);
554            schemas.queueType(currentNs, name, cm);
555            return currentNs.getName() + ":" + name;
556         }
557
558         return name;
559      }
560   }
561
562   @SafeVarargs
563   static <T> T first(T...tt) {
564      for (T t : tt)
565         if (t != null)
566            return t;
567      return null;
568   }
569
570   static String getXmlAttrType(ClassMeta<?> cm) {
571      if (cm.isBoolean())
572         return "boolean";
573      if (cm.isNumber()) {
574         if (cm.isDecimal())
575            return "decimal";
576         return "integer";
577      }
578      return "string";
579   }
580}