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