/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.cxf.tools.wsdlto.databinding.jaxb;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.XMLConstants;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.util.StreamReaderDelegate;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;


import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.XMLFilterImpl;

import com.sun.codemodel.ClassType;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JType;
import com.sun.tools.xjc.BadCommandLineException;
import com.sun.tools.xjc.ErrorReceiver;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.api.Mapping;
import com.sun.tools.xjc.api.Property;
import com.sun.tools.xjc.api.S2JJAXBModel;
import com.sun.tools.xjc.api.SchemaCompiler;
import com.sun.tools.xjc.api.TypeAndAnnotation;
import com.sun.tools.xjc.api.XJC;
import com.sun.tools.xjc.reader.internalizer.AbstractReferenceFinderImpl;
import com.sun.tools.xjc.reader.internalizer.DOMForest;
import com.sun.tools.xjc.reader.xmlschema.parser.LSInputSAXWrapper;
import com.sun.tools.xjc.reader.xmlschema.parser.XMLSchemaInternalizationLogic;

import org.apache.cxf.Bus;
import org.apache.cxf.catalog.OASISCatalogManager;
import org.apache.cxf.catalog.OASISCatalogManagerHelper;
import org.apache.cxf.common.WSDLConstants;
import org.apache.cxf.common.i18n.Message;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.common.xmlschema.SchemaCollection;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.helpers.FileUtils;
import org.apache.cxf.helpers.XMLUtils;
import org.apache.cxf.resource.URIResolver;
import org.apache.cxf.service.model.SchemaInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.staxutils.StaxUtils;
import org.apache.cxf.staxutils.W3CNamespaceContext;
import org.apache.cxf.tools.common.ToolConstants;
import org.apache.cxf.tools.common.ToolContext;
import org.apache.cxf.tools.common.ToolException;
import org.apache.cxf.tools.common.model.DefaultValueWriter;
import org.apache.cxf.tools.util.ClassCollector;
import org.apache.cxf.tools.util.JAXBUtils;
import org.apache.cxf.tools.util.URIParserUtil;
import org.apache.cxf.tools.wsdlto.core.DataBindingProfile;
import org.apache.cxf.tools.wsdlto.core.DefaultValueProvider;
import org.apache.cxf.tools.wsdlto.core.RandomValueProvider;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaSerializer;
import org.apache.ws.commons.schema.XmlSchemaSerializer.XmlSchemaSerializerException;

public class JAXBDataBinding implements DataBindingProfile {


    public class LocationFilterReader extends StreamReaderDelegate implements XMLStreamReader {
        boolean isImport;
        boolean isInclude;
        int locIdx = -1;
        OASISCatalogManager catalog;

        LocationFilterReader(XMLStreamReader read, OASISCatalogManager catalog) {
            super(read);
            this.catalog = catalog;
        }

        public int next() throws XMLStreamException {
            int i = super.next();
            if (i == XMLStreamReader.START_ELEMENT) {
                QName qn = super.getName();
                isInclude = qn.equals(WSDLConstants.QNAME_SCHEMA_INCLUDE);
                isImport = qn.equals(WSDLConstants.QNAME_SCHEMA_IMPORT);
                if (isImport) {
                    findLocation();
                } else {
                    locIdx = -1;
                }
            } else {
                isImport = false;
                locIdx = -1;
            }
            return i;
        }

        public int nextTag() throws XMLStreamException {
            int i = super.nextTag();
            if (i == XMLStreamReader.START_ELEMENT) {
                QName qn = super.getName();
                isInclude = qn.equals(WSDLConstants.QNAME_SCHEMA_INCLUDE);
                isImport = qn.equals(WSDLConstants.QNAME_SCHEMA_IMPORT);
                if (isImport) {
                    findLocation();
                } else {
                    locIdx = -1;
                }
            } else {
                isImport = false;
                locIdx = -1;
            }
            return i;
        }
        private void findLocation() {
            locIdx = -1;
            for (int x = super.getAttributeCount(); x > 0; --x) {
                String nm = super.getAttributeLocalName(x - 1);
                if ("schemaLocation".equals(nm)) {
                    locIdx = x - 1;
                }
            }
        }
        public int getAttributeCount() {
            int i = super.getAttributeCount();
            if (locIdx != -1) {
                --i;
            }
            return i;
        }
        private int mapIdx(int index) {
            if (locIdx != -1
                && index >= locIdx) {
                ++index;
            }
            return index;
        }

        private String mapSchemaLocation(String target) {
            return JAXBDataBinding.mapSchemaLocation(target, this.getLocation().getSystemId(), catalog);
        }

        public String getAttributeValue(String namespaceURI, String localName) {
            if (isInclude && "schemaLocation".equals(localName)) {
                return mapSchemaLocation(super.getAttributeValue(namespaceURI, localName));
            }
            return super.getAttributeValue(namespaceURI, localName);
        }
        public String getAttributeValue(int index) {
            if (isInclude) {
                String n = getAttributeLocalName(index);
                if ("schemaLocation".equals(n)) {
                    return mapSchemaLocation(super.getAttributeValue(index));
                }
            }
            return super.getAttributeValue(mapIdx(index));
        }

