blob: 5a4bc14259137d8d59dc77c381e8ad0c07c51737 [file] [log] [blame]
/*
* Copyright (c) 1997, 2021 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 com.sun.tools.xjc.reader.xmlschema;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JJavaName;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.util.JavadocEscapeWriter;
import com.sun.istack.NotNull;
import com.sun.tools.xjc.model.CBuiltinLeafInfo;
import com.sun.tools.xjc.model.CClassInfo;
import com.sun.tools.xjc.model.CClassInfoParent;
import com.sun.tools.xjc.model.CElement;
import com.sun.tools.xjc.model.CElementInfo;
import com.sun.tools.xjc.model.CTypeInfo;
import com.sun.tools.xjc.model.TypeUse;
import com.sun.tools.xjc.model.CClass;
import com.sun.tools.xjc.model.CNonElement;
import com.sun.tools.xjc.reader.Ring;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BIProperty;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BISchemaBinding;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.LocalScoping;
import org.glassfish.jaxb.core.v2.WellKnownNamespace;
import com.sun.xml.xsom.XSComplexType;
import com.sun.xml.xsom.XSComponent;
import com.sun.xml.xsom.XSDeclaration;
import com.sun.xml.xsom.XSElementDecl;
import com.sun.xml.xsom.XSSchema;
import com.sun.xml.xsom.XSSchemaSet;
import com.sun.xml.xsom.XSSimpleType;
import com.sun.xml.xsom.XSType;
import com.sun.xml.xsom.impl.util.SchemaWriter;
import com.sun.xml.xsom.util.ComponentNameFunction;
import org.xml.sax.Locator;
import javax.xml.XMLConstants;
/**
* Manages association between {@link XSComponent}s and generated
* {@link CTypeInfo}s.
*
* <p>
* This class determines which component is mapped to (or is not mapped to)
* what types.
*
* @author
* Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com)
*/
public final class ClassSelector extends BindingComponent {
/** Center of owner classes. */
private final BGMBuilder builder = Ring.get(BGMBuilder.class);
/**
* Map from XSComponents to {@link Binding}s. Keeps track of all
* content interfaces that are already built or being built.
*/
private final Map<XSComponent,Binding> bindMap = new HashMap<>();
/**
* UGLY HACK.
* <p>
* To avoid cyclic dependency between binding elements and types,
* we need additional markers that tell which elements are definitely not bound
* to a class.
* <p>
* the cyclic dependency is as follows:
* elements need to bind its types first, because otherwise it can't
* determine T of JAXBElement<T>.
* OTOH, types need to know whether its parent is bound to a class to decide
* which class name to use.
*/
/*package*/ final Map<XSComponent,CElementInfo> boundElements = new HashMap<>();
/**
* A list of {@link Binding}s object that needs to be built.
*/
private final Stack<Binding> bindQueue = new Stack<>();
/**
* {@link CClassInfo}s that are already {@link Binding#build() built}.
*/
private final Set<CClassInfo> built = new HashSet<>();
/**
* Object that determines components that are mapped
* to classes.
*/
private final ClassBinder classBinder;
/**
* {@link CClassInfoParent}s that determines where a new class
* should be created.
*/
private final Stack<CClassInfoParent> classScopes = new Stack<>();
/**
* The component that is being bound to {@link #currentBean}.
*/
private XSComponent currentRoot;
/**
* The bean representation we are binding right now.
*/
private CClassInfo currentBean;
private final class Binding {
private final XSComponent sc;
private final CTypeInfo bean;
public Binding(XSComponent sc, CTypeInfo bean) {
this.sc = sc;
this.bean = bean;
}
void build() {
if(!(this.bean instanceof CClassInfo))
return; // no need to "build"
CClassInfo bean = (CClassInfo)this.bean;
if(!built.add(bean))
return; // already built
for( String reservedClassName : reservedClassNames ) {
if( bean.getName().equals(reservedClassName) ) {
getErrorReporter().error( sc.getLocator(),
Messages.ERR_RESERVED_CLASS_NAME, reservedClassName );
break;
}
}
// if this schema component is an element declaration
// and it satisfies a set of conditions specified in the spec,
// this class will receive a constructor.
if(needValueConstructor(sc)) {
// TODO: fragile. There is no guarantee that the property name
// is in fact "value".
bean.addConstructor("value");
}
if(bean.javadoc==null)
addSchemaFragmentJavadoc(bean,sc);
// build the body
if(builder.getGlobalBinding().getFlattenClasses()==LocalScoping.NESTED)
pushClassScope(bean);
else
pushClassScope(bean.parent());
XSComponent oldRoot = currentRoot;
CClassInfo oldBean = currentBean;
currentRoot = sc;
currentBean = bean;
sc.visit(Ring.get(BindRed.class));
currentBean = oldBean;
currentRoot = oldRoot;
popClassScope();
// acknowledge property customization on this schema component,
// since it is OK to have a customization at the point of declaration
// even when no one is using it.
BIProperty prop = builder.getBindInfo(sc).get(BIProperty.class);
if(prop!=null) prop.markAsAcknowledged();
}
}
// should be instanciated only from BGMBuilder.
public ClassSelector() {
classBinder = new Abstractifier(new DefaultClassBinder());
Ring.add(ClassBinder.class,classBinder);
classScopes.push(null); // so that the getClassFactory method returns null
XSComplexType anyType = Ring.get(XSSchemaSet.class).getComplexType(XMLConstants.W3C_XML_SCHEMA_NS_URI,"anyType");
bindMap.put(anyType,new Binding(anyType,CBuiltinLeafInfo.ANYTYPE));
}
/** Gets the current class scope. */
public final CClassInfoParent getClassScope() {
assert !classScopes.isEmpty();
return classScopes.peek();
}
public final void pushClassScope( CClassInfoParent clsFctry ) {
assert clsFctry!=null;
classScopes.push(clsFctry);
}
public final void popClassScope() {
classScopes.pop();
}
public XSComponent getCurrentRoot() {
return currentRoot;
}
public CClassInfo getCurrentBean() {
return currentBean;
}
/**
* Checks if the given component is bound to a class.
*/
public final CElement isBound( XSElementDecl x, XSComponent referer ) {
CElementInfo r = boundElements.get(x);
if(r!=null)
return r;
return bindToType(x,referer);
}
/**
* Checks if the given component is being mapped to a type.
* If so, build that type and return that object.
* If it is not being mapped to a type item, return null.
*/
public CTypeInfo bindToType( XSComponent sc, XSComponent referer ) {
return _bindToClass(sc,referer,false);
}
//
// some schema components are guaranteed to map to a particular CTypeInfo.
// the following versions capture those constraints in the signature
// and making the bindToType invocation more type safe.
//
public CElement bindToType( XSElementDecl e, XSComponent referer ) {
return (CElement)_bindToClass(e,referer,false);
}
public CClass bindToType( XSComplexType t, XSComponent referer, boolean cannotBeDelayed ) {
// this assumption that a complex type always binds to a ClassInfo
// does not hold for xs:anyType --- our current approach of handling
// this idiosynchracy is to make sure that xs:anyType doesn't use
// this codepath.
return (CClass)_bindToClass(t,referer,cannotBeDelayed);
}
public TypeUse bindToType( XSType t, XSComponent referer ) {
if(t instanceof XSSimpleType) {
return Ring.get(SimpleTypeBuilder.class).build((XSSimpleType)t);
} else
return (CNonElement)_bindToClass(t,referer,false);
}
/**
* The real meat of the "bindToType" code.
*
* @param cannotBeDelayed
* if the binding of the body of the class cannot be defered
* and needs to be done immediately. If the flag is false,
* the binding of the body will be done later, to avoid
* cyclic binding problem.
* @param referer
* The component that refers to {@code sc}. This can be null,
* if figuring out the referer is too hard, in which case
* the error message might be less user friendly.
*/
// TODO: consider getting rid of "cannotBeDelayed"
CTypeInfo _bindToClass( @NotNull XSComponent sc, XSComponent referer, boolean cannotBeDelayed ) {
// check if this class is already built.
if(!bindMap.containsKey(sc)) {
// craete a bind task
// if this is a global declaration, make sure they will be generated
// under a package.
boolean isGlobal = false;
if( sc instanceof XSDeclaration ) {
isGlobal = ((XSDeclaration)sc).isGlobal();
if( isGlobal )
pushClassScope( new CClassInfoParent.Package(
getPackage(((XSDeclaration)sc).getTargetNamespace())) );
}
// otherwise check if this component should become a class.
CElement bean = sc.apply(classBinder);
if( isGlobal )
popClassScope();
if(bean==null)
return null;
// can this namespace generate a class?
if (bean instanceof CClassInfo) {
XSSchema os = sc.getOwnerSchema();
BISchemaBinding sb = builder.getBindInfo(os).get(BISchemaBinding.class);
if(sb!=null && !sb.map) {
// nope
getErrorReporter().error(sc.getLocator(),
Messages.ERR_REFERENCE_TO_NONEXPORTED_CLASS, sc.apply( new ComponentNameFunction() ) );
getErrorReporter().error(sb.getLocation(),
Messages.ERR_REFERENCE_TO_NONEXPORTED_CLASS_MAP_FALSE, os.getTargetNamespace() );
if(referer!=null)
getErrorReporter().error(referer.getLocator(),
Messages.ERR_REFERENCE_TO_NONEXPORTED_CLASS_REFERER, referer.apply( new ComponentNameFunction() ) );
}
}
queueBuild( sc, bean );
}
Binding bind = bindMap.get(sc);
if( cannotBeDelayed )
bind.build();
return bind.bean;
}
/**
* Runs all the pending build tasks.
*/
public void executeTasks() {
while( bindQueue.size()!=0 )
bindQueue.pop().build();
}
/**
* Determines if the given component needs to have a value
* constructor (a constructor that takes a parmater.) on ObjectFactory.
*/
private boolean needValueConstructor( XSComponent sc ) {
if(!(sc instanceof XSElementDecl)) return false;
XSElementDecl decl = (XSElementDecl)sc;
if(!decl.getType().isSimpleType()) return false;
return true;
}
private static final String[] reservedClassNames = new String[]{"ObjectFactory"};
public void queueBuild( XSComponent sc, CElement bean ) {
// it is an error if the same component is built twice,
// or the association is modified.
Binding b = new Binding(sc,bean);
bindQueue.push(b);
Binding old = bindMap.put(sc, b);
assert old==null || old.bean==bean;
}
/**
* Copies a schema fragment into the javadoc of the generated class.
*/
private void addSchemaFragmentJavadoc( CClassInfo bean, XSComponent sc ) {
// first, pick it up from <documentation> if any.
String doc = builder.getBindInfo(sc).getDocumentation();
if(doc!=null)
append(bean, doc);
// then the description of where this component came from
Locator loc = sc.getLocator();
String fileName = null;
if(loc!=null) {
fileName = loc.getPublicId();
if(fileName==null)
fileName = loc.getSystemId();
}
if(fileName==null) fileName="";
String lineNumber=Messages.format( Messages.JAVADOC_LINE_UNKNOWN);
if(loc!=null && loc.getLineNumber()!=-1)
lineNumber = String.valueOf(loc.getLineNumber());
String componentName = sc.apply( new ComponentNameFunction() );
String jdoc = Messages.format( Messages.JAVADOC_HEADING, componentName, fileName, lineNumber );
append(bean,jdoc);
// then schema fragment
StringWriter out = new StringWriter();
out.write("<pre>\n");
SchemaWriter sw = new SchemaWriter(new JavadocEscapeWriter(out));
sc.visit(sw);
out.write("</pre>");
append(bean,out.toString());
}
private void append(CClassInfo bean, String doc) {
if(bean.javadoc==null)
bean.javadoc = doc+'\n';
else
bean.javadoc += '\n'+doc+'\n';
}
/**
* Set of package names that are tested (set of {@code String}s.)
*
* This set is used to avoid duplicating "incorrect package name"
* errors.
*/
private static Set<String> checkedPackageNames = new HashSet<>();
/**
* Gets the Java package to which classes from
* this namespace should go.
*
* <p>
* Usually, the getOuterClass method should be used
* to determine where to put a class.
*/
public JPackage getPackage(String targetNamespace) {
XSSchema s = Ring.get(XSSchemaSet.class).getSchema(targetNamespace);
BISchemaBinding sb =
builder.getBindInfo(s).get(BISchemaBinding.class);
if(sb!=null) sb.markAsAcknowledged();
String name = null;
// "-p" takes precedence over everything else
if( builder.defaultPackage1 != null )
name = builder.defaultPackage1;
// use the <jaxb:package> customization
if( name == null && sb!=null && sb.getPackageName()!=null )
name = sb.getPackageName();
// the JAX-RPC option goes below the <jaxb:package>
if( name == null && builder.defaultPackage2 != null )
name = builder.defaultPackage2;
// generate the package name from the targetNamespace
if( name == null )
name = builder.getNameConverter().toPackageName( targetNamespace );
// hardcode a package name because the code doesn't compile
// if it generated into the default java package
if( name == null )
name = "generated"; // the last resort
// check if the package name is a valid name.
if( checkedPackageNames.add(name) ) {
// this is the first time we hear about this package name.
if( !JJavaName.isJavaPackageName(name) )
// TODO: s.getLocator() is not very helpful.
// ideally, we'd like to use the locator where this package name
// comes from.
getErrorReporter().error(s.getLocator(),
Messages.ERR_INCORRECT_PACKAGE_NAME, targetNamespace, name );
}
return Ring.get(JCodeModel.class)._package(name);
}
}