/* * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.xml.internal.bind.v2.schemagen; import java.io.IOException; import java.io.OutputStream; import java.io.Writer; import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; import javax.activation.MimeType; import javax.xml.bind.SchemaOutputResolver; import javax.xml.bind.annotation.XmlElement; import javax.xml.namespace.QName; import javax.xml.transform.Result; import javax.xml.transform.stream.StreamResult; import com.sun.istack.internal.Nullable; import com.sun.istack.internal.NotNull; import com.sun.xml.internal.bind.Util; import com.sun.xml.internal.bind.api.CompositeStructure; import com.sun.xml.internal.bind.api.ErrorListener; import com.sun.xml.internal.bind.v2.TODO; import com.sun.xml.internal.bind.v2.WellKnownNamespace; import com.sun.xml.internal.bind.v2.util.CollisionCheckStack; import com.sun.xml.internal.bind.v2.util.StackRecorder; import static com.sun.xml.internal.bind.v2.WellKnownNamespace.XML_SCHEMA; import com.sun.xml.internal.bind.v2.model.core.Adapter; import com.sun.xml.internal.bind.v2.model.core.ArrayInfo; import com.sun.xml.internal.bind.v2.model.core.AttributePropertyInfo; import com.sun.xml.internal.bind.v2.model.core.ClassInfo; import com.sun.xml.internal.bind.v2.model.core.Element; import com.sun.xml.internal.bind.v2.model.core.ElementInfo; import com.sun.xml.internal.bind.v2.model.core.ElementPropertyInfo; import com.sun.xml.internal.bind.v2.model.core.EnumConstant; import com.sun.xml.internal.bind.v2.model.core.EnumLeafInfo; import com.sun.xml.internal.bind.v2.model.core.MapPropertyInfo; import com.sun.xml.internal.bind.v2.model.core.MaybeElement; import com.sun.xml.internal.bind.v2.model.core.NonElement; import com.sun.xml.internal.bind.v2.model.core.NonElementRef; import com.sun.xml.internal.bind.v2.model.core.PropertyInfo; import com.sun.xml.internal.bind.v2.model.core.ReferencePropertyInfo; import com.sun.xml.internal.bind.v2.model.core.TypeInfo; import com.sun.xml.internal.bind.v2.model.core.TypeInfoSet; import com.sun.xml.internal.bind.v2.model.core.TypeRef; import com.sun.xml.internal.bind.v2.model.core.ValuePropertyInfo; import com.sun.xml.internal.bind.v2.model.core.WildcardMode; import com.sun.xml.internal.bind.v2.model.impl.ClassInfoImpl; import com.sun.xml.internal.bind.v2.model.nav.Navigator; import com.sun.xml.internal.bind.v2.runtime.SwaRefAdapter; import static com.sun.xml.internal.bind.v2.schemagen.Util.*; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.Any; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.AttrDecls; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.ComplexExtension; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.ComplexType; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.ComplexTypeHost; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.ExplicitGroup; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.Import; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.List; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.LocalAttribute; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.LocalElement; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.Schema; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.SimpleExtension; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.SimpleRestrictionModel; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.SimpleType; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.SimpleTypeHost; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.TopLevelAttribute; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.TopLevelElement; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.TypeHost; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.ContentModelContainer; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.TypeDefParticle; import com.sun.xml.internal.bind.v2.schemagen.xmlschema.AttributeType; import com.sun.xml.internal.bind.v2.schemagen.episode.Bindings; import com.sun.xml.internal.txw2.TXW; import com.sun.xml.internal.txw2.TxwException; import com.sun.xml.internal.txw2.TypedXmlWriter; import com.sun.xml.internal.txw2.output.ResultFactory; import com.sun.xml.internal.txw2.output.XmlSerializer; import java.util.Collection; import org.xml.sax.SAXParseException; /** * Generates a set of W3C XML Schema documents from a set of Java classes. * *

* A client must invoke methods in the following order: *

    *
  1. Create a new {@link XmlSchemaGenerator} *
  2. Invoke {@link #add} methods, multiple times if necessary. *
  3. Invoke {@link #write} *
  4. Discard the {@link XmlSchemaGenerator}. *
* * @author Ryan Shoemaker * @author Kohsuke Kawaguchi (kk@kohsuke.org) */ public final class XmlSchemaGenerator { private static final Logger logger = Util.getClassLogger(); /** * Java classes to be written, organized by their namespace. * *

* We use a {@link TreeMap} here so that the suggested names will * be consistent across JVMs. * * @see SchemaOutputResolver#createOutput(String, String) */ private final Map namespaces = new TreeMap(NAMESPACE_COMPARATOR); /** * {@link ErrorListener} to send errors to. */ private ErrorListener errorListener; /** model navigator **/ private Navigator navigator; private final TypeInfoSet types; /** * Representation for xs:string. */ private final NonElement stringType; /** * Represents xs:anyType. */ private final NonElement anyType; /** * Used to detect cycles in anonymous types. */ private final CollisionCheckStack> collisionChecker = new CollisionCheckStack>(); public XmlSchemaGenerator( Navigator navigator, TypeInfoSet types ) { this.navigator = navigator; this.types = types; this.stringType = types.getTypeInfo(navigator.ref(String.class)); this.anyType = types.getAnyTypeInfo(); // populate the object for( ClassInfo ci : types.beans().values() ) add(ci); for( ElementInfo ei1 : types.getElementMappings(null).values() ) add(ei1); for( EnumLeafInfo ei : types.enums().values() ) add(ei); for( ArrayInfo a : types.arrays().values()) add(a); } private Namespace getNamespace(String uri) { Namespace n = namespaces.get(uri); if(n==null) namespaces.put(uri,n=new Namespace(uri)); return n; } /** * Adds a new class to the list of classes to be written. * *

* A {@link ClassInfo} may have two namespaces --- one for the element name * and the other for the type name. If they are different, we put the same * {@link ClassInfo} to two {@link Namespace}s. */ public void add( ClassInfo clazz ) { assert clazz!=null; String nsUri = null; if(clazz.getClazz()==navigator.asDecl(CompositeStructure.class)) return; // this is a special class we introduced for JAX-WS that we *don't* want in the schema if(clazz.isElement()) { // put element -> type reference nsUri = clazz.getElementName().getNamespaceURI(); Namespace ns = getNamespace(nsUri); ns.classes.add(clazz); ns.addDependencyTo(clazz.getTypeName()); // schedule writing this global element add(clazz.getElementName(),false,clazz); } QName tn = clazz.getTypeName(); if(tn!=null) { nsUri = tn.getNamespaceURI(); } else { // anonymous type if(nsUri==null) return; } Namespace n = getNamespace(nsUri); n.classes.add(clazz); // search properties for foreign namespace references for( PropertyInfo p : clazz.getProperties()) { n.processForeignNamespaces(p, 1); if (p instanceof AttributePropertyInfo) { AttributePropertyInfo ap = (AttributePropertyInfo) p; String aUri = ap.getXmlName().getNamespaceURI(); if(aUri.length()>0) { // global attribute getNamespace(aUri).addGlobalAttribute(ap); n.addDependencyTo(ap.getXmlName()); } } if (p instanceof ElementPropertyInfo) { ElementPropertyInfo ep = (ElementPropertyInfo) p; for (TypeRef tref : ep.getTypes()) { String eUri = tref.getTagName().getNamespaceURI(); if(eUri.length()>0 && !eUri.equals(n.uri)) { getNamespace(eUri).addGlobalElement(tref); n.addDependencyTo(tref.getTagName()); } } } if(generateSwaRefAdapter(p)) n.useSwaRef = true; MimeType mimeType = p.getExpectedMimeType(); if( mimeType != null ) { n.useMimeNs = true; } } // recurse on baseTypes to make sure that we can refer to them in the schema ClassInfo bc = clazz.getBaseClass(); if (bc != null) { add(bc); n.addDependencyTo(bc.getTypeName()); } } /** * Adds a new element to the list of elements to be written. */ public void add( ElementInfo elem ) { assert elem!=null; @SuppressWarnings("UnusedAssignment") boolean nillable = false; // default value QName name = elem.getElementName(); Namespace n = getNamespace(name.getNamespaceURI()); ElementInfo ei; if (elem.getScope() != null) { // (probably) never happens ei = this.types.getElementInfo(elem.getScope().getClazz(), name); } else { ei = this.types.getElementInfo(null, name); } XmlElement xmlElem = ei.getProperty().readAnnotation(XmlElement.class); if (xmlElem == null) { nillable = false; } else { nillable = xmlElem.nillable(); } n.elementDecls.put(name.getLocalPart(),n.new ElementWithType(nillable, elem.getContentType())); // search for foreign namespace references n.processForeignNamespaces(elem.getProperty(), 1); } public void add( EnumLeafInfo envm ) { assert envm!=null; String nsUri = null; if(envm.isElement()) { // put element -> type reference nsUri = envm.getElementName().getNamespaceURI(); Namespace ns = getNamespace(nsUri); ns.enums.add(envm); ns.addDependencyTo(envm.getTypeName()); // schedule writing this global element add(envm.getElementName(),false,envm); } final QName typeName = envm.getTypeName(); if (typeName != null) { nsUri = typeName.getNamespaceURI(); } else { if(nsUri==null) return; // anonymous type } Namespace n = getNamespace(nsUri); n.enums.add(envm); // search for foreign namespace references n.addDependencyTo(envm.getBaseType().getTypeName()); } public void add( ArrayInfo a ) { assert a!=null; final String namespaceURI = a.getTypeName().getNamespaceURI(); Namespace n = getNamespace(namespaceURI); n.arrays.add(a); // search for foreign namespace references n.addDependencyTo(a.getItemType().getTypeName()); } /** * Adds an additional element declaration. * * @param tagName * The name of the element declaration to be added. * @param type * The type this element refers to. * Can be null, in which case the element refers to an empty anonymous complex type. */ public void add( QName tagName, boolean isNillable, NonElement type ) { if(type!=null && type.getType()==navigator.ref(CompositeStructure.class)) return; // this is a special class we introduced for JAX-WS that we *don't* want in the schema Namespace n = getNamespace(tagName.getNamespaceURI()); n.elementDecls.put(tagName.getLocalPart(), n.new ElementWithType(isNillable,type)); // search for foreign namespace references if(type!=null) n.addDependencyTo(type.getTypeName()); } /** * Writes out the episode file. */ public void writeEpisodeFile(XmlSerializer out) { Bindings root = TXW.create(Bindings.class, out); if(namespaces.containsKey("")) // otherwise jaxb binding NS should be the default namespace root._namespace(WellKnownNamespace.JAXB,"jaxb"); root.version("2.1"); // TODO: don't we want to bake in versions? // generate listing per schema for (Map.Entry e : namespaces.entrySet()) { Bindings group = root.bindings(); String prefix; String tns = e.getKey(); if(!tns.equals("")) { group._namespace(tns,"tns"); prefix = "tns:"; } else { prefix = ""; } group.scd("x-schema::"+(tns.equals("")?"":"tns")); group.schemaBindings().map(false); for (ClassInfo ci : e.getValue().classes) { if(ci.getTypeName()==null) continue; // local type if(ci.getTypeName().getNamespaceURI().equals(tns)) { Bindings child = group.bindings(); child.scd('~'+prefix+ci.getTypeName().getLocalPart()); child.klass().ref(ci.getName()); } if(ci.isElement() && ci.getElementName().getNamespaceURI().equals(tns)) { Bindings child = group.bindings(); child.scd(prefix+ci.getElementName().getLocalPart()); child.klass().ref(ci.getName()); } } for (EnumLeafInfo en : e.getValue().enums) { if(en.getTypeName()==null) continue; // local type Bindings child = group.bindings(); child.scd('~'+prefix+en.getTypeName().getLocalPart()); child.klass().ref(navigator.getClassName(en.getClazz())); } group.commit(true); } root.commit(); } /** * Write out the schema documents. */ public void write(SchemaOutputResolver resolver, ErrorListener errorListener) throws IOException { if(resolver==null) throw new IllegalArgumentException(); if(logger.isLoggable(Level.FINE)) { // debug logging to see what's going on. logger.log(Level.FINE,"Wrigin XML Schema for "+toString(),new StackRecorder()); } // make it fool-proof resolver = new FoolProofResolver(resolver); this.errorListener = errorListener; Map schemaLocations = types.getSchemaLocations(); Map out = new HashMap(); Map systemIds = new HashMap(); // we create a Namespace object for the XML Schema namespace // as a side-effect, but we don't want to generate it. namespaces.remove(WellKnownNamespace.XML_SCHEMA); // first create the outputs for all so that we can resolve references among // schema files when we write for( Namespace n : namespaces.values() ) { String schemaLocation = schemaLocations.get(n.uri); if(schemaLocation!=null) { systemIds.put(n,schemaLocation); } else { Result output = resolver.createOutput(n.uri,"schema"+(out.size()+1)+".xsd"); if(output!=null) { // null result means no schema for that namespace out.put(n,output); systemIds.put(n,output.getSystemId()); } } } // then write'em all for( Map.Entry e : out.entrySet() ) { Result result = e.getValue(); e.getKey().writeTo( result, systemIds ); if(result instanceof StreamResult) { OutputStream outputStream = ((StreamResult)result).getOutputStream(); if(outputStream != null) { outputStream.close(); // fix for bugid: 6291301 } else { final Writer writer = ((StreamResult)result).getWriter(); if(writer != null) writer.close(); } } } } /** * Schema components are organized per namespace. */ private class Namespace { final @NotNull String uri; /** * Other {@link Namespace}s that this namespace depends on. */ private final Set depends = new LinkedHashSet(); /** * If this schema refers to components from this schema by itself. */ private boolean selfReference; /** * List of classes in this namespace. */ private final Set> classes = new LinkedHashSet>(); /** * Set of enums in this namespace */ private final Set> enums = new LinkedHashSet>(); /** * Set of arrays in this namespace */ private final Set> arrays = new LinkedHashSet>(); /** * Global attribute declarations keyed by their local names. */ private final MultiMap> attributeDecls = new MultiMap>(null); /** * Global element declarations to be written, keyed by their local names. */ private final MultiMap elementDecls = new MultiMap(new ElementWithType(true,anyType)); private Form attributeFormDefault; private Form elementFormDefault; /** * Does schema in this namespace uses swaRef? If so, we need to generate import * statement. */ private boolean useSwaRef; /** * Import for mime namespace needs to be generated. * See #856 */ private boolean useMimeNs; public Namespace(String uri) { this.uri = uri; assert !XmlSchemaGenerator.this.namespaces.containsKey(uri); XmlSchemaGenerator.this.namespaces.put(uri,this); } /** * Process the given PropertyInfo looking for references to namespaces that * are foreign to the given namespace. Any foreign namespace references * found are added to the given namespaces dependency list and an <import> * is generated for it. * * @param p the PropertyInfo */ private void processForeignNamespaces(PropertyInfo p, int processingDepth) { for (TypeInfo t : p.ref()) { if ((t instanceof ClassInfo) && (processingDepth > 0)) { java.util.List l = ((ClassInfo) t).getProperties(); for (PropertyInfo subp : l) { processForeignNamespaces(subp, --processingDepth); } } if (t instanceof Element) { addDependencyTo(((Element) t).getElementName()); } if (t instanceof NonElement) { addDependencyTo(((NonElement) t).getTypeName()); } } } private void addDependencyTo(@Nullable QName qname) { // even though the Element interface says getElementName() returns non-null, // ClassInfo always implements Element (even if an instance of ClassInfo might not be an Element). // so this check is still necessary if (qname==null) { return; } String nsUri = qname.getNamespaceURI(); if (nsUri.equals(XML_SCHEMA)) { // no need to explicitly refer to XSD namespace return; } if (nsUri.equals(uri)) { selfReference = true; return; } // found a type in a foreign namespace, so make sure we generate an import for it depends.add(getNamespace(nsUri)); } /** * Writes the schema document to the specified result. * * @param systemIds * System IDs of the other schema documents. "" indicates 'implied'. */ private void writeTo(Result result, Map systemIds) throws IOException { try { Schema schema = TXW.create(Schema.class,ResultFactory.createSerializer(result)); // additional namespace declarations to be made. Map xmlNs = types.getXmlNs(uri); for (Map.Entry e : xmlNs.entrySet()) { schema._namespace(e.getValue(),e.getKey()); } if(useSwaRef) schema._namespace(WellKnownNamespace.SWA_URI,"swaRef"); if(useMimeNs) schema._namespace(WellKnownNamespace.XML_MIME_URI,"xmime"); attributeFormDefault = Form.get(types.getAttributeFormDefault(uri)); attributeFormDefault.declare("attributeFormDefault",schema); elementFormDefault = Form.get(types.getElementFormDefault(uri)); // TODO: if elementFormDefault is UNSET, figure out the right default value to use elementFormDefault.declare("elementFormDefault",schema); // declare XML Schema namespace to be xs, but allow the user to override it. // if 'xs' is used for other things, we'll just let TXW assign a random prefix if(!xmlNs.containsValue(WellKnownNamespace.XML_SCHEMA) && !xmlNs.containsKey("xs")) schema._namespace(WellKnownNamespace.XML_SCHEMA,"xs"); schema.version("1.0"); if(uri.length()!=0) schema.targetNamespace(uri); // declare prefixes for them at this level, so that we can avoid redundant // namespace declarations for (Namespace ns : depends) { schema._namespace(ns.uri); } if(selfReference && uri.length()!=0) { // use common 'tns' prefix for the own namespace // if self-reference is needed schema._namespace(uri,"tns"); } schema._pcdata(newline); // refer to other schemas for( Namespace n : depends ) { Import imp = schema._import(); if(n.uri.length()!=0) imp.namespace(n.uri); String refSystemId = systemIds.get(n); if(refSystemId!=null && !refSystemId.equals("")) { // "" means implied. null if the SchemaOutputResolver said "don't generate!" imp.schemaLocation(relativize(refSystemId,result.getSystemId())); } schema._pcdata(newline); } if(useSwaRef) { schema._import().namespace(WellKnownNamespace.SWA_URI).schemaLocation("http://ws-i.org/profiles/basic/1.1/swaref.xsd"); } if(useMimeNs) { schema._import().namespace(WellKnownNamespace.XML_MIME_URI).schemaLocation("http://www.w3.org/2005/05/xmlmime"); } // then write each component for (Map.Entry e : elementDecls.entrySet()) { e.getValue().writeTo(e.getKey(),schema); schema._pcdata(newline); } for (ClassInfo c : classes) { if (c.getTypeName()==null) { // don't generate anything if it's an anonymous type continue; } if(uri.equals(c.getTypeName().getNamespaceURI())) writeClass(c, schema); schema._pcdata(newline); } for (EnumLeafInfo e : enums) { if (e.getTypeName()==null) { // don't generate anything if it's an anonymous type continue; } if(uri.equals(e.getTypeName().getNamespaceURI())) writeEnum(e,schema); schema._pcdata(newline); } for (ArrayInfo a : arrays) { writeArray(a,schema); schema._pcdata(newline); } for (Map.Entry> e : attributeDecls.entrySet()) { TopLevelAttribute a = schema.attribute(); a.name(e.getKey()); if(e.getValue()==null) writeTypeRef(a,stringType,"type"); else writeAttributeTypeRef(e.getValue(),a); schema._pcdata(newline); } // close the schema schema.commit(); } catch( TxwException e ) { logger.log(Level.INFO,e.getMessage(),e); throw new IOException(e.getMessage()); } } /** * Writes a type attribute (if the referenced type is a global type) * or writes out the definition of the anonymous type in place (if the referenced * type is not a global type.) * * Also provides processing for ID/IDREF, MTOM @xmime, and swa:ref * * ComplexTypeHost and SimpleTypeHost don't share an api for creating * and attribute in a type-safe way, so we will compromise for now and * use _attribute(). */ private void writeTypeRef(TypeHost th, NonElementRef typeRef, String refAttName) { // ID / IDREF handling switch(typeRef.getSource().id()) { case ID: th._attribute(refAttName, new QName(WellKnownNamespace.XML_SCHEMA, "ID")); return; case IDREF: th._attribute(refAttName, new QName(WellKnownNamespace.XML_SCHEMA, "IDREF")); return; case NONE: // no ID/IDREF, so continue on and generate the type break; default: throw new IllegalStateException(); } // MTOM handling MimeType mimeType = typeRef.getSource().getExpectedMimeType(); if( mimeType != null ) { th._attribute(new QName(WellKnownNamespace.XML_MIME_URI, "expectedContentTypes", "xmime"), mimeType.toString()); } // ref:swaRef handling if(generateSwaRefAdapter(typeRef)) { th._attribute(refAttName, new QName(WellKnownNamespace.SWA_URI, "swaRef", "ref")); return; } // type name override if(typeRef.getSource().getSchemaType()!=null) { th._attribute(refAttName,typeRef.getSource().getSchemaType()); return; } // normal type generation writeTypeRef(th, typeRef.getTarget(), refAttName); } /** * Writes a type attribute (if the referenced type is a global type) * or writes out the definition of the anonymous type in place (if the referenced * type is not a global type.) * * @param th * the TXW interface to which the attribute will be written. * @param type * type to be referenced. * @param refAttName * The name of the attribute used when referencing a type by QName. */ private void writeTypeRef(TypeHost th, NonElement type, String refAttName) { Element e = null; if (type instanceof MaybeElement) { MaybeElement me = (MaybeElement)type; boolean isElement = me.isElement(); if (isElement) e = me.asElement(); } if (type instanceof Element) { e = (Element)type; } if (type.getTypeName()==null) { if ((e != null) && (e.getElementName() != null)) { th.block(); // so that the caller may write other attributes if(type instanceof ClassInfo) { writeClass( (ClassInfo)type, th ); } else { writeEnum( (EnumLeafInfo)type, (SimpleTypeHost)th); } } else { // anonymous th.block(); // so that the caller may write other attributes if(type instanceof ClassInfo) { if(collisionChecker.push((ClassInfo)type)) { errorListener.warning(new SAXParseException( Messages.ANONYMOUS_TYPE_CYCLE.format(collisionChecker.getCycleString()), null )); } else { writeClass( (ClassInfo)type, th ); } collisionChecker.pop(); } else { writeEnum( (EnumLeafInfo)type, (SimpleTypeHost)th); } } } else { th._attribute(refAttName,type.getTypeName()); } } /** * writes the schema definition for the given array class */ private void writeArray(ArrayInfo a, Schema schema) { ComplexType ct = schema.complexType().name(a.getTypeName().getLocalPart()); ct._final("#all"); LocalElement le = ct.sequence().element().name("item"); le.type(a.getItemType().getTypeName()); le.minOccurs(0).maxOccurs("unbounded"); le.nillable(true); ct.commit(); } /** * writes the schema definition for the specified type-safe enum in the given TypeHost */ private void writeEnum(EnumLeafInfo e, SimpleTypeHost th) { SimpleType st = th.simpleType(); writeName(e,st); SimpleRestrictionModel base = st.restriction(); writeTypeRef(base, e.getBaseType(), "base"); for (EnumConstant c : e.getConstants()) { base.enumeration().value(c.getLexicalValue()); } st.commit(); } /** * Writes the schema definition for the specified class to the schema writer. * * @param c the class info * @param parent the writer of the parent element into which the type will be defined */ private void writeClass(ClassInfo c, TypeHost parent) { // special handling for value properties if (containsValueProp(c)) { if (c.getProperties().size() == 1) { // [RESULT 2 - simpleType if the value prop is the only prop] // // // // ValuePropertyInfo vp = (ValuePropertyInfo)c.getProperties().get(0); SimpleType st = ((SimpleTypeHost)parent).simpleType(); writeName(c, st); if(vp.isCollection()) { writeTypeRef(st.list(),vp.getTarget(),"itemType"); } else { writeTypeRef(st.restriction(),vp.getTarget(),"base"); } return; } else { // [RESULT 1 - complexType with simpleContent] // // // // // // // // // ... // // ... ComplexType ct = ((ComplexTypeHost)parent).complexType(); writeName(c,ct); if(c.isFinal()) ct._final("extension restriction"); SimpleExtension se = ct.simpleContent().extension(); se.block(); // because we might have attribute before value for (PropertyInfo p : c.getProperties()) { switch (p.kind()) { case ATTRIBUTE: handleAttributeProp((AttributePropertyInfo)p,se); break; case VALUE: TODO.checkSpec("what if vp.isCollection() == true?"); ValuePropertyInfo vp = (ValuePropertyInfo) p; se.base(vp.getTarget().getTypeName()); break; case ELEMENT: // error case REFERENCE: // error default: assert false; throw new IllegalStateException(); } } se.commit(); } TODO.schemaGenerator("figure out what to do if bc != null"); TODO.checkSpec("handle sec 8.9.5.2, bullet #4"); // Java types containing value props can only contain properties of type // ValuePropertyinfo and AttributePropertyInfo which have just been handled, // so return. return; } // we didn't fall into the special case for value props, so we // need to initialize the ct. // generate the complexType ComplexType ct = ((ComplexTypeHost)parent).complexType(); writeName(c,ct); if(c.isFinal()) ct._final("extension restriction"); if(c.isAbstract()) ct._abstract(true); // these are where we write content model and attributes AttrDecls contentModel = ct; TypeDefParticle contentModelOwner = ct; // if there is a base class, we need to generate an extension in the schema final ClassInfo bc = c.getBaseClass(); if (bc != null) { if(bc.hasValueProperty()) { // extending complex type with simple content SimpleExtension se = ct.simpleContent().extension(); contentModel = se; contentModelOwner = null; se.base(bc.getTypeName()); } else { ComplexExtension ce = ct.complexContent().extension(); contentModel = ce; contentModelOwner = ce; ce.base(bc.getTypeName()); // TODO: what if the base type is anonymous? } } if(contentModelOwner!=null) { // build the tree that represents the explicit content model from iterate over the properties ArrayList children = new ArrayList(); for (PropertyInfo p : c.getProperties()) { // handling for if(p instanceof ReferencePropertyInfo && ((ReferencePropertyInfo)p).isMixed()) { ct.mixed(true); } Tree t = buildPropertyContentModel(p); if(t!=null) children.add(t); } Tree top = Tree.makeGroup( c.isOrdered() ? GroupKind.SEQUENCE : GroupKind.ALL, children); // write the content model top.write(contentModelOwner); } // then attributes for (PropertyInfo p : c.getProperties()) { if (p instanceof AttributePropertyInfo) { handleAttributeProp((AttributePropertyInfo)p, contentModel); } } if( c.hasAttributeWildcard()) { contentModel.anyAttribute().namespace("##other").processContents("skip"); } ct.commit(); } /** * Writes the name attribute if it's named. */ private void writeName(NonElement c, TypedXmlWriter xw) { QName tn = c.getTypeName(); if(tn!=null) xw._attribute("name",tn.getLocalPart()); // named } private boolean containsValueProp(ClassInfo c) { for (PropertyInfo p : c.getProperties()) { if (p instanceof ValuePropertyInfo) return true; } return false; } /** * Builds content model writer for the specified property. */ private Tree buildPropertyContentModel(PropertyInfo p) { switch(p.kind()) { case ELEMENT: return handleElementProp((ElementPropertyInfo)p); case ATTRIBUTE: // attribuets are handled later return null; case REFERENCE: return handleReferenceProp((ReferencePropertyInfo)p); case MAP: return handleMapProp((MapPropertyInfo)p); case VALUE: // value props handled above in writeClass() assert false; throw new IllegalStateException(); default: assert false; throw new IllegalStateException(); } } /** * Generate the proper schema fragment for the given element property into the * specified schema compositor. * * The element property may or may not represent a collection and it may or may * not be wrapped. * * @param ep the element property */ private Tree handleElementProp(final ElementPropertyInfo ep) { if (ep.isValueList()) { return new Tree.Term() { protected void write(ContentModelContainer parent, boolean isOptional, boolean repeated) { TypeRef t = ep.getTypes().get(0); LocalElement e = parent.element(); e.block(); // we will write occurs later QName tn = t.getTagName(); e.name(tn.getLocalPart()); List lst = e.simpleType().list(); writeTypeRef(lst,t, "itemType"); elementFormDefault.writeForm(e,tn); writeOccurs(e,isOptional||!ep.isRequired(),repeated); } }; } ArrayList children = new ArrayList(); for (final TypeRef t : ep.getTypes()) { children.add(new Tree.Term() { protected void write(ContentModelContainer parent, boolean isOptional, boolean repeated) { LocalElement e = parent.element(); QName tn = t.getTagName(); PropertyInfo propInfo = t.getSource(); TypeInfo parentInfo = (propInfo == null) ? null : propInfo.parent(); if (canBeDirectElementRef(t, tn, parentInfo)) { if ((!t.getTarget().isSimpleType()) && (t.getTarget() instanceof ClassInfo) && collisionChecker.findDuplicate((ClassInfo) t.getTarget())) { e.ref(new QName(uri, tn.getLocalPart())); } else { QName elemName = null; if (t.getTarget() instanceof Element) { Element te = (Element) t.getTarget(); elemName = te.getElementName(); } Collection refs = propInfo.ref(); if ((refs != null) && (!refs.isEmpty()) && (elemName != null)){ ClassInfoImpl cImpl = null; for (TypeInfo ref : refs) { if (ref == null || ref instanceof ClassInfoImpl) { if (elemName.equals(((ClassInfoImpl)ref).getElementName())) { cImpl = (ClassInfoImpl) ref; break; } } } if (cImpl != null) e.ref(new QName(cImpl.getElementName().getNamespaceURI(), tn.getLocalPart())); else e.ref(new QName("", tn.getLocalPart())); } else e.ref(tn); } } else { e.name(tn.getLocalPart()); writeTypeRef(e,t, "type"); elementFormDefault.writeForm(e,tn); } if (t.isNillable()) { e.nillable(true); } if(t.getDefaultValue()!=null) e._default(t.getDefaultValue()); writeOccurs(e,isOptional,repeated); } }); } final Tree choice = Tree.makeGroup(GroupKind.CHOICE, children) .makeOptional(!ep.isRequired()) .makeRepeated(ep.isCollection()); // see Spec table 8-13 final QName ename = ep.getXmlName(); if (ename != null) { // wrapped collection return new Tree.Term() { protected void write(ContentModelContainer parent, boolean isOptional, boolean repeated) { LocalElement e = parent.element(); if(ename.getNamespaceURI().length()>0) { if (!ename.getNamespaceURI().equals(uri)) { // TODO: we need to generate the corresponding element declaration for this // table 8-25: Property/field element wrapper with ref attribute e.ref(new QName(ename.getNamespaceURI(), ename.getLocalPart())); return; } } e.name(ename.getLocalPart()); elementFormDefault.writeForm(e,ename); if(ep.isCollectionNillable()) { e.nillable(true); } writeOccurs(e,!ep.isCollectionRequired(),repeated); ComplexType p = e.complexType(); choice.write(p); } }; } else {// non-wrapped return choice; } } /** * Checks if we can collapse * <element name='foo' type='t' /> to <element ref='foo' />. * * This is possible if we already have such declaration to begin with. */ private boolean canBeDirectElementRef(TypeRef t, QName tn, TypeInfo parentInfo) { Element te = null; ClassInfo ci = null; QName targetTagName = null; if(t.isNillable() || t.getDefaultValue()!=null) { // can't put those attributes on return false; } if (t.getTarget() instanceof Element) { te = (Element) t.getTarget(); targetTagName = te.getElementName(); if (te instanceof ClassInfo) { ci = (ClassInfo)te; } } String nsUri = tn.getNamespaceURI(); if ((!nsUri.equals(uri) && nsUri.length()>0) && (!((parentInfo instanceof ClassInfo) && (((ClassInfo)parentInfo).getTypeName() == null)))) { return true; } if ((ci != null) && ((targetTagName != null) && (te.getScope() == null) && (targetTagName.getNamespaceURI() == null))) { if (targetTagName.equals(tn)) { return true; } } // we have the precise element defined already if (te != null) { // it is instanceof Element return targetTagName!=null && targetTagName.equals(tn); } return false; } /** * Generate an attribute for the specified property on the specified complexType * * @param ap the attribute * @param attr the schema definition to which the attribute will be added */ private void handleAttributeProp(AttributePropertyInfo ap, AttrDecls attr) { // attr is either a top-level ComplexType or a ComplexExtension // // [RESULT] // // // <...>... // // // // or // // // // // <...>... // // // // // // or it could also be an in-lined type (attr ref) // LocalAttribute localAttribute = attr.attribute(); final String attrURI = ap.getXmlName().getNamespaceURI(); if (attrURI.equals("") /*|| attrURI.equals(uri) --- those are generated as global attributes anyway, so use them.*/) { localAttribute.name(ap.getXmlName().getLocalPart()); writeAttributeTypeRef(ap, localAttribute); attributeFormDefault.writeForm(localAttribute,ap.getXmlName()); } else { // generate an attr ref localAttribute.ref(ap.getXmlName()); } if(ap.isRequired()) { // TODO: not type safe localAttribute.use("required"); } } private void writeAttributeTypeRef(AttributePropertyInfo ap, AttributeType a) { if( ap.isCollection() ) writeTypeRef(a.simpleType().list(), ap, "itemType"); else writeTypeRef(a, ap, "type"); } /** * Generate the proper schema fragment for the given reference property into the * specified schema compositor. * * The reference property may or may not refer to a collection and it may or may * not be wrapped. */ private Tree handleReferenceProp(final ReferencePropertyInfo rp) { // fill in content model ArrayList children = new ArrayList(); for (final Element e : rp.getElements()) { children.add(new Tree.Term() { protected void write(ContentModelContainer parent, boolean isOptional, boolean repeated) { LocalElement eref = parent.element(); boolean local=false; QName en = e.getElementName(); if(e.getScope()!=null) { // scoped. needs to be inlined boolean qualified = en.getNamespaceURI().equals(uri); boolean unqualified = en.getNamespaceURI().equals(""); if(qualified || unqualified) { // can be inlined indeed // write form="..." if necessary if(unqualified) { if(elementFormDefault.isEffectivelyQualified) eref.form("unqualified"); } else { if(!elementFormDefault.isEffectivelyQualified) eref.form("qualified"); } local = true; eref.name(en.getLocalPart()); // write out type reference if(e instanceof ClassInfo) { writeTypeRef(eref,(ClassInfo)e,"type"); } else { writeTypeRef(eref,((ElementInfo)e).getContentType(),"type"); } } } if(!local) eref.ref(en); writeOccurs(eref,isOptional,repeated); } }); } final WildcardMode wc = rp.getWildcard(); if( wc != null ) { children.add(new Tree.Term() { protected void write(ContentModelContainer parent, boolean isOptional, boolean repeated) { Any any = parent.any(); final String pcmode = getProcessContentsModeName(wc); if( pcmode != null ) any.processContents(pcmode); any.namespace("##other"); writeOccurs(any,isOptional,repeated); } }); } final Tree choice = Tree.makeGroup(GroupKind.CHOICE, children).makeRepeated(rp.isCollection()).makeOptional(!rp.isRequired()); final QName ename = rp.getXmlName(); if (ename != null) { // wrapped return new Tree.Term() { protected void write(ContentModelContainer parent, boolean isOptional, boolean repeated) { LocalElement e = parent.element().name(ename.getLocalPart()); elementFormDefault.writeForm(e,ename); if(rp.isCollectionNillable()) e.nillable(true); writeOccurs(e,true,repeated); ComplexType p = e.complexType(); choice.write(p); } }; } else { // unwrapped return choice; } } /** * Generate the proper schema fragment for the given map property into the * specified schema compositor. * * @param mp the map property */ private Tree handleMapProp(final MapPropertyInfo mp) { return new Tree.Term() { protected void write(ContentModelContainer parent, boolean isOptional, boolean repeated) { QName ename = mp.getXmlName(); LocalElement e = parent.element(); elementFormDefault.writeForm(e,ename); if(mp.isCollectionNillable()) e.nillable(true); e = e.name(ename.getLocalPart()); writeOccurs(e,isOptional,repeated); ComplexType p = e.complexType(); // TODO: entry, key, and value are always unqualified. that needs to be fixed, too. // TODO: we need to generate the corresponding element declaration, if they are qualified e = p.sequence().element(); e.name("entry").minOccurs(0).maxOccurs("unbounded"); ExplicitGroup seq = e.complexType().sequence(); writeKeyOrValue(seq, "key", mp.getKeyType()); writeKeyOrValue(seq, "value", mp.getValueType()); } }; } private void writeKeyOrValue(ExplicitGroup seq, String tagName, NonElement typeRef) { LocalElement key = seq.element().name(tagName); key.minOccurs(0); writeTypeRef(key, typeRef, "type"); } public void addGlobalAttribute(AttributePropertyInfo ap) { attributeDecls.put( ap.getXmlName().getLocalPart(), ap ); addDependencyTo(ap.getTarget().getTypeName()); } public void addGlobalElement(TypeRef tref) { elementDecls.put( tref.getTagName().getLocalPart(), new ElementWithType(false,tref.getTarget()) ); addDependencyTo(tref.getTarget().getTypeName()); } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("[classes=").append(classes); buf.append(",elementDecls=").append(elementDecls); buf.append(",enums=").append(enums); buf.append("]"); return super.toString(); } /** * Represents a global element declaration to be written. * *

* Because multiple properties can name the same global element even if * they have different Java type, the schema generator first needs to * walk through the model and decide what to generate for the given * element declaration. * *

* This class represents what will be written, and its {@link #equals(Object)} * method is implemented in such a way that two identical declarations * are considered as the same. */ abstract class ElementDeclaration { /** * Returns true if two {@link ElementDeclaration}s are representing * the same schema fragment. */ @Override public abstract boolean equals(Object o); @Override public abstract int hashCode(); /** * Generates the declaration. */ public abstract void writeTo(String localName, Schema schema); } /** * {@link ElementDeclaration} that refers to a {@link NonElement}. */ class ElementWithType extends ElementDeclaration { private final boolean nillable; private final NonElement type; public ElementWithType(boolean nillable,NonElement type) { this.type = type; this.nillable = nillable; } public void writeTo(String localName, Schema schema) { TopLevelElement e = schema.element().name(localName); if(nillable) e.nillable(true); if (type != null) { writeTypeRef(e,type, "type"); } else { e.complexType(); // refer to the nested empty complex type } e.commit(); } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final ElementWithType that = (ElementWithType) o; return type.equals(that.type); } public int hashCode() { return type.hashCode(); } } } /** * Examine the specified element ref and determine if a swaRef attribute needs to be generated * @param typeRef */ private boolean generateSwaRefAdapter(NonElementRef typeRef) { return generateSwaRefAdapter(typeRef.getSource()); } /** * Examine the specified element ref and determine if a swaRef attribute needs to be generated */ private boolean generateSwaRefAdapter(PropertyInfo prop) { final Adapter adapter = prop.getAdapter(); if (adapter == null) return false; final Object o = navigator.asDecl(SwaRefAdapter.class); if (o == null) return false; return (o.equals(adapter.adapterType)); } /** * Debug information of what's in this {@link XmlSchemaGenerator}. */ @Override public String toString() { StringBuilder buf = new StringBuilder(); for (Namespace ns : namespaces.values()) { if(buf.length()>0) buf.append(','); buf.append(ns.uri).append('=').append(ns); } return super.toString()+'['+buf+']'; } /** * return the string representation of the processContents mode of the * give wildcard, or null if it is the schema default "strict" * */ private static String getProcessContentsModeName(WildcardMode wc) { switch(wc) { case LAX: case SKIP: return wc.name().toLowerCase(); case STRICT: return null; default: throw new IllegalStateException(); } } /** * Relativizes a URI by using another URI (base URI.) * *

* For example, {@code relative("http://www.sun.com/abc/def","http://www.sun.com/pqr/stu") => "../abc/def"} * *

* This method only works on hierarchical URI's, not opaque URI's (refer to the * java.net.URI * javadoc for complete definitions of these terms. * *

* This method will not normalize the relative URI. * * @return the relative URI or the original URI if a relative one could not be computed */ protected static String relativize(String uri, String baseUri) { try { assert uri!=null; if(baseUri==null) return uri; URI theUri = new URI(escapeURI(uri)); URI theBaseUri = new URI(escapeURI(baseUri)); if (theUri.isOpaque() || theBaseUri.isOpaque()) return uri; if (!equalsIgnoreCase(theUri.getScheme(), theBaseUri.getScheme()) || !equal(theUri.getAuthority(), theBaseUri.getAuthority())) return uri; String uriPath = theUri.getPath(); String basePath = theBaseUri.getPath(); // normalize base path if (!basePath.endsWith("/")) { basePath = normalizeUriPath(basePath); } if( uriPath.equals(basePath)) return "."; String relPath = calculateRelativePath(uriPath, basePath, fixNull(theUri.getScheme()).equals("file")); if (relPath == null) return uri; // recursion found no commonality in the two uris at all StringBuilder relUri = new StringBuilder(); relUri.append(relPath); if (theUri.getQuery() != null) relUri.append('?').append(theUri.getQuery()); if (theUri.getFragment() != null) relUri.append('#').append(theUri.getFragment()); return relUri.toString(); } catch (URISyntaxException e) { throw new InternalError("Error escaping one of these uris:\n\t"+uri+"\n\t"+baseUri); } } private static String fixNull(String s) { if(s==null) return ""; else return s; } private static String calculateRelativePath(String uri, String base, boolean fileUrl) { // if this is a file URL (very likely), and if this is on a case-insensitive file system, // then treat it accordingly. boolean onWindows = File.pathSeparatorChar==';'; if (base == null) { return null; } if ((fileUrl && onWindows && startsWithIgnoreCase(uri,base)) || uri.startsWith(base)) { return uri.substring(base.length()); } else { return "../" + calculateRelativePath(uri, getParentUriPath(base), fileUrl); } } private static boolean startsWithIgnoreCase(String s, String t) { return s.toUpperCase().startsWith(t.toUpperCase()); } /** * JAX-RPC wants the namespaces to be sorted in the reverse order * so that the empty namespace "" comes to the very end. Don't ask me why. */ private static final Comparator NAMESPACE_COMPARATOR = new Comparator() { public int compare(String lhs, String rhs) { return -lhs.compareTo(rhs); } }; private static final String newline = "\n"; }