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