        public QName getAttributeName(int index) {
            return super.getAttributeName(mapIdx(index));
        }

        public String getAttributePrefix(int index) {
            return super.getAttributePrefix(mapIdx(index));
        }

        public String getAttributeNamespace(int index) {
            return super.getAttributeNamespace(mapIdx(index));
        }

        public String getAttributeLocalName(int index) {
            return super.getAttributeLocalName(mapIdx(index));
        }

        public String getAttributeType(int index) {
            return super.getAttributeType(mapIdx(index));
        }


        public boolean isAttributeSpecified(int index) {
            return super.isAttributeSpecified(mapIdx(index));
        }
    }


    private static final Logger LOG = LogUtils.getL7dLogger(JAXBDataBinding.class);

    private static final Set<String> DEFAULT_TYPE_MAP = new HashSet<String>();
    private static final Map<String, String> JLDEFAULT_TYPE_MAP = new HashMap<String, String>();

    private S2JJAXBModel rawJaxbModelGenCode;
    private ToolContext context;
    private DefaultValueProvider defaultValues;
    private boolean initialized;

    static {
        DEFAULT_TYPE_MAP.add("boolean");
        DEFAULT_TYPE_MAP.add("int");
        DEFAULT_TYPE_MAP.add("long");
        DEFAULT_TYPE_MAP.add("short");
        DEFAULT_TYPE_MAP.add("byte");
        DEFAULT_TYPE_MAP.add("float");
        DEFAULT_TYPE_MAP.add("double");
        DEFAULT_TYPE_MAP.add("char");
        DEFAULT_TYPE_MAP.add("java.lang.String");
        DEFAULT_TYPE_MAP.add("javax.xml.namespace.QName");
        DEFAULT_TYPE_MAP.add("java.net.URI");
        DEFAULT_TYPE_MAP.add("java.math.BigInteger");
        DEFAULT_TYPE_MAP.add("java.math.BigDecimal");
        DEFAULT_TYPE_MAP.add("javax.xml.datatype.XMLGregorianCalendar");
        DEFAULT_TYPE_MAP.add("javax.xml.datatype.Duration");

        JLDEFAULT_TYPE_MAP.put("java.lang.Character", "char");
        JLDEFAULT_TYPE_MAP.put("java.lang.Boolean", "boolean");
        JLDEFAULT_TYPE_MAP.put("java.lang.Integer", "int");
        JLDEFAULT_TYPE_MAP.put("java.lang.Long", "long");
        JLDEFAULT_TYPE_MAP.put("java.lang.Short", "short");
        JLDEFAULT_TYPE_MAP.put("java.lang.Byte", "byte");
        JLDEFAULT_TYPE_MAP.put("java.lang.Float", "float");
        JLDEFAULT_TYPE_MAP.put("java.lang.Double", "double");
        DEFAULT_TYPE_MAP.addAll(JLDEFAULT_TYPE_MAP.keySet());
    }

