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