| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.xmlschema; |
| |
| import static org.apache.juneau.internal.ArrayUtils.*; |
| import static org.apache.juneau.xml.annotation.XmlFormat.*; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.util.regex.*; |
| |
| import javax.xml.*; |
| import javax.xml.transform.stream.*; |
| import javax.xml.validation.*; |
| |
| import org.apache.juneau.*; |
| import org.apache.juneau.serializer.*; |
| import org.apache.juneau.xml.*; |
| import org.apache.juneau.xml.annotation.*; |
| import org.w3c.dom.bootstrap.*; |
| import org.w3c.dom.ls.*; |
| |
| /** |
| * Session object that lives for the duration of a single use of {@link XmlSchemaSerializer}. |
| * |
| * <p> |
| * This class is NOT thread safe. |
| * It is typically discarded after one-time use although it can be reused within the same thread. |
| */ |
| public class XmlSchemaSerializerSession extends XmlSerializerSession { |
| |
| /** |
| * Create a new session using properties specified in the context. |
| * |
| * @param ctx |
| * The context creating this session object. |
| * The context contains all the configuration settings for this object. |
| * @param args |
| * Runtime arguments. |
| * These specify session-level information such as locale and URI context. |
| * It also include session-level properties that override the properties defined on the bean and |
| * serializer contexts. |
| */ |
| protected XmlSchemaSerializerSession(XmlSerializer ctx, SerializerSessionArgs args) { |
| super(ctx, args); |
| } |
| |
| @Override /* SerializerSession */ |
| protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException { |
| if (isEnableNamespaces() && isAutoDetectNamespaces()) |
| findNsfMappings(o); |
| |
| Namespace xs = getXsNamespace(); |
| Namespace[] allNs = append(new Namespace[]{getDefaultNamespace()}, getNamespaces()); |
| |
| Schemas schemas = new Schemas(this, xs, getDefaultNamespace(), allNs); |
| schemas.process(o); |
| schemas.serializeTo(out.getWriter()); |
| } |
| |
| /** |
| * Returns an XML-Schema validator based on the output returned by {@link #doSerialize(SerializerPipe, Object)}; |
| * |
| * @param out The target writer. |
| * @param o The object to serialize. |
| * @return The new validator. |
| * @throws Exception If a problem was detected in the XML-Schema output produced by this serializer. |
| */ |
| public Validator getValidator(SerializerPipe out, Object o) throws Exception { |
| doSerialize(out, o); |
| String xmlSchema = out.getWriter().toString(); |
| |
| // create a SchemaFactory capable of understanding WXS schemas |
| SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); |
| |
| if (xmlSchema.indexOf('\u0000') != -1) { |
| |
| // Break it up into a map of namespaceURI->schema document |
| final Map<String,String> schemas = new HashMap<>(); |
| String[] ss = xmlSchema.split("\u0000"); |
| xmlSchema = ss[0]; |
| for (String s : ss) { |
| Matcher m = pTargetNs.matcher(s); |
| if (m.find()) |
| schemas.put(m.group(1), s); |
| } |
| |
| // Create a custom resolver |
| factory.setResourceResolver( |
| new LSResourceResolver() { |
| |
| @Override /* LSResourceResolver */ |
| public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { |
| |
| String schema = schemas.get(namespaceURI); |
| if (schema == null) |
| throw new FormattedRuntimeException("No schema found for namespaceURI ''{0}''", namespaceURI); |
| |
| try { |
| DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); |
| DOMImplementationLS domImplementationLS = (DOMImplementationLS)registry.getDOMImplementation("LS 3.0"); |
| LSInput in = domImplementationLS.createLSInput(); |
| in.setCharacterStream(new StringReader(schema)); |
| in.setSystemId(systemId); |
| return in; |
| |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| ); |
| } |
| return factory.newSchema(new StreamSource(new StringReader(xmlSchema))).newValidator(); |
| } |
| |
| private static Pattern pTargetNs = Pattern.compile("targetNamespace=['\"]([^'\"]+)['\"]"); |
| |
| |
| /* An instance of a global element, global attribute, or XML type to be serialized. */ |
| private static class QueueEntry { |
| Namespace ns; |
| String name; |
| ClassMeta<?> cm; |
| QueueEntry(Namespace ns, String name, ClassMeta<?> cm) { |
| this.ns = ns; |
| this.name = name; |
| this.cm = cm; |
| } |
| } |
| |
| /* An encapsulation of all schemas present in the metamodel of the serialized object. */ |
| final class Schemas extends LinkedHashMap<Namespace,Schema> { |
| |
| private static final long serialVersionUID = 1L; |
| |
| private Namespace defaultNs; |
| BeanSession session; |
| private LinkedList<QueueEntry> |
| elementQueue = new LinkedList<>(), |
| attributeQueue = new LinkedList<>(), |
| typeQueue = new LinkedList<>(); |
| |
| Schemas(BeanSession session, Namespace xs, Namespace defaultNs, Namespace[] allNs) throws IOException { |
| this.session = session; |
| this.defaultNs = defaultNs; |
| for (Namespace ns : allNs) |
| put(ns, new Schema(this, xs, ns, defaultNs, allNs)); |
| } |
| |
| Schema getSchema(Namespace ns) { |
| if (ns == null) |
| ns = defaultNs; |
| Schema s = get(ns); |
| if (s == null) |
| throw new FormattedRuntimeException("No schema defined for namespace ''{0}''", ns); |
| return s; |
| } |
| |
| void process(Object o) throws IOException { |
| ClassMeta<?> cm = getClassMetaForObject(o); |
| if (cm != null && cm.isOptional()) |
| cm = getClassMetaForObject(((Optional<?>)o).orElse(null)); |
| Namespace ns = defaultNs; |
| if (cm == null) |
| queueElement(ns, "null", object()); |
| else { |
| XmlClassMeta xmlMeta = getXmlClassMeta(cm); |
| if (cm.getDictionaryName() != null && xmlMeta.getNamespace() != null) |
| ns = xmlMeta.getNamespace(); |
| queueElement(ns, cm.getDictionaryName(), cm); |
| } |
| processQueue(); |
| } |
| |
| void processQueue() throws IOException { |
| boolean b; |
| do { |
| b = false; |
| while (! elementQueue.isEmpty()) { |
| QueueEntry q = elementQueue.removeFirst(); |
| b |= getSchema(q.ns).processElement(q.name, q.cm); |
| } |
| while (! typeQueue.isEmpty()) { |
| QueueEntry q = typeQueue.removeFirst(); |
| b |= getSchema(q.ns).processType(q.name, q.cm); |
| } |
| while (! attributeQueue.isEmpty()) { |
| QueueEntry q = attributeQueue.removeFirst(); |
| b |= getSchema(q.ns).processAttribute(q.name, q.cm); |
| } |
| } while (b); |
| } |
| |
| void queueElement(Namespace ns, String name, ClassMeta<?> cm) { |
| elementQueue.add(new QueueEntry(ns, name, cm)); |
| } |
| |
| void queueType(Namespace ns, String name, ClassMeta<?> cm) { |
| if (name == null) |
| name = XmlUtils.encodeElementName(cm); |
| typeQueue.add(new QueueEntry(ns, name, cm)); |
| } |
| |
| void queueAttribute(Namespace ns, String name, ClassMeta<?> cm) { |
| attributeQueue.add(new QueueEntry(ns, name, cm)); |
| } |
| |
| void serializeTo(Writer w) throws IOException { |
| boolean b = false; |
| for (Schema s : values()) { |
| if (b) |
| w.append('\u0000'); |
| w.append(s.toString()); |
| b = true; |
| } |
| } |
| } |
| |
| @Override /* SerializerSession */ |
| protected boolean isTrimStrings() { |
| return super.isTrimStrings(); |
| } |
| |
| /* An encapsulation of a single schema. */ |
| private final class Schema { |
| private StringWriter sw = new StringWriter(); |
| private XmlWriter w; |
| private Namespace defaultNs, targetNs; |
| private Schemas schemas; |
| private Set<String> |
| processedTypes = new HashSet<>(), |
| processedAttributes = new HashSet<>(), |
| processedElements = new HashSet<>(); |
| |
| @SuppressWarnings("synthetic-access") |
| public Schema(Schemas schemas, Namespace xs, Namespace targetNs, Namespace defaultNs, Namespace[] allNs) throws IOException { |
| this.schemas = schemas; |
| this.defaultNs = defaultNs; |
| this.targetNs = targetNs; |
| w = new XmlWriter(sw, isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), null, true, null); |
| int i = indent; |
| w.oTag(i, "schema"); |
| w.attr("xmlns", xs.getUri()); |
| w.attr("targetNamespace", targetNs.getUri()); |
| w.attr("elementFormDefault", "qualified"); |
| if (targetNs != defaultNs) |
| w.attr("attributeFormDefault", "qualified"); |
| for (Namespace ns2 : allNs) |
| w.attr("xmlns", ns2.getName(), ns2.getUri()); |
| w.append('>').nl(i); |
| for (Namespace ns : allNs) { |
| if (ns != targetNs) { |
| w.oTag(i+1, "import") |
| .attr("namespace", ns.getUri()) |
| .attr("schemaLocation", ns.getName()+".xsd") |
| .append("/>").nl(i+1); |
| } |
| } |
| } |
| |
| boolean processElement(String name, ClassMeta<?> cm) throws IOException { |
| if (processedElements.contains(name)) |
| return false; |
| processedElements.add(name); |
| |
| ClassMeta<?> ft = cm.getSerializedClassMeta(schemas.session); |
| if (name == null) |
| name = getElementName(ft); |
| Namespace ns = first(getXmlClassMeta(ft).getNamespace(), defaultNs); |
| String type = getXmlType(ns, ft); |
| |
| w.oTag(indent+1, "element") |
| .attr("name", XmlUtils.encodeElementName(name)) |
| .attr("type", type) |
| .append('/').append('>').nl(indent+1); |
| |
| schemas.queueType(ns, null, ft); |
| schemas.processQueue(); |
| return true; |
| } |
| |
| boolean processAttribute(String name, ClassMeta<?> cm) throws IOException { |
| if (processedAttributes.contains(name)) |
| return false; |
| processedAttributes.add(name); |
| |
| String type = getXmlAttrType(cm); |
| |
| w.oTag(indent+1, "attribute") |
| .attr("name", name) |
| .attr("type", type) |
| .append('/').append('>').nl(indent+1); |
| |
| return true; |
| } |
| |
| boolean processType(String name, ClassMeta<?> cm) throws IOException { |
| if (processedTypes.contains(name)) |
| return false; |
| processedTypes.add(name); |
| |
| int i = indent + 1; |
| |
| cm = cm.getSerializedClassMeta(schemas.session); |
| while (cm.isOptional()) |
| cm = cm.getElementType(); |
| |
| XmlBeanMeta xbm = cm.isBean() ? getXmlBeanMeta(cm.getBeanMeta()) : null; |
| |
| w.oTag(i, "complexType") |
| .attr("name", name); |
| |
| // This element can have mixed content if: |
| // 1) It's a generic Object (so it can theoretically be anything) |
| // 2) The bean has a property defined with @XmlFormat.CONTENT. |
| if ((xbm != null && (xbm.getContentFormat() != null && xbm.getContentFormat().isOneOf(TEXT,TEXT_PWS,MIXED,MIXED_PWS,XMLTEXT))) || ! cm.isMapOrBean()) |
| w.attr("mixed", "true"); |
| |
| w.cTag().nl(i); |
| |
| boolean hasAnyAttrs = false; |
| |
| if (! (cm.isMapOrBean() || cm.isCollectionOrArray() || (cm.isAbstract() && ! cm.isNumber()) || cm.isObject())) { |
| w.oTag(i+1, "attribute").attr("name", getBeanTypePropertyName(cm)).attr("type", "string").ceTag().nl(i+1); |
| |
| } else { |
| |
| //----- Bean ----- |
| if (cm.isBean()) { |
| BeanMeta<?> bm = cm.getBeanMeta(); |
| |
| boolean hasChildElements = false; |
| |
| for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) { |
| if (pMeta.canRead()) { |
| XmlFormat bpXml = getXmlBeanPropertyMeta(pMeta).getXmlFormat(); |
| if (bpXml == ATTRS) |
| hasAnyAttrs = true; |
| else if (bpXml != XmlFormat.ATTR) |
| hasChildElements = true; |
| } |
| } |
| |
| XmlBeanMeta xbm2 = getXmlBeanMeta(bm); |
| if (xbm2.getContentProperty() != null && xbm2.getContentFormat() == ELEMENTS) { |
| w.sTag(i+1, "sequence").nl(i+1); |
| w.oTag(i+2, "any") |
| .attr("processContents", "skip") |
| .attr("minOccurs", 0) |
| .ceTag().nl(i+2); |
| w.eTag(i+1, "sequence").nl(i+1); |
| |
| } else if (hasChildElements) { |
| |
| boolean hasOtherNsElement = false; |
| boolean hasCollapsed = false; |
| |
| for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) { |
| if (pMeta.canRead()) { |
| XmlBeanPropertyMeta xmlMeta = getXmlBeanPropertyMeta(pMeta); |
| if (xmlMeta.getXmlFormat() != ATTR) { |
| if (xmlMeta.getNamespace() != null) { |
| ClassMeta<?> ct2 = pMeta.getClassMeta(); |
| Namespace cNs = first(xmlMeta.getNamespace(), getXmlClassMeta(ct2).getNamespace(), getXmlClassMeta(cm).getNamespace(), defaultNs); |
| // Child element is in another namespace. |
| schemas.queueElement(cNs, pMeta.getName(), ct2); |
| hasOtherNsElement = true; |
| } |
| if (xmlMeta.getXmlFormat() == COLLAPSED) |
| hasCollapsed = true; |
| } |
| } |
| } |
| |
| if (hasAnyAttrs) { |
| w.oTag(i+1, "anyAttribute").attr("processContents", "skip").ceTag().nl(i+1); |
| } else if (hasOtherNsElement || hasCollapsed) { |
| // If this bean has any child elements in another namespace, |
| // we need to add an <any> element. |
| w.oTag(i+1, "choice").attr("maxOccurs", "unbounded").cTag().nl(i+1); |
| w.oTag(i+2, "any") |
| .attr("processContents", "skip") |
| .attr("minOccurs", 0) |
| .ceTag().nl(i+2); |
| w.eTag(i+1, "choice").nl(i+1); |
| |
| } else { |
| w.sTag(i+1, "all").nl(i+1); |
| for (BeanPropertyMeta pMeta : bm.getPropertyMetas()) { |
| if (pMeta.canRead()) { |
| XmlBeanPropertyMeta xmlMeta = getXmlBeanPropertyMeta(pMeta); |
| if (xmlMeta.getXmlFormat() != ATTR) { |
| boolean isCollapsed = xmlMeta.getXmlFormat() == COLLAPSED; |
| ClassMeta<?> ct2 = pMeta.getClassMeta(); |
| String childName = pMeta.getName(); |
| if (isCollapsed) { |
| if (xmlMeta.getChildName() != null) |
| childName = xmlMeta.getChildName(); |
| ct2 = pMeta.getClassMeta().getElementType(); |
| } |
| Namespace cNs = first(xmlMeta.getNamespace(), getXmlClassMeta(ct2).getNamespace(), getXmlClassMeta(cm).getNamespace(), defaultNs); |
| if (xmlMeta.getNamespace() == null) { |
| w.oTag(i+2, "element") |
| .attr("name", XmlUtils.encodeElementName(childName), false) |
| .attr("type", getXmlType(cNs, ct2)) |
| .attr("minOccurs", 0); |
| |
| w.ceTag().nl(i+2); |
| } else { |
| // Child element is in another namespace. |
| schemas.queueElement(cNs, pMeta.getName(), ct2); |
| hasOtherNsElement = true; |
| } |
| } |
| } |
| } |
| w.eTag(i+1, "all").nl(i+1); |
| } |
| |
| } |
| |
| for (BeanPropertyMeta pMeta : getXmlBeanMeta(bm).getAttrProperties().values()) { |
| if (pMeta.canRead()) { |
| Namespace pNs = getXmlBeanPropertyMeta(pMeta).getNamespace(); |
| if (pNs == null) |
| pNs = defaultNs; |
| |
| // If the bean attribute has a different namespace than the bean, then it needs to |
| // be added as a top-level entry in the appropriate schema file. |
| if (pNs != targetNs) { |
| schemas.queueAttribute(pNs, pMeta.getName(), pMeta.getClassMeta()); |
| w.oTag(i+1, "attribute") |
| //.attr("name", pMeta.getName(), true) |
| .attr("ref", pNs.getName() + ':' + pMeta.getName()) |
| .ceTag().nl(i+1); |
| } |
| |
| // Otherwise, it's just a plain attribute of this bean. |
| else { |
| if (! hasAnyAttrs) { |
| w.oTag(i+1, "attribute") |
| .attr("name", pMeta.getName(), true) |
| .attr("type", getXmlAttrType(pMeta.getClassMeta())) |
| .ceTag().nl(i+1); |
| } |
| } |
| } |
| } |
| |
| //----- Collection ----- |
| } else if (cm.isCollectionOrArray()) { |
| ClassMeta<?> elementType = cm.getElementType(); |
| if (elementType.isObject()) { |
| w.sTag(i+1, "sequence").nl(i+1); |
| w.oTag(i+2, "any") |
| .attr("processContents", "skip") |
| .attr("maxOccurs", "unbounded") |
| .attr("minOccurs", "0") |
| .ceTag().nl(i+2); |
| w.eTag(i+1, "sequence").nl(i+1); |
| } else { |
| Namespace cNs = first(getXmlClassMeta(elementType).getNamespace(), getXmlClassMeta(cm).getNamespace(), defaultNs); |
| schemas.queueType(cNs, null, elementType); |
| w.sTag(i+1, "sequence").nl(i+1); |
| w.oTag(i+2, "any") |
| .attr("processContents", "skip") |
| .attr("maxOccurs", "unbounded") |
| .attr("minOccurs", "0") |
| .ceTag().nl(i+2); |
| w.eTag(i+1, "sequence").nl(i+1); |
| } |
| |
| //----- Map ----- |
| } else if (cm.isMap() || cm.isAbstract() || cm.isObject()) { |
| w.sTag(i+1, "sequence").nl(i+1); |
| w.oTag(i+2, "any") |
| .attr("processContents", "skip") |
| .attr("maxOccurs", "unbounded") |
| .attr("minOccurs", "0") |
| .ceTag().nl(i+2); |
| w.eTag(i+1, "sequence").nl(i+1); |
| } |
| |
| if (! hasAnyAttrs) { |
| w.oTag(i+1, "attribute") |
| .attr("name", getBeanTypePropertyName(null)) |
| .attr("type", "string") |
| .ceTag().nl(i+1); |
| } |
| } |
| |
| w.eTag(i, "complexType").nl(i); |
| schemas.processQueue(); |
| |
| return true; |
| } |
| |
| private String getElementName(ClassMeta<?> cm) { |
| cm = cm.getSerializedClassMeta(schemas.session); |
| String name = cm.getDictionaryName(); |
| |
| if (name == null) { |
| if (cm.isBoolean()) |
| name = "boolean"; |
| else if (cm.isNumber()) |
| name = "number"; |
| else if (cm.isCollectionOrArray()) |
| name = "array"; |
| else if (! (cm.isMapOrBean() || cm.isCollectionOrArray() || cm.isObject() || cm.isAbstract())) |
| name = "string"; |
| else |
| name = "object"; |
| } |
| return name; |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| try { |
| w.eTag(indent, "schema").nl(indent); |
| } catch (IOException e) { |
| throw new RuntimeException(e); // Shouldn't happen. |
| } |
| return sw.toString(); |
| } |
| |
| private String getXmlType(Namespace currentNs, ClassMeta<?> cm) { |
| String name = null; |
| cm = cm.getSerializedClassMeta(schemas.session); |
| if (currentNs == targetNs) { |
| if (cm.isPrimitive()) { |
| if (cm.isBoolean()) |
| name = "boolean"; |
| else if (cm.isNumber()) { |
| if (cm.isDecimal()) |
| name = "decimal"; |
| else |
| name = "integer"; |
| } |
| } |
| } |
| if (name == null) { |
| name = XmlUtils.encodeElementName(cm); |
| schemas.queueType(currentNs, name, cm); |
| return currentNs.getName() + ":" + name; |
| } |
| |
| return name; |
| } |
| } |
| |
| @SafeVarargs |
| static <T> T first(T...tt) { |
| for (T t : tt) |
| if (t != null) |
| return t; |
| return null; |
| } |
| |
| static String getXmlAttrType(ClassMeta<?> cm) { |
| if (cm.isBoolean()) |
| return "boolean"; |
| if (cm.isNumber()) { |
| if (cm.isDecimal()) |
| return "decimal"; |
| return "integer"; |
| } |
| return "string"; |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Other methods |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| @Override /* Session */ |
| public ObjectMap toMap() { |
| return super.toMap() |
| .append("XmlSchemaSerializerSession", new DefaultFilteringObjectMap() |
| ); |
| } |
| } |