    public void initialize(ToolContext c) throws ToolException {
        this.context = c;

        SchemaCompiler schemaCompiler = XJC.createSchemaCompiler();
        Bus bus = context.get(Bus.class);
        OASISCatalogManager catalog = bus.getExtension(OASISCatalogManager.class);
        hackInNewInternalizationLogic(schemaCompiler, catalog);

        ClassCollector classCollector = context.get(ClassCollector.class);

        ClassNameAllocatorImpl allocator
            = new ClassNameAllocatorImpl(classCollector,
                                         c.optionSet(ToolConstants.CFG_AUTORESOLVE));

        schemaCompiler.setClassNameAllocator(allocator);

        JAXBBindErrorListener listener = new JAXBBindErrorListener(context.isVerbose());
        schemaCompiler.setErrorListener(listener);
        // Collection<SchemaInfo> schemas = serviceInfo.getSchemas();
        List<InputSource> jaxbBindings = context.getJaxbBindingFile();
        SchemaCollection schemas = (SchemaCollection) context.get(ToolConstants.XML_SCHEMA_COLLECTION);

        Options opts = null;
        opts = getOptions(schemaCompiler);

        List<String> args = new ArrayList<String>();

        if (context.get(ToolConstants.CFG_NO_ADDRESS_BINDING) == null) {
            //hard code to enable jaxb extensions
            args.add("-extension");
            String name = "W3CEPRJaxbBinding.xml";
            if (isJAXB22()) {
                name = "W3CEPRJaxbBinding_jaxb22.xml";
            }
            URL bindingFileUrl = getClass().getResource(name);
            InputSource ins = new InputSource(bindingFileUrl.toString());
            opts.addBindFile(ins);
        }

        if (context.get(ToolConstants.CFG_XJC_ARGS) != null) {
            Object o = context.get(ToolConstants.CFG_XJC_ARGS);
            if (o instanceof String) {
                o = new String[] {(String)o};
            }
            String[] xjcArgss = (String[])o;
            for (String xjcArgs : xjcArgss) {
                StringTokenizer tokenizer = new StringTokenizer(xjcArgs, ",", false);
                while (tokenizer.hasMoreTokens()) {
                    String arg = tokenizer.nextToken();
                    args.add(arg);
                    LOG.log(Level.FINE, "xjc arg:" + arg);
                }
            }
        }

        if (context.get(ToolConstants.CFG_NO_ADDRESS_BINDING) == null
            || context.get(ToolConstants.CFG_XJC_ARGS) != null) {
            try {
                // keep parseArguments happy, supply dummy required command-line
                // opts
                opts.addGrammar(new InputSource("null"));
                opts.parseArguments(args.toArray(new String[] {}));
            } catch (BadCommandLineException e) {
                String msg = "XJC reported 'BadCommandLineException' for -xjc argument:";
                for (String arg : args) {
                    msg = msg + arg + " ";
                }
                LOG.log(Level.FINE, msg, e);
                if (opts != null) {
                    String pluginUsage = getPluginUsageString(opts);
                    msg = msg + System.getProperty("line.separator");
                    if (args.contains("-X")) {
                        msg = pluginUsage;
                    } else {
                        msg += pluginUsage;
                    }
                }

                throw new ToolException(msg, e);
            }
        }
        
        if (context.optionSet(ToolConstants.CFG_MARK_GENERATED)) {
            // Add the @Generated annotation in the Java files generated. This is done by passing
            // '-mark-generated' attribute to jaxb xjc.
            try {
                opts.parseArgument(new String[] {"-mark-generated" }, 0);
            } catch (BadCommandLineException e) {
                LOG.log(Level.SEVERE, e.getMessage());
                throw new ToolException(e);
            }
        }
        addSchemas(opts, schemaCompiler, schemas);
        addBindingFiles(opts, jaxbBindings, schemas);


        for (String ns : context.getNamespacePackageMap().keySet()) {
            File file = JAXBUtils.getPackageMappingSchemaBindingFile(ns, context.mapPackageName(ns));
            try {
                InputSource ins = new InputSource(file.toURI().toString());
                schemaCompiler.parseSchema(ins);
            } finally {
                FileUtils.delete(file);
            }
        }

        if (context.getPackageName() != null) {
            schemaCompiler.setDefaultPackageName(context.getPackageName());
        }

        
        rawJaxbModelGenCode = schemaCompiler.bind();

        addedEnumClassToCollector(schemas, allocator);

        if (context.get(ToolConstants.CFG_DEFAULT_VALUES) != null) {
            String cname = (String)context.get(ToolConstants.CFG_DEFAULT_VALUES);
            if (StringUtils.isEmpty(cname)) {
                defaultValues = new RandomValueProvider();
            } else {
                if (cname.charAt(0) == '=') {
                    cname = cname.substring(1);
                }
                try {
                    defaultValues = (DefaultValueProvider)Class.forName(cname).newInstance();
                } catch (Exception e) {
                    LOG.log(Level.SEVERE, e.getMessage());
                    throw new ToolException(e);
                }
            }
        }
        initialized = true;
    }

    private boolean isJAXB22() {
        Target t = XmlElement.class.getAnnotation(Target.class);
        //JAXB 2.2 allows XmlElement on params.
        for (ElementType et : t.value()) {
            if (et == ElementType.PARAMETER) {
                return true;
            }
        }
        return false;
    }

    private static final class ReferenceFinder extends AbstractReferenceFinderImpl {
        private Locator locator;
        private OASISCatalogManager catalog;

        ReferenceFinder(DOMForest parent, OASISCatalogManager cat) {
            super(parent);
            catalog = cat;
        }

