org.glassfish.jaxb.runtime.v2.schemagen.XmlSchemaGenerator Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0, which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.glassfish.jaxb.runtime.v2.schemagen;
import com.sun.istack.NotNull;
import com.sun.istack.Nullable;
import org.glassfish.jaxb.core.Utils;
import org.glassfish.jaxb.core.api.ErrorListener;
import org.glassfish.jaxb.runtime.api.CompositeStructure;
import org.glassfish.jaxb.core.v2.TODO;
import org.glassfish.jaxb.core.v2.WellKnownNamespace;
import org.glassfish.jaxb.core.v2.model.core.*;
import org.glassfish.jaxb.runtime.v2.model.impl.ClassInfoImpl;
import org.glassfish.jaxb.core.v2.model.core.Element;
import org.glassfish.jaxb.core.v2.model.nav.Navigator;
import org.glassfish.jaxb.runtime.v2.runtime.SwaRefAdapter;
import org.glassfish.jaxb.core.v2.schemagen.episode.Bindings;
import org.glassfish.jaxb.runtime.v2.util.CollisionCheckStack;
import org.glassfish.jaxb.runtime.v2.util.StackRecorder;
import com.sun.xml.txw2.TXW;
import com.sun.xml.txw2.TxwException;
import com.sun.xml.txw2.TypedXmlWriter;
import com.sun.xml.txw2.output.ResultFactory;
import com.sun.xml.txw2.output.XmlSerializer;
import jakarta.activation.MimeType;
import jakarta.xml.bind.SchemaOutputResolver;
import jakarta.xml.bind.annotation.XmlElement;
import org.glassfish.jaxb.runtime.v2.schemagen.xmlschema.*;
import org.xml.sax.SAXParseException;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.glassfish.jaxb.runtime.v2.schemagen.Util.*;
/**
* Generates a set of W3C XML Schema documents from a set of Java classes.
*
*
* A client must invoke methods in the following order:
*
* - Create a new {@link XmlSchemaGenerator}
*
- Invoke {@link #add} methods, multiple times if necessary.
*
- Invoke {@link #write}
*
- Discard the {@link XmlSchemaGenerator}.
*
*
* @author Ryan Shoemaker
* @author Kohsuke Kawaguchi ([email protected])
*/
public final class XmlSchemaGenerator {
private static final Logger logger = Utils.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,"Writing XML Schema for "+ this, 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(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// 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());
}
}
//Clear the namespace specific set with already written classes
n.resetWritten();
}
// 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;
/**
* Container for already processed classes
*/
private final Set written = new HashSet<>();
public Namespace(String uri) {
this.uri = uri;
assert !XmlSchemaGenerator.this.namespaces.containsKey(uri);
XmlSchemaGenerator.this.namespaces.put(uri,this);
}
/**
* Clear out the set of already processed classes for this namespace
*/
void resetWritten() {
written.clear();
}
/**
* 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 {@code }
* 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 (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(nsUri)) {
// 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(XMLConstants.W3C_XML_SCHEMA_NS_URI)
&& !xmlNs.containsKey("xs"))
schema._namespace(XMLConstants.W3C_XML_SCHEMA_NS_URI,"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("https://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(XMLConstants.W3C_XML_SCHEMA_NS_URI, "ID"));
return;
case IDREF:
th._attribute(refAttName, new QName(XMLConstants.W3C_XML_SCHEMA_NS_URI, "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) {
if (written.contains(c)) { // to avoid cycles let's check if we haven't already processed the class
return;
}
written.add(c);
// 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() {
@Override
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());
org.glassfish.jaxb.runtime.v2.schemagen.xmlschema.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() {
@Override
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) {
if (tn.getNamespaceURI() != null && tn.getNamespaceURI().trim().length() != 0) {
e.ref(new QName(tn.getNamespaceURI(), tn.getLocalPart()));
} else {
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() {
@Override
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
* {@code }
* to {@code }.
*
* 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() {
@Override
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() {
@Override
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() {
@Override
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() {
@Override
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 buf.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 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;
}
@Override
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();
}
@Override
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);
}
@Override
public int hashCode() {
return type.hashCode();
}
}
}
/**
* Examine the specified element ref and determine if a swaRef attribute needs to be generated
*/
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 .
*/
@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<>() {
@Override
public int compare(String lhs, String rhs) {
return -lhs.compareTo(rhs);
}
};
private static final String newline = "\n";
}