        public void setDocumentLocator(Locator loc) {
            super.setDocumentLocator(loc);
            this.locator = loc;
        }
        protected String findExternalResource(String nsURI, String localName, Attributes atts) {
            if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(nsURI)
                && ("import".equals(localName)
                    || "include".equals(localName))) {
                String s = atts.getValue("schemaLocation");
                if (!StringUtils.isEmpty(s)) {
                    s = JAXBDataBinding.mapSchemaLocation(s, locator.getSystemId(), catalog);
                }
                return s;
            }
            return null;
        }
    }
    private void hackInNewInternalizationLogic(SchemaCompiler schemaCompiler,
                                               final OASISCatalogManager catalog) {
        try {
            Field f = schemaCompiler.getClass().getDeclaredField("forest");
            f.setAccessible(true);
            DOMForest forest = new DOMForest(new XMLSchemaInternalizationLogic() {
                public XMLFilterImpl createExternalReferenceFinder(DOMForest parent) {
                    return new ReferenceFinder(parent, catalog);
                }

            });
            forest.setErrorHandler((ErrorReceiver)schemaCompiler);
            f.set(schemaCompiler, forest);
        } catch (Throwable ex)  {
            //ignore
        }
    }
    private void addBindingFiles(Options opts, List<InputSource> jaxbBindings, SchemaCollection schemas) {
        for (InputSource binding : jaxbBindings) {
            XMLStreamReader r = StaxUtils.createXMLStreamReader(binding);
            try {
                StaxUtils.toNextTag(r);
                String s = r.getAttributeValue(null, "schemaLocation");
                if (StringUtils.isEmpty(s)) {
                    Document d = StaxUtils.read(r);
                    XPath p = XPathFactory.newInstance().newXPath();
                    p.setNamespaceContext(new W3CNamespaceContext(d.getDocumentElement()));
                    XPathExpression xpe = p.compile(d.getDocumentElement().getAttribute("node"));
                    for (XmlSchema schema : schemas.getXmlSchemas()) {
                        if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(schema.getTargetNamespace())) {
                            continue;
                        }
                        Object src = getSchemaNode(schema, schemas);
                        NodeList nodes = (NodeList)xpe.evaluate(src, XPathConstants.NODESET);
                        if (nodes.getLength() > 0) {
                            String key = schema.getSourceURI();
                            binding = convertToTmpInputSource(d.getDocumentElement(), key);
                            opts.addBindFile(binding);
                            binding = null;
                        }
                    }
                }
            } catch (Exception ex) {
                //ignore, just pass to jaxb
            } finally {
                try {
                    r.close();
                } catch (Exception ex) {
                    //ignore
                }
            }
            if (binding != null) {
                opts.addBindFile(binding);
            }
        }
    }
    private Object getSchemaNode(XmlSchema schema, SchemaCollection schemaCollection) {
        XmlSchemaSerializer xser = new XmlSchemaSerializer();
        xser.setExtReg(schemaCollection.getExtReg());
        Document[] docs;
        try {
            docs = xser.serializeSchema(schema, false);
        } catch (XmlSchemaSerializerException e) {
            throw new RuntimeException(e);
        }
        return docs[0].getDocumentElement();
    }
    private InputSource convertToTmpInputSource(Element ele, String schemaLoc) throws Exception {
        InputSource result = null;
        ele.setAttribute("schemaLocation", schemaLoc);
        File tmpFile = FileUtils.createTempFile("jaxbbinding", ".xml");
        XMLUtils.writeTo(ele, new FileOutputStream(tmpFile));
        result = new InputSource(URIParserUtil.getAbsoluteURI(tmpFile.getAbsolutePath()));
        tmpFile.deleteOnExit();
        return result;
    }

    @SuppressWarnings("unchecked")
    private void addSchemas(Options opts,
                            SchemaCompiler schemaCompiler,
                            SchemaCollection schemaCollection) {

        Set<String> ids = new HashSet<String>();
        List<ServiceInfo> serviceList = (List<ServiceInfo>)context.get(ToolConstants.SERVICE_LIST);
        for (ServiceInfo si : serviceList) {
            for (SchemaInfo sci : si.getSchemas()) {
                String key = sci.getSystemId();
                if (ids.contains(key)) {
                    continue;
                }
                ids.add(key);
            }
        }
        Bus bus = context.get(Bus.class);
        OASISCatalogManager catalog = bus.getExtension(OASISCatalogManager.class);
        for (XmlSchema schema : schemaCollection.getXmlSchemas()) {
            if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(schema.getTargetNamespace())) {
                continue;
            }
            String key = schema.getSourceURI();
            if (ids.contains(key)) {
                continue;
            }
            if (!key.startsWith("file:") && !key.startsWith("jar:")) {
                XmlSchemaSerializer xser = new XmlSchemaSerializer();
                xser.setExtReg(schemaCollection.getExtReg());
                Document[] docs;
                try {
                    docs = xser.serializeSchema(schema, false);
                } catch (XmlSchemaSerializerException e) {
                    throw new RuntimeException(e);
                }
                Element ele = docs[0].getDocumentElement();
                ele = removeImportElement(ele, key, catalog);
                if (context.get(ToolConstants.CFG_VALIDATE_WSDL) != null) {
                    String uri = null;
                    try {
                        uri = docs[0].getDocumentURI();
                    } catch (Throwable ex) {
                        //ignore - DOM level 3
                    }
                    validateSchema(ele, uri, catalog);
                }
                try {
                    docs[0].setDocumentURI(key);
                } catch (Throwable t) {
                    //ignore - DOM level 3
                }
                InputSource is = new InputSource((InputStream)null);
                //key = key.replaceFirst("#types[0-9]+$", "");
                is.setSystemId(key);
                is.setPublicId(key);
                opts.addGrammar(is);
                try {
                    schemaCompiler.parseSchema(key, StaxUtils.createXMLStreamReader(ele, key));
                } catch (XMLStreamException e) {
                    throw new ToolException(e);
                }
            }
        }
        for (XmlSchema schema : schemaCollection.getXmlSchemas()) {
            if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(schema.getTargetNamespace())) {
                continue;
            }
            String key = schema.getSourceURI();
            if (ids.contains(key)) {
                continue;
            }
            if (key.startsWith("file:") || key.startsWith("jar:")) {
                InputStream in = null;
                try {
                    if (key.startsWith("file:")) {
                        in = new FileInputStream(new File(new URI(key)));
                    } else {
                        in = new URL(key).openStream();
                    }

                    XMLStreamReader reader = StaxUtils.createXMLStreamReader(key, in);
                    reader = new LocationFilterReader(reader, catalog);
                    InputSource is = new InputSource(key);
                    opts.addGrammar(is);
                    schemaCompiler.parseSchema(key, reader);
                    reader.close();
                } catch (RuntimeException ex) {
                    throw ex;
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException e) {
                            //ignore
                        }
                    }
                }
            }
        }
        ids.clear();
        for (ServiceInfo si : serviceList) {
            for (SchemaInfo sci : si.getSchemas()) {
                String key = sci.getSystemId();
                if (ids.contains(key)) {
                    continue;
                }
                ids.add(key);
                Element ele = sci.getElement();
                ele = removeImportElement(ele, key, catalog);
                if (context.get(ToolConstants.CFG_VALIDATE_WSDL) != null) {
                    validateSchema(ele, sci.getSystemId(), catalog);
                }
                InputSource is = new InputSource((InputStream)null);
                //key = key.replaceFirst("#types[0-9]+$", "");
                is.setSystemId(key);
                is.setPublicId(key);
                opts.addGrammar(is);
                try {
                    schemaCompiler.parseSchema(key, StaxUtils.createXMLStreamReader(ele, key));
                } catch (XMLStreamException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
    private String getPluginUsageString(Options opts) {
        StringBuilder buf = new StringBuilder();
        buf.append("\nAvailable plugin options:\n");
        for (Plugin pl : opts.getAllPlugins()) {
            buf.append(pl.getUsage());
            buf.append('\n');
        }
        return buf.toString();
    }

    // JAXB 'deprecates' getOptions, by which they mean that they reserve the right to change it.
    @SuppressWarnings("deprecation")
    private Options getOptions(SchemaCompiler schemaCompiler) throws ToolException {
        return schemaCompiler.getOptions();
    }

    // JAXB bug. JAXB ClassNameCollector may not be invoked when generated
    // class is an enum. We need to use this method to add the missed file
    // to classCollector.
    private void addedEnumClassToCollector(SchemaCollection schemaCollection,
                                           ClassNameAllocatorImpl allocator) {
        //for (Element schemaElement : schemaList.values()) {
        for (XmlSchema schema : schemaCollection.getXmlSchemas()) {
            String targetNamespace = schema.getTargetNamespace();
            if (StringUtils.isEmpty(targetNamespace)) {
                continue;
            }
            String packageName = context.mapPackageName(targetNamespace);
            if (!addedToClassCollector(packageName)) {
                allocator.assignClassName(packageName, "*");
            }
        }
    }

    private boolean addedToClassCollector(String packageName) {
        ClassCollector classCollector = context.get(ClassCollector.class);
        Collection<String> files = classCollector.getGeneratedFileInfo();
        for (String file : files) {
            int dotIndex = file.lastIndexOf(".");
            String sub = dotIndex <= 0 ? "" : file.substring(0, dotIndex - 1);
            if (sub.equals(packageName)) {
                return true;
            }
        }
        return false;
    }

    private boolean isSuppressCodeGen() {
        return context.optionSet(ToolConstants.CFG_SUPPRESS_GEN);
    }

    public void generate(ToolContext c) throws ToolException {
        if (!initialized) {
            initialize(c);
        }
        if (rawJaxbModelGenCode == null) {
            return;
        }

        try {
            String dir = (String)context.get(ToolConstants.CFG_OUTPUTDIR);

            TypesCodeWriter fileCodeWriter = new TypesCodeWriter(new File(dir), context.getExcludePkgList());

            if (rawJaxbModelGenCode instanceof S2JJAXBModel) {
                S2JJAXBModel schem2JavaJaxbModel = (S2JJAXBModel)rawJaxbModelGenCode;

                ClassCollector classCollector = context.get(ClassCollector.class);
                for (JClass cls : schem2JavaJaxbModel.getAllObjectFactories()) {
                    classCollector.getTypesPackages().add(cls._package().name());
                }

                JCodeModel jcodeModel = schem2JavaJaxbModel.generateCode(null, null);

                if (!isSuppressCodeGen()) {
                    jcodeModel.build(fileCodeWriter);
                }

                context.put(JCodeModel.class, jcodeModel);

                for (String str : fileCodeWriter.getExcludeFileList()) {
                    context.getExcludeFileList().add(str);
                }
            }
            return;
        } catch (IOException e) {
            Message msg = new Message("FAIL_TO_GENERATE_TYPES", LOG);
            throw new ToolException(msg, e);
        }
    }

    public String getType(QName qname, boolean element) {
        TypeAndAnnotation typeAnno = rawJaxbModelGenCode.getJavaType(qname);
        if (element) {
            Mapping mapping = rawJaxbModelGenCode.get(qname);
            if (mapping != null) {
                typeAnno = mapping.getType();
            }
        }

        if (typeAnno != null && typeAnno.getTypeClass() != null) {
            return typeAnno.getTypeClass().fullName();
        }
        return null;

    }

    public String getWrappedElementType(QName wrapperElement, QName item) {
        Mapping mapping = rawJaxbModelGenCode.get(wrapperElement);
        if (mapping != null) {
            List<? extends Property> propList = mapping.getWrapperStyleDrilldown();
            if (propList != null) {
                for (Property pro : propList) {
                    if (pro.elementName().getNamespaceURI().equals(item.getNamespaceURI())
                        && pro.elementName().getLocalPart().equals(item.getLocalPart())) {
                        return pro.type().fullName();
                    }
                }
            }
        }
        return null;
    }

    private Element removeImportElement(Element element, String sysId, OASISCatalogManager catalog) {
        List<Element> impElemList = DOMUtils.findAllElementsByTagNameNS(element,
                                                                     ToolConstants.SCHEMA_URI,
                                                                     "import");
        List<Element> incElemList = DOMUtils.findAllElementsByTagNameNS(element,
                                                                     ToolConstants.SCHEMA_URI,
                                                                     "include");
        boolean hasJAXB = DOMUtils.hasElementInNS(element, ToolConstants.NS_JAXB_BINDINGS);
        if (impElemList.size() == 0 && incElemList.size() == 0 && !hasJAXB) {
            return element;
        }
        element = (Element)cloneNode(element.getOwnerDocument(), element, true);
        List<Node> ns = new ArrayList<Node>();

        impElemList = DOMUtils.findAllElementsByTagNameNS(element,
                                                       ToolConstants.SCHEMA_URI,
                                                       "import");
        for (Element elem : impElemList) {
            Node importNode = elem;
            ns.add(importNode);
        }
        for (Node item : ns) {
            Node schemaNode = item.getParentNode();
            schemaNode.removeChild(item);
        }

        incElemList = DOMUtils.findAllElementsByTagNameNS(element,
                                                       ToolConstants.SCHEMA_URI,
                                                       "include");
        for (Element elem : incElemList) {
            Attr val = elem.getAttributeNode("schemaLocation");
            val.setNodeValue(mapSchemaLocation(val.getNodeValue(), sysId, catalog));
        }

        if (hasJAXB) {
            //need to add ns and version
            String pfx = DOMUtils.getPrefix(element, ToolConstants.NS_JAXB_BINDINGS);
            if (StringUtils.isEmpty(pfx)) {
                pfx = DOMUtils.createNamespace(element, ToolConstants.NS_JAXB_BINDINGS);
            }
            element.setAttributeNS(ToolConstants.NS_JAXB_BINDINGS,
                                   pfx + ":version", "2.0");
        }
        return element;
    }

    public Node cloneNode(Document document, Node node, boolean deep) throws DOMException {
        if (document == null || node == null) {
            return null;
        }
        int type = node.getNodeType();

        if (node.getOwnerDocument() == document) {
            return node.cloneNode(deep);
        }
        Node clone;
        switch (type) {
        case Node.CDATA_SECTION_NODE:
            clone = document.createCDATASection(node.getNodeValue());
            break;
        case Node.COMMENT_NODE:
            clone = document.createComment(node.getNodeValue());
            break;
        case Node.ENTITY_REFERENCE_NODE:
            clone = document.createEntityReference(node.getNodeName());
            break;
        case Node.ELEMENT_NODE:
            clone = document.createElement(node.getNodeName());
            NamedNodeMap attributes = node.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                ((Element)clone).setAttribute(attributes.item(i).getNodeName(), attributes.item(i)
                    .getNodeValue());
            }
            break;

        case Node.TEXT_NODE:
            clone = document.createTextNode(node.getNodeValue());
            break;
        default:
            return null;
        }
        if (deep && type == Node.ELEMENT_NODE) {
            Node child = node.getFirstChild();
            while (child != null) {
                clone.appendChild(cloneNode(document, child, true));
                child = child.getNextSibling();
            }
        }
        return clone;
    }


    public void validateSchema(Element ele, String uri,
                               final OASISCatalogManager catalog) throws ToolException {
        SchemaFactory schemaFact = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        schemaFact.setResourceResolver(new LSResourceResolver() {
            public LSInput resolveResource(String type,
                                           String namespaceURI,
                                           String publicId,
                                           String systemId,
                                           String baseURI) {
                String s = JAXBDataBinding.mapSchemaLocation(systemId, baseURI, catalog);
                return new LSInputSAXWrapper(new InputSource(s));
            }
        });
        DOMSource domSrc = new DOMSource(ele, uri);
        try {
            schemaFact.newSchema(domSrc);
        } catch (SAXException e) {
            if (e.getLocalizedMessage().indexOf("src-resolve.4.2") > -1)  {
                //Ignore schema resolve error and do nothing
            } else {
                throw new ToolException("Schema Error : " + e.getLocalizedMessage(), e);
            }
        }
    }

    public DefaultValueWriter createDefaultValueWriter(QName qname, boolean element) {
        if (defaultValues == null) {
            return null;
        }
        TypeAndAnnotation typeAnno = rawJaxbModelGenCode.getJavaType(qname);
        if (element) {
            Mapping mapping = rawJaxbModelGenCode.get(qname);
            if (mapping != null) {
                typeAnno = mapping.getType();
            }
        }
        if (typeAnno != null && typeAnno.getTypeClass() instanceof JDefinedClass) {
            JDefinedClass dc = (JDefinedClass)typeAnno.getTypeClass();
            if (dc.isAbstract()) {
                //no default values for abstract classes
                typeAnno = null;
            }
        }
        if (typeAnno != null) {
            final JType type = typeAnno.getTypeClass();
            return new JAXBDefaultValueWriter(type);
        }
        return null;
    }

    public DefaultValueWriter createDefaultValueWriterForWrappedElement(QName wrapperElement, QName item) {
        if (defaultValues != null) {
            Mapping mapping = rawJaxbModelGenCode.get(wrapperElement);
            if (mapping != null) {
                List<? extends Property> propList = mapping.getWrapperStyleDrilldown();
                for (Property pro : propList) {
                    if (pro.elementName().getNamespaceURI().equals(item.getNamespaceURI())
                        && pro.elementName().getLocalPart().equals(item.getLocalPart())) {

                        JType type = pro.type();
                        if (type instanceof JDefinedClass
                            && ((JDefinedClass)type).isAbstract()) {
                            //no default values for abstract classes
                            return null;
                        }
                        return new JAXBDefaultValueWriter(pro.type());
                    }
                }
            }
        }
        return null;
    }


    private class JAXBDefaultValueWriter implements DefaultValueWriter {
        final JType type;
        JAXBDefaultValueWriter(JType tp) {
            type = tp;
        }
        public void writeDefaultValue(Writer writer, String indent,
                                      String path, String varName) throws IOException {
            path = path + "/" + varName;
            writeDefaultValue(writer, indent, path, varName, type);
        }

        public void writeDefaultValue(Writer writer, String indent,
                                      String path, String varName,
                                      JType tp) throws IOException {
            writer.write(tp.fullName());
            writer.write(" ");
            writer.write(varName);
            writer.write(" = ");
            if (tp.isArray()) {
                writer.write("new ");
                writer.write(tp.fullName());
                writer.write(" {};");
            } else if (DEFAULT_TYPE_MAP.contains(tp.fullName())) {
                writeDefaultType(writer, tp, path);
                writer.write(";");
            } else if (tp instanceof JDefinedClass) {
                JDefinedClass jdc = (JDefinedClass)tp;
                if (jdc.getClassType() == ClassType.ENUM) {
                    //no way to get the field list as it's private with
                    //no accessors :-(
                    try {
                        Field f = jdc.getClass().getDeclaredField("enumConstantsByName");
                        f.setAccessible(true);
                        Map map = (Map)f.get(jdc);
                        Set<String> values = CastUtils.cast(map.keySet());
                        String first = defaultValues.chooseEnumValue(path, values);
                        writer.write(tp.fullName());
                        writer.write(".");
                        writer.write(first);
                        writer.write(";");
                    } catch (Exception e) {
                        IOException ex = new IOException(e.getMessage());
                        ex.initCause(e);
                        throw ex;
                    }
                } else if (jdc.isAbstract()) {
                    writer.write("null;");
                } else {
                    writer.write("new ");
                    writer.write(tp.fullName());
                    writer.write("();");
                    fillInFields(writer, indent, path, varName, jdc);
                }
            } else {
                boolean found = false;
                JType tp2 = tp.erasure();
                try {
                    Field f = tp2.getClass().getDeclaredField("_class");
                    f.setAccessible(true);
                    Class<?> cls = (Class)f.get(tp2);
                    if (List.class.isAssignableFrom(cls)) {
                        found = true;

                        writer.write("new ");
                        writer.write(tp.fullName().replace("java.util.List", "java.util.ArrayList"));
                        writer.write("();");

                        f = tp.getClass().getDeclaredField("args");
                        f.setAccessible(true);
                        List<JClass> lcl = CastUtils.cast((List)f.get(tp));
                        JClass cl = lcl.get(0);

                        int cnt = defaultValues.getListLength(path + "/" + varName);
                        for (int x = 0; x < cnt; x++) {

                            writer.write("\n");
                            writer.write(indent);
                            writeDefaultValue(writer, indent, path + "/" + varName + "Val",
                                              varName + "Val" + cnt , cl);
                            writer.write("\n");
                            writer.write(indent);
                            writer.write(varName);
                            writer.write(".add(");
                            writer.write(varName + "Val" + cnt);
                            writer.write(");");
                        }
                    }
                } catch (Exception e) {
                    //ignore
                }

                if (!found) {
                    //System.err.println("No idea what to do with " + tp.fullName());
                    //System.err.println("        class " + tp.getClass().getName());
                    writer.write("null;");
                }
            }
        }
        public void fillInFields(Writer writer, String indent,
                                      String path, String varName,
                                      JDefinedClass tp) throws IOException {
            JClass sp = tp._extends();
            if (sp instanceof JDefinedClass) {
                fillInFields(writer, indent, path, varName, (JDefinedClass)sp);
            }

            Collection<JMethod> methods = tp.methods();
            for (JMethod m : methods) {
                if (m.name().startsWith("set")) {
                    writer.write("\n");
                    writer.write(indent);
                    if (DEFAULT_TYPE_MAP.contains(m.listParamTypes()[0].fullName())) {
                        writer.write(varName);
                        writer.write(".");
                        writer.write(m.name());
                        writer.write("(");
                        writeDefaultType(writer, m.listParamTypes()[0], path + "/" + m.name().substring(3));
                        writer.write(");");
                    } else {
                        int idx = path.indexOf("/" + m.name().substring(3) + "/");
                        if (idx > 0) {
                            idx = path.indexOf("/" + m.name().substring(3) + "/", idx + 1);
                        }
                        boolean hasTwo = idx > 0;
                        if (!hasTwo) {
                            writeDefaultValue(writer, indent,
                                              path + "/" + m.name().substring(3),
                                              varName + m.name().substring(3),
                                              m.listParamTypes()[0]);
                            writer.write("\n");
                        }
                        writer.write(indent);
                        writer.write(varName);
                        writer.write(".");
                        writer.write(m.name());
                        writer.write("(");
                        if (!hasTwo) {
                            writer.write(varName + m.name().substring(3));
                        } else {
                            writer.write("null");
                        }
                        writer.write(");");
                    }
                } else if (m.type().fullName().startsWith("java.util.List")) {
                    writer.write("\n");
                    writer.write(indent);
                    writeDefaultValue(writer, indent,
                                      path + "/" + m.name().substring(3),
                                      varName + m.name().substring(3),
                                      m.type());
                    writer.write("\n");
                    writer.write(indent);
                    writer.write(varName);
                    writer.write(".");
                    writer.write(m.name());
                    writer.write("().addAll(");
                    writer.write(varName + m.name().substring(3));
                    writer.write(");");
                }
            }
        }
        private void writeDefaultType(Writer writer, JType t, String path) throws IOException {
            String name = t.fullName();
            writeDefaultType(writer, name, path);

        }
        private void writeDefaultType(Writer writer, String name, String path) throws IOException {
            if (JLDEFAULT_TYPE_MAP.containsKey(name)) {
                writer.append(name.substring("java.lang.".length())).append(".valueOf(");
                writeDefaultType(writer, JLDEFAULT_TYPE_MAP.get(name), path);
                writer.append(")");
            } else if ("boolean".equals(name)) {
                writer.append(defaultValues.getBooleanValue(path) ? "true" : "false");
            } else if ("int".equals(name)) {
                writer.append(Integer.toString(defaultValues.getIntValue(path)));
            } else if ("long".equals(name)) {
                writer.append(Long.toString(defaultValues.getLongValue(path))).append("l");
            } else if ("short".equals(name)) {
                writer.append("(short)").append(Short.toString(defaultValues.getShortValue(path)));
            } else if ("byte".equals(name)) {
                writer.append("(byte)").append(Byte.toString(defaultValues.getByteValue(path)));
            } else if ("float".equals(name)) {
                writer.append(Float.toString(defaultValues.getFloatValue(path))).append("f");
            } else if ("double".equals(name)) {
                writer.append(Double.toString(defaultValues.getDoubleValue(path)));
            } else if ("char".equals(name)) {
                writer.append("(char)").append(Character.toString(defaultValues.getCharValue(path)));
            } else if ("java.lang.String".equals(name)) {
                writer.append("\"")
                    .append(defaultValues.getStringValue(path))
                    .append("\"");
            } else if ("javax.xml.namespace.QName".equals(name)) {
                QName qn = defaultValues.getQNameValue(path);
                writer.append("new javax.xml.namespace.QName(\"")
                      .append(qn.getNamespaceURI())
                      .append("\", \"")
                      .append(qn.getLocalPart())
                      .append("\")");
            } else if ("java.net.URI".equals(name)) {
                writer.append("new java.net.URI(\"")
                      .append(defaultValues.getURIValue(path).toASCIIString())
                      .append("\")");
            } else if ("java.math.BigInteger".equals(name)) {
                writer.append("new java.math.BigInteger(\"")
                      .append(defaultValues.getBigIntegerValue(path).toString())
                      .append("\")");
            } else if ("java.math.BigDecimal".equals(name)) {
                writer.append("new java.math.BigDecimal(\"")
                      .append(defaultValues.getBigDecimalValue(path).toString())
                      .append("\")");
            } else if ("javax.xml.datatype.XMLGregorianCalendar".equals(name)) {
                writer.append("javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar(\"")
                      .append(defaultValues.getXMLGregorianCalendarValueString(path))
                      .append("\")");
            } else if ("javax.xml.datatype.Duration".equals(name)) {
                writer.append("javax.xml.datatype.DatatypeFactory.newInstance().newDuration(\"")
                      .append(defaultValues.getDurationValueString(path))
                      .append("\")");
            }
        }
    }
    private static String mapSchemaLocation(String target, String base, OASISCatalogManager catalog) {
        try {
            String resolvedLocation = new OASISCatalogManagerHelper().resolve(catalog, 
                                                                              target, base);
            if (resolvedLocation != null) {
                return resolvedLocation;
            }

        } catch (Exception ex) {
            //ignore
        }
        

        try {
            URIResolver resolver = new URIResolver(base, target);
            if (resolver.isResolved()) {
                target = resolver.getURI().toString();
            }
        } catch (Exception ex) {
            //ignore
        }
        return target;
    }

}
