/*
 * 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.axis2.jibx;

import org.apache.axis2.AxisFault;
import org.apache.axis2.description.AxisMessage;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.wsdl.SOAPHeaderMessage;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.axis2.wsdl.WSDLUtil;
import org.apache.axis2.wsdl.codegen.CodeGenConfiguration;
import org.apache.axis2.wsdl.codegen.extension.JiBXExtension;
import org.apache.axis2.wsdl.databinding.JavaTypeMapper;
import org.apache.axis2.wsdl.util.Constants;
import org.apache.axis2.wsdl.util.MessagePartInformationHolder;
import org.apache.ws.commons.schema.XmlSchemaComplexType;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaParticle;
import org.apache.ws.commons.schema.XmlSchemaSequence;
import org.apache.ws.commons.schema.XmlSchemaSequenceMember;
import org.apache.ws.commons.schema.XmlSchemaSimpleType;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeContent;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeRestriction;
import org.apache.ws.commons.schema.XmlSchemaType;
import org.jibx.binding.model.BindingElement;
import org.jibx.binding.model.ElementBase;
import org.jibx.binding.model.FormatElement;
import org.jibx.binding.model.IncludeElement;
import org.jibx.binding.model.MappingElementBase;
import org.jibx.binding.model.ModelVisitor;
import org.jibx.binding.model.NamespaceElement;
import org.jibx.binding.model.ValidationContext;
import org.jibx.binding.model.ValidationProblem;
import org.jibx.runtime.JiBXException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Framework-linked code used by JiBX data binding support. This is accessed via reflection from the
 * JiBX code generation extension when JiBX data binding is selected. JiBX uses a different approach
 * to unwrapping method parameters from that implemented by ADB, and since the ADB technique is
 * assumed by all code generation templates this has to create the same data structures. These data
 * structures are undocumented, and can only be determined by going through the {@link
 * org.apache.axis2.wsdl.codegen.extension.SchemaUnwrapperExtension} and {@link
 * org.apache.axis2.wsdl.codegen.emitter.AxisServiceBasedMultiLanguageEmitter} code.
 */
public class CodeGenerationUtility {
    private static final String SCHEMA_NAMESPACE = "http://www.w3.org/2001/XMLSchema";

    private final CodeGenConfiguration codeGenConfig;

    private static HashSet s_primitiveSet = new HashSet();

    static {
        s_primitiveSet.add("boolean");
        s_primitiveSet.add("byte");
        s_primitiveSet.add("char");
        s_primitiveSet.add("double");
        s_primitiveSet.add("float");
        s_primitiveSet.add("int");
        s_primitiveSet.add("long");
        s_primitiveSet.add("short");
        s_primitiveSet.add("void");
    }

    private static HashMap s_wrapperMap = new HashMap();

    static {
        s_wrapperMap.put("boolean", "Boolean");
        s_wrapperMap.put("byte", "Byte");
        s_wrapperMap.put("char", "Character");
        s_wrapperMap.put("double", "Double");
        s_wrapperMap.put("float", "Float");
        s_wrapperMap.put("int", "Integer");
        s_wrapperMap.put("long", "Long");
        s_wrapperMap.put("short", "Short");
    }

    /** Reserved words for Java (keywords and literals). */
    private static final HashSet s_reservedWords = new HashSet();

    static {

        // keywords
        s_reservedWords.add("abstract");
        s_reservedWords.add("assert");
        s_reservedWords.add("boolean");
        s_reservedWords.add("break");
        s_reservedWords.add("byte");
        s_reservedWords.add("case");
        s_reservedWords.add("catch");
        s_reservedWords.add("char");
        s_reservedWords.add("class");
        s_reservedWords.add("const");
        s_reservedWords.add("continue");

        s_reservedWords.add("default");
        s_reservedWords.add("do");
        s_reservedWords.add("double");
        s_reservedWords.add("else");
        s_reservedWords.add("enum");
        s_reservedWords.add("extends");
        s_reservedWords.add("final");
        s_reservedWords.add("finally");
        s_reservedWords.add("float");
        s_reservedWords.add("for");
        s_reservedWords.add("goto");

        s_reservedWords.add("if");
        s_reservedWords.add("implements");
        s_reservedWords.add("import");
        s_reservedWords.add("instanceof");
        s_reservedWords.add("int");
        s_reservedWords.add("interface");
        s_reservedWords.add("long");
        s_reservedWords.add("native");
        s_reservedWords.add("new");
        s_reservedWords.add("package");

        s_reservedWords.add("private");
        s_reservedWords.add("protected");
        s_reservedWords.add("public");
        s_reservedWords.add("return");
        s_reservedWords.add("short");
        s_reservedWords.add("static");
        s_reservedWords.add("strictfp");
        s_reservedWords.add("super");
        s_reservedWords.add("switch");
        s_reservedWords.add("synchronized");

        s_reservedWords.add("this");
        s_reservedWords.add("throw");
        s_reservedWords.add("throws");
        s_reservedWords.add("transient");
        s_reservedWords.add("try");
        s_reservedWords.add("void");
        s_reservedWords.add("volatile");
        s_reservedWords.add("while");

        // literals
        s_reservedWords.add("true");
        s_reservedWords.add("false");
        s_reservedWords.add("null");

        // variable names used by template unwrapped code generation
        // TODO change templates to use $xxx names, block generation from WSDL
        s_reservedWords.add("true");
        s_reservedWords.add("uctx");
        s_reservedWords.add("child");
        s_reservedWords.add("wrapper");
    }

    /**
     * Constructor.
     *
     * @param config code generation configuration
     */
    public CodeGenerationUtility(CodeGenConfiguration config) {
        codeGenConfig = config;
    }

    /**
     * Configure the code generation based on the supplied parameters and WSDL. This first gets type
     * mappings from binding definition, then goes through the operations checking the input and
     * output messages. If unwrapping is disabled the message element must be handled directly by a
     * mapping. If unwrapping is enabled, this checks that the message element is of the proper form
     * (a sequence of other elements, which all have maxOccurs="1"). It then generates an unwrapping
     * description and adds it to the code generation configuration, where it'll be picked up and
     * included in the XML passed to code generation. This also constructs a type mapping, since
     * that's required by the base Axis2 code generation. In the case of unwrapped elements, the
     * type mapping includes a synthesized qname for each unwrapped parameter, and the detailed
     * information is set on the message information. Sound confusing? Welcome to Axis2 code
     * generation.
     *
     * @param path binding path (<code>null</code> if none)
     */
    public void engage(String path) {

        // make sure the binding definition file is present, if passed
        File file = null;
        if (path != null) {
            file = new File(path);
            if (!file.exists()) {
                throw new RuntimeException("jibx binding definition file " + path + " not found");
            }
        }
        try {

            // set flag for unwrapping
            boolean unwrap = !codeGenConfig.isParametersWrapped();

            // initialize the binding information
            BindingElement binding = null;
            if (file == null) {

                // unwrapped can be used without a binding, but wrapped requires one
                if (!unwrap) {
                    throw new RuntimeException(
                            "JiBX wrapped support requires a binding definition to be provided using the -E" +
                                    JiBXExtension.BINDING_PATH_OPTION + " {file-path} parameter");
                }

            } else {

                // Read the JiBX binding definition into memory. The binding definition
                // is only prevalidated so as not to require the user to have all
                // the referenced classes in the classpath, though this does make for
                // added work in finding the namespaces.
                ValidationContext vctx = BindingElement.newValidationContext();
                binding = BindingElement.readBinding(new FileInputStream(file), path, vctx);
                binding.setBaseUrl(file.toURI().toURL());
                vctx.setBindingRoot(binding);
                IncludePrevalidationVisitor ipv = new IncludePrevalidationVisitor(vctx);
                vctx.tourTree(binding, ipv);
                if (vctx.getErrorCount() != 0 || vctx.getFatalCount() != 0) {
                    ArrayList probs = vctx.getProblems();
                    System.err.println("Errors in generated binding:");
                    for (int j = 0; j < probs.size(); j++) {
                        ValidationProblem prob = (ValidationProblem)probs.get(j);
                        System.err.print(prob.getSeverity() >=
                                ValidationProblem.ERROR_LEVEL ? "Error: " : "Warning: ");
                        System.err.println(prob.getDescription());
                    }
                    throw new RuntimeException("invalid jibx binding definition file " + path);
                }
            }

            // create table with all built-in format definitions
            Map simpleTypeMap = new HashMap();
            buildFormat("byte", "byte",
                        "org.jibx.runtime.Utility.serializeByte",
                        "org.jibx.runtime.Utility.parseByte", "0", simpleTypeMap);
            buildFormat("unsignedShort", "char",
                        "org.jibx.runtime.Utility.serializeChar",
                        "org.jibx.runtime.Utility.parseChar", "0", simpleTypeMap);
            buildFormat("double", "double",
                        "org.jibx.runtime.Utility.serializeDouble",
                        "org.jibx.runtime.Utility.parseDouble", "0.0", simpleTypeMap);
            buildFormat("float", "float",
                        "org.jibx.runtime.Utility.serializeFloat",
                        "org.jibx.runtime.Utility.parseFloat", "0.0", simpleTypeMap);
            buildFormat("int", "int",
                        "org.jibx.runtime.Utility.serializeInt",
                        "org.jibx.runtime.Utility.parseInt", "0", simpleTypeMap);
            buildFormat("long", "long",
                        "org.jibx.runtime.Utility.serializeLong",
                        "org.jibx.runtime.Utility.parseLong", "0", simpleTypeMap);
            buildFormat("short", "short",
                        "org.jibx.runtime.Utility.serializeShort",
                        "org.jibx.runtime.Utility.parseShort", "0", simpleTypeMap);
            buildFormat("boolean", "boolean",
                        "org.jibx.runtime.Utility.serializeBoolean",
                        "org.jibx.runtime.Utility.parseBoolean", "false",
                        simpleTypeMap);
            buildFormat("dateTime", "java.util.Date",
                        "org.jibx.runtime.Utility.serializeDateTime",
                        "org.jibx.runtime.Utility.deserializeDateTime", null,
                        simpleTypeMap);
            buildFormat("date", "java.sql.Date",
                        "org.jibx.runtime.Utility.serializeSqlDate",
                        "org.jibx.runtime.Utility.deserializeSqlDate", null,
                        simpleTypeMap);
            buildFormat("time", "java.sql.Time",
                        "org.jibx.runtime.Utility.serializeSqlTime",
                        "org.jibx.runtime.Utility.deserializeSqlTime", null,
                        simpleTypeMap);
            buildFormat("base64Binary", "byte[]",
                        "org.jibx.runtime.Utility.serializeBase64",
                        "org.jibx.runtime.Utility.deserializeBase64", null,
                        simpleTypeMap);
            buildFormat("string", "java.lang.String", null, null, null,
                        simpleTypeMap);

            // collect all the top-level mapping and format definitions
            Map elementMap = new HashMap();
            Map complexTypeMap = new HashMap();
            Map bindingMap = new HashMap();
            if (binding != null) {
                collectTopLevelComponents(binding, "", elementMap,
                                          complexTypeMap, simpleTypeMap, bindingMap);
            }

            // make sure classes will be generated for abstract mappings
            if (unwrap && complexTypeMap.size() > 0 && (binding == null || !binding.isForceClasses())) {
                throw new RuntimeException(
                        "unwrapped binding must use force-classes='true' option in " + path);
            }

            // force off inappropriate option (set by error in options handling)
            codeGenConfig.setPackClasses(false);

            // configure handling for all operations of service
            codeGenConfig.setTypeMapper(new NamedParameterTypeMapper());
            Iterator operations = codeGenConfig.getAxisService().getOperations();
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            int opindex = 0;
            Map typeMappedClassMap = new HashMap();
            String mappedclass = null;
            Set objins = new HashSet();
            Set objouts = new HashSet();
            Set objfaults = new HashSet();
            Map nsMap = new HashMap();
            ArrayList wrappers = new ArrayList();
            while (operations.hasNext()) {

                // get the basic operation information
                AxisOperation op = (AxisOperation)operations.next();
                String mep = op.getMessageExchangePattern();
                AxisMessage inmsg = null;
                AxisMessage outmsg = null;
                if (WSDLUtil.isInputPresentForMEP(mep)) {
                    inmsg = op.getMessage(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
                    if (inmsg == null) {
                        throw new RuntimeException(
                                "Expected input message not found for operation " + op.getName());
                    }
                    ArrayList headers = inmsg.getSoapHeaders();
                    for (int i = 0; i < headers.size(); i++) {
                        SOAPHeaderMessage header = (SOAPHeaderMessage)headers.get(i);
                        String cname = mapMessage(header, elementMap);
                        objins.add(cname);
                        if (mappedclass == null && isLookupClass(cname)) {
                            mappedclass = cname;
                        }
                    }
                }
                if (WSDLUtil.isOutputPresentForMEP(mep)) {
                    outmsg = op.getMessage(WSDLConstants.MESSAGE_LABEL_OUT_VALUE);
                    if (outmsg == null) {
                        throw new RuntimeException(
                                "Expected output message not found for operation " + op.getName());
                    }
                    ArrayList headers = outmsg.getSoapHeaders();
                    for (int i = 0; i < headers.size(); i++) {
                        SOAPHeaderMessage header = (SOAPHeaderMessage)headers.get(i);
                        String cname = mapMessage(header, elementMap);
                        objouts.add(cname);
                        if (mappedclass == null && isLookupClass(cname)) {
                            mappedclass = cname;
                        }
                    }
                }
                if (unwrap) {

                    // use unwrapping for both input and output
                    String receivername = "jibxReceiver" + opindex++;
                    Element dbmethod = doc.createElement("dbmethod");
                    dbmethod.setAttribute("receiver-name", receivername);
                    dbmethod.setAttribute("method-name", op.getName().getLocalPart());
                    Set nameset = new HashSet(s_reservedWords);
                    if (inmsg != null) {
                        Element wrapper = unwrapMessage(inmsg, false, simpleTypeMap, elementMap,
                                                        complexTypeMap, typeMappedClassMap,
                                                        bindingMap, nameset, nsMap, doc);
                        dbmethod.appendChild(wrapper);
                        wrappers.add(wrapper);
                    }
                    if (outmsg != null) {
                        Element wrapper = unwrapMessage(outmsg, true, simpleTypeMap, elementMap,
                                                        complexTypeMap, typeMappedClassMap,
                                                        bindingMap, nameset, nsMap, doc);
                        dbmethod.appendChild(wrapper);
                        wrappers.add(wrapper);
                    }

                    // save unwrapping information for use in code generation
                    op.addParameter(
                            new Parameter(Constants.DATABINDING_GENERATED_RECEIVER, receivername));
                    op.addParameter(new Parameter(Constants.DATABINDING_GENERATED_IMPLEMENTATION,
                                                  Boolean.TRUE));
                    op.addParameter(
                            new Parameter(Constants.DATABINDING_OPERATION_DETAILS, dbmethod));

                } else {

                    // concrete mappings, just save the mapped class name(s)
                    if (inmsg != null) {
                        String cname = mapMessage(inmsg, elementMap);
                        objins.add(cname);
                        if (mappedclass == null && isLookupClass(cname)) {
                            mappedclass = cname;
                        }
                    }
                    if (outmsg != null) {
                        String cname = mapMessage(outmsg, elementMap);
                        objouts.add(cname);
                        if (mappedclass == null && isLookupClass(cname)) {
                            mappedclass = cname;
                        }
                    }

                }

                // always handle faults as wrapped
                for (Iterator iter = op.getFaultMessages().iterator(); iter.hasNext();) {
                    String cname = mapMessage((AxisMessage)iter.next(), elementMap);
                    objfaults.add(cname);
                    if (mappedclass == null && isLookupClass(cname)) {
                        mappedclass = cname;
                    }
                }
            }

            // check for default namespace usage within bindings or wrappers
            //  (meaning we can't declare a conflicting default namespace)
            Collection prefixes = nsMap.values();
            boolean dfltns = prefixes.contains("");
            boolean wrapdflt = false;
            if (!dfltns) {
                for (int i = 0; i < wrappers.size(); i++) {
                    Element wrapper = (Element)wrappers.get(i);
                    if ("true".equals(wrapper.getAttribute("uses-default"))) {
                        wrapdflt = true;
                        break;
                    }
                }
            }

            // find a prefix that we can use where needed for extra namespace
            String xtrapref = "";
            if (dfltns || wrapdflt) {
                xtrapref = "_";
                int index = 0;
                while (prefixes.contains(xtrapref)) {
                    xtrapref = "_" + index++;
                }
            }

            // for each wrapper (input and output), determine what additional
            //  namespaces need to be declared, what prefix is to be used for
            //  the wrapper element, and what prefix to be used for each child
            //  element
            for (int i = 0; i < wrappers.size(); i++) {
                Element wrapper = (Element)wrappers.get(i);
                boolean addns = false;
                String ns = wrapper.getAttribute("ns");
                String prefix = "";
                if ("true".equals(wrapper.getAttribute("need-namespaces"))) {

                    // check extra definition needed for wrapper namespace
                    if (!"".equals(ns)) {
                        if (dfltns || wrapdflt) {

                            // need a namespace, can't be default, get or set it
                            prefix = (String)nsMap.get(ns);
                            if (prefix == null) {
                                prefix = xtrapref;
                                addns = true;
                            }

                        } else {

                            // just make the wrapper namespace the default
                            prefix = "";
                            addns = true;

                        }
                    }
                    wrapper.setAttribute("prefix", prefix);

                    // set prefixes for child elements of wrapper
                    Node node = wrapper.getFirstChild();
                    while (node != null) {
                        if (node.getNodeType() == Node.ELEMENT_NODE) {
                            Element element = (Element)node;
                            String lname = element.getNodeName();
                            if ("parameter-element".equals(lname) || "return-element".equals(lname))
                            {
                                String childns = element.getAttribute("ns");
                                if ("".equals(childns)) {
                                    element.setAttribute("prefix", "");
                                } else if (ns.equals(childns)) {
                                    element.setAttribute("prefix", prefix);
                                } else {
                                    String childprefix = (String)nsMap.get(childns);
                                    if (childprefix == null) {
                                        throw new RuntimeException("Unable to set namespace " +
                                                childns + " for child element");
                                    }
                                    element.setAttribute("prefix", childprefix);
                                }
                            }
                        }
                        node = node.getNextSibling();
                    }

                } else {

                    // check extra definition needed for wrapper namespace
                    if (!"".equals(ns)) {

                        // just make the wrapper namespace the default
                        prefix = "";
                        addns = true;

                    }
                    wrapper.setAttribute("prefix", prefix);

                    // set prefixes for child elements of wrapper
                    Node node = wrapper.getFirstChild();
                    while (node != null) {
                        if (node.getNodeType() == Node.ELEMENT_NODE) {
                            Element element = (Element)node;
                            String lname = element.getNodeName();
                            if ("parameter-element".equals(lname) || "return-element".equals(lname))
                            {
                                String childns = element.getAttribute("ns");
                                if ("".equals(childns)) {
                                    element.setAttribute("prefix", "");
                                } else if (ns.equals(childns)) {
                                    element.setAttribute("prefix", prefix);
                                } else {
                                    throw new RuntimeException("Unable to set namespace " +
                                            childns + " for child element");
                                }
                            }
                        }
                        node = node.getNextSibling();
                    }

                }
                if (addns) {
                    Element addedns = doc.createElement("extra-namespace");
                    addedns.setAttribute("ns", ns);
                    addedns.setAttribute("prefix", prefix);
                    wrapper.appendChild(addedns);
                }
            }

            // add type usage information for binding initialization
            List details = new ArrayList();
            Element bindinit = doc.createElement("initialize-binding");
            if (!typeMappedClassMap.isEmpty()) {
                for (Iterator iter = typeMappedClassMap.keySet().iterator(); iter.hasNext();) {
                    QName tname = (QName)iter.next();
                    String clsindex = ((Integer)typeMappedClassMap.get(tname)).toString();
                    Element detail = doc.createElement("abstract-type");
                    detail.setAttribute("ns", tname.getNamespaceURI());
                    detail.setAttribute("name", tname.getLocalPart());
                    detail.setAttribute("type-index", clsindex);
                    bindinit.appendChild(detail);
                    if (mappedclass == null) {
                        MappingElementBase mapping = (MappingElementBase)complexTypeMap.get(tname);
                        mappedclass = mapping.getClassName();
                    }
                }
            }

            // set binding lookup parameters
            if (binding != null && binding.getName() != null && binding.getTargetPackage() != null) {
                bindinit.setAttribute("binding-name", binding.getName());
                bindinit.setAttribute("binding-package", binding.getTargetPackage());
            } else {
                if (mappedclass == null) {
                    mappedclass = "";
                }
                bindinit.setAttribute("bound-class", mappedclass);
            }

            // include binding namespaces in initialization data
            for (Iterator iter = nsMap.keySet().iterator(); iter.hasNext();) {
                String ns = (String)iter.next();
                String prefix = (String)nsMap.get(ns);
                Element detail = doc.createElement("binding-namespace");
                detail.setAttribute("ns", ns);
                detail.setAttribute("prefix", prefix);
                bindinit.appendChild(detail);
            }
            details.add(bindinit);

            // add details for all objects used as inputs/outputs/faults
            for (Iterator iter = objins.iterator(); iter.hasNext();) {
                String classname = (String)iter.next();
                Element detail = doc.createElement("object-input");
                detail.setAttribute("type", classname);
                details.add(detail);
            }
            for (Iterator iter = objouts.iterator(); iter.hasNext();) {
                String classname = (String)iter.next();
                Element detail = doc.createElement("object-output");
                detail.setAttribute("type", classname);
                details.add(detail);
            }
            for (Iterator iter = objfaults.iterator(); iter.hasNext();) {
                String classname = (String)iter.next();
                Element detail = doc.createElement("object-fault");
                detail.setAttribute("type", classname);
                details.add(detail);
            }
            codeGenConfig.getAxisService()
                    .addParameter(new Parameter(Constants.DATABINDING_SERVICE_DETAILS, details));

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (JiBXException e) {
            throw new RuntimeException(e);
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        } catch (AxisFault e) {
            throw new RuntimeException(e);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Check if a class is potentially usable for looking up binding information.
     *
     * @param type fully-qualified type name
     * @return <code>true</code> if potentially usable, <code>false</code> if not
     */
    private static boolean isLookupClass(String type) {
        return !type.startsWith("java.") && !type.startsWith("javax.") &&
                !type.startsWith("org.w3c.");
    }

    /**
     * Add format definition for type with built-in JiBX handling to map.
     *
     * @param stype schema type name
     * @param jtype java type name
     * @param sname serializer method name
     * @param dname deserializer method name
     * @param dflt  default value
     * @param map   schema type qname to format definition map
     */
    private static void buildFormat(String stype, String jtype, String sname,
                                    String dname, String dflt, Map map) {
        FormatElement format = new FormatElement();
        format.setTypeName(jtype);
        format.setSerializerName(sname);
        format.setDeserializerName(dname);
        format.setDefaultText(dflt);
        map.put(new QName(SCHEMA_NAMESPACE, stype), format);
    }

    /**
     * Handles unwrapping a message. This generates and returns the detailed description of how the
     * message is to be unwrapped. It also creates the data structures expected by the code
     * generation in order to be somewhat compatible with ADB unwrapping.
     *
     * @param msg                message to be unwrapped
     * @param isout              output message flag (wrapper inherits inner type, for XSLTs)
     * @param simpleTypeMap      binding formats
     * @param elementMap         map from element names to concrete mapping components of binding
     * @param complexTypeMap     binding mappings
     * @param typeMappedClassMap map from type qname to index
     * @param bindingMap         map from mapping components to containing binding definition
     * @param nameset            parameter variable names used in method
     * @param nsmap              mapr from URI to prefix for namespaces to be defined on wrapper
     *                           element
     * @param doc                document used for DOM components
     * @return detailed description element for code generation
     */
    private Element unwrapMessage(AxisMessage msg, boolean isout,
                                  Map simpleTypeMap, Map elementMap, Map complexTypeMap, Map typeMappedClassMap,
                                  Map bindingMap, Set nameset, Map nsmap, Document doc) {

        // find the schema definition for this message element
        QName qname = msg.getElementQName();
        if (qname == null) {
            throw new RuntimeException("No element reference in message " + msg.getName());
        }
        XmlSchemaElement wrapdef = codeGenConfig.getAxisService().getSchemaElement(qname);
        if (wrapdef == null) {
            throw new RuntimeException("Cannot unwrap - no definition found for element " + qname);
        }
        XmlSchemaType type = wrapdef.getSchemaType();

        // create document to hold data binding details for element
        Element wrapdetail = doc.createElement(isout ? "out-wrapper" : "in-wrapper");
        wrapdetail.setAttribute("ns", qname.getNamespaceURI());
        wrapdetail.setAttribute("name", qname.getLocalPart());

        // dig down to the complexType particle
        List partNameList = new ArrayList();
        String wrappertype = "";
        boolean nons = qname.getNamespaceURI().length() == 0;
        boolean dfltns = false;
        boolean complex = false;
        if (type instanceof XmlSchemaComplexType) {
            XmlSchemaComplexType ctype = (XmlSchemaComplexType)type;
            if (!ctype.getAttributes().isEmpty()) {
                throw new RuntimeException("Cannot unwrap element " +
                        qname + ": attributes not allowed on type to be unwrapped");
            }
            XmlSchemaParticle particle = ctype.getParticle();
            if (particle != null) {

                // if there's a particle present, it must be a sequence
                if (!(particle instanceof XmlSchemaSequence)) {
                    throw new RuntimeException("Cannot unwrap element " +
                            qname + ": type to be unwrapped must be a sequence");
                }
                if (particle.getMinOccurs() != 1 || particle.getMaxOccurs() != 1) {
                    throw new RuntimeException("Cannot unwrap element " +
                            qname +
                            ": contained sequence must have minOccurs='1' and maxOccurs='1'");
                }
                XmlSchemaSequence sequence = (XmlSchemaSequence)particle;

                // add child param element matching each child of wrapper element
                QName opName = msg.getAxisOperation().getName();
                boolean first = true;
                for (XmlSchemaSequenceMember member : sequence.getItems()) {

                    // check that child item obeys the unwrapping rules
                    XmlSchemaParticle item = (XmlSchemaParticle)member;

                    if (!(item instanceof XmlSchemaElement)) {
                        throw new RuntimeException("Cannot unwrap element " +
                                qname + ": only element items allowed in sequence");
                    }
                    XmlSchemaElement element = (XmlSchemaElement)item;
                    QName refname = element.getRef().getTargetQName();
                    QName typename = element.getSchemaTypeName();
                    if (refname == null && typename == null) {
                        throw new RuntimeException("Cannot unwrap element " +
                                qname +
                                ": all elements in contained sequence must be element references or reference a named type");
                    }
                    if (first) {
                        first = false;
                    } else if (isout) {
                        throw new RuntimeException("Cannot unwrap element " +
                                qname +
                                ": only one child element allowed in sequence for wrapped output");
                    }

                    // add element to output with details of this element handling
                    Element param =
                            doc.createElement(isout ? "return-element" : "parameter-element");
                    QName itemname = (refname == null) ? element.getQName() : refname;
                    nons = nons || itemname.getNamespaceURI().length() == 0;
                    param.setAttribute("ns", itemname.getNamespaceURI());
                    param.setAttribute("name", itemname.getLocalPart());
                    String javaname = toJavaName(itemname.getLocalPart(), nameset);
                    param.setAttribute("java-name", javaname);
                    param.setAttribute("nillable", Boolean.toString(element.isNillable()));
                    boolean optional = element.getMinOccurs() == 0;
                    param.setAttribute("optional", Boolean.toString(optional));
                    boolean isarray = element.getMaxOccurs() > 1;
                    param.setAttribute("array", Boolean.toString(isarray));
                    String javatype;
                    String createtype = null;
                    if (element.getSchemaType() instanceof XmlSchemaSimpleType) {

                        // simple type translates to format element in binding
                        FormatElement format = (FormatElement)simpleTypeMap.get(typename);
                        if (format == null) {
                            
                            // check for restriction with simple base, and treat as base if so
                            XmlSchemaSimpleType stype = (XmlSchemaSimpleType)element.getSchemaType();
                            XmlSchemaSimpleTypeContent content = stype.getContent();
                            if (content instanceof XmlSchemaSimpleTypeRestriction) {
                                QName tname = ((XmlSchemaSimpleTypeRestriction)content).getBaseTypeName();
                                if (SCHEMA_NAMESPACE.equals(tname.getNamespaceURI())) {
                                    format = (FormatElement)simpleTypeMap.get(tname);
                                    if (format != null) {
                                        typename = tname;
                                    }
                                }
                            }
                        }
                        if (format == null) {
                            throw new RuntimeException("Cannot unwrap element " +
                                qname + ": no format definition found for type " +
                                typename + " (used by element " + itemname + ')');
                        }
                        javatype = format.getTypeName();
                        param.setAttribute("form", "simple");
                        param.setAttribute("serializer", format.getSerializerName());
                        param.setAttribute("deserializer", format.getDeserializerName());

                        // convert primitive types to wrapper types for nillable
                        if ((optional || element.isNillable()) &&
                                s_wrapperMap.containsKey(javatype)) {
                            param.setAttribute("wrapped-primitive", "true");
                            param.setAttribute("value-method", javatype + "Value");
                            javatype = (String)s_wrapperMap.get(javatype);
                        } else {
                            param.setAttribute("wrapped-primitive", "false");
                            String dflt = element.getDefaultValue();
                            if (dflt == null) {
                                dflt = format.getDefaultText();
                                if (javatype.equals("float")) {
                                    dflt = dflt + 'F';
                                }
                            }
                            if (dflt != null) {
                                param.setAttribute("default", dflt);
                            }
                        }

                    } else {
                        
                        // conversion must be defined by mapping
                        MappingElementBase mapping;
                        if (refname == null) {

                            // complex type reference translates to abstract mapping in binding
                            mapping = (MappingElementBase)complexTypeMap.get(typename);
                            if (mapping == null) {
                                throw new RuntimeException("Cannot unwrap element " +
                                        qname + ": no abstract mapping definition found for type " +
                                        typename + " (used by element " + itemname + ')');
                            }
                            Integer tindex = (Integer)typeMappedClassMap.get(typename);
                            if (tindex == null) {
                                tindex = new Integer(typeMappedClassMap.size());
                                typeMappedClassMap.put(typename, tindex);
                            }
                            param.setAttribute("type-index", tindex.toString());
                            
                        } else {
                            
                            // element reference translates to concrete mapping
                            mapping = (MappingElementBase)elementMap.get(refname);
                            if (mapping == null) {
                                throw new RuntimeException("Cannot unwrap element " +
                                        qname + ": no concrete mapping definition found for element " +
                                        refname + " (used by element " + itemname + ')');
                            }
                            param.setAttribute("type-index", "");
                            
                        }

                        // configure based on the mapping information
                        param.setAttribute("form", "complex");
                        complex = true;
                        javatype = mapping.getClassName();
                        createtype = mapping.getCreateType();
                        if (createtype == null && mapping.isAbstract() &&
                            mapping.getExtensionTypes().isEmpty()) {
                            
                            // abstract mapping with no extensions requires instance
                            //  this assumes the mapped type can be created, but no easy way to check
                            createtype = javatype;
                        }

                        // merge contained namespace definitions into set for operation
                        Iterator citer = mapping.topChildIterator();
                        while (citer.hasNext()) {
                            ElementBase child = (ElementBase)citer.next();
                            if (child.type() == ElementBase.NAMESPACE_ELEMENT) {
                                dfltns = mapNamespace((NamespaceElement)child, dfltns, nsmap);
                            } else {
                                break;
                            }
                        }

                        // also merge namespace definitions from binding
                        BindingElement binding = (BindingElement)bindingMap.get(mapping);
                        citer = binding.topChildIterator();
                        while (citer.hasNext()) {
                            ElementBase child = (ElementBase)citer.next();
                            if (child.type() == ElementBase.NAMESPACE_ELEMENT) {
                                dfltns = mapNamespace((NamespaceElement)child, dfltns, nsmap);
                            } else if (child.type() != ElementBase.INCLUDE_ELEMENT) {
                                break;
                            }
                        }
                    }
                    param.setAttribute("java-type", javatype);
                    if (createtype != null) {
                        param.setAttribute("create-type", createtype);
                    }

                    boolean isobj = !s_primitiveSet.contains(javatype);
                    String fulltype = javatype;
                    if (isarray) {
                        fulltype += "[]";
                        isobj = false;
                    }
                    param.setAttribute("object", Boolean.toString(isobj));
                    if (isout) {
                        wrappertype = fulltype;
                    } else {
                        wrappertype = "java.lang.Object";
                    }
                    wrapdetail.appendChild(param);

                    // this magic code comes from org.apache.axis2.wsdl.codegen.extension.SchemaUnwrapperExtension
                    //  it's used here to fit into the ADB-based code generation model
                    QName partqname = WSDLUtil.getPartQName(opName.getLocalPart(),
                                                            WSDLConstants.INPUT_PART_QNAME_SUFFIX,
                                                            javaname);
                    partNameList.add(partqname);

                    // add type mapping so we look like ADB
                    codeGenConfig.getTypeMapper().addTypeMappingName(partqname, fulltype);
                }

                // check namespace prefix usage
                if (nons && dfltns) {
                    throw new RuntimeException("Cannot unwrap element " + qname +
                            ": no-namespace element(s) conflict with default namespace use in binding");
                }
                wrapdetail.setAttribute("uses-default", Boolean.toString(nons));

                // set flag for namespace declarations needed on wrapper
                wrapdetail.setAttribute("need-namespaces", Boolean.toString(complex));

            }

        } else if (type != null) {
            throw new RuntimeException("Cannot unwrap element " + qname +
                    ": not a complexType definition");
        }
        if (wrapdetail.getFirstChild() == null) {
            wrapdetail.setAttribute("empty", "true");
            wrapdetail.setAttribute("need-namespaces", "false");
            wrappertype = "";
        } else {
            wrapdetail.setAttribute("empty", "false");
        }

        // this magic code comes from org.apache.axis2.wsdl.codegen.extension.SchemaUnwrapperExtension
        //  it's used here to fit into the ADB-based code generation model
        MessagePartInformationHolder infoHolder = new MessagePartInformationHolder();
        infoHolder.setOperationName(msg.getAxisOperation().getName());
        infoHolder.setPartsList(partNameList);
        try {
            msg.addParameter(new Parameter(Constants.UNWRAPPED_DETAILS, infoHolder));
        } catch (AxisFault e) {
            throw new RuntimeException(e);
        }

        // set indication for unwrapped message
        try {
            msg.addParameter(new Parameter(Constants.UNWRAPPED_KEY, Boolean.TRUE));
        } catch (AxisFault e) {
            throw new RuntimeException(e);
        }

        // add fake mapping for wrapper name (necessary for current XSLTs)
        codeGenConfig.getTypeMapper().addTypeMappingName(qname, wrappertype);

        // return the unwrapping details
        return wrapdetail;
    }

    /**
     * Add mapping from namespace URI to prefix. In the case where multiple prefixes are used with a
     * single URI, this will preserve the last non-empty prefix for that URI.
     *
     * @param ns     namespace definition
     * @param dfltns flag for default namespace used in binding
     * @param nsmap  map from namespace URIs to prefixes
     * @return flag for default namespace used in binding
     */
    private boolean mapNamespace(NamespaceElement ns, boolean dfltns, Map nsmap) {
        String prefix = ns.getPrefix();
        if (prefix == null) {
            prefix = "";
        }
        String prior = (String)nsmap.get(ns.getUri());
        if (prior != null) {
            if (prefix.length() == 0) {
                return dfltns;
            } else if (prior.length() == 0) {
                dfltns = false;
            }
        }
        nsmap.put(ns.getUri(), prefix);
        return dfltns || prefix.length() == 0;
    }

    private static String toJavaName(String name, Set nameset) {
        StringBuffer buff = new StringBuffer(name.length());
        for (int i = 0; i < name.length(); i++) {
            char chr = name.charAt(i);
            if ((i == 0 && Character.isJavaIdentifierStart(chr)) ||
                    (i > 0 && Character.isJavaIdentifierPart(chr))) {
                buff.append(chr);
            } else if (chr == ':' || chr == '.') {
                buff.append('$');
            } else {
                buff.append('_');
            }
        }
        int count = 0;
        String jname = buff.toString();
        while (!nameset.add(jname)) {
            jname = buff.toString() + count++;
        }
        return jname;
    }

    private String mapMessage(AxisMessage msg, Map complexTypeMap) {
        QName qname = msg.getElementQName();
        if (qname == null) {
            throw new RuntimeException("No element reference in message " + msg.getName());
        }
        return mapQName(qname, complexTypeMap);
    }

    private String mapMessage(SOAPHeaderMessage msg, Map complexTypeMap) {
        QName qname = msg.getElement();
        if (qname == null) {
            throw new RuntimeException("No element reference in header");
        }
        return mapQName(qname, complexTypeMap);
    }

    private String mapQName(QName qname, Map complexTypeMap) throws RuntimeException {
        Object obj = complexTypeMap.get(qname);
        if (obj == null) {
            throw new RuntimeException("No mapping defined for element " + qname);
        }
        MappingElementBase mapping = (MappingElementBase)obj;
        String cname = mapping.getClassName();
        codeGenConfig.getTypeMapper().addTypeMappingName(qname, cname);
        return cname;
    }

    /**
     * Collect mapping from qnames to classes for top level mappings in JiBX binding.
     *
     * @param binding
     * @param dns            default namespace to be used unless overridden (empty string if none)
     * @param elementMap     map from element names to concrete mapping components of binding
     * @param complexTypeMap map from type names to abstract mapping components of binding
     * @param simpleTypeMap  map from type names to format definition components of binding
     * @param bindingMap     map from mapping components to containing binding definition
     */
    private static void collectTopLevelComponents(BindingElement binding,
                                                  String dns, Map elementMap, Map complexTypeMap,
                                                  Map simpleTypeMap,
                                                  Map bindingMap) {

        // check default namespace set at top level of binding
        String defaultns = findDefaultNS(binding.topChildIterator(), dns);

        // add all top level mapping and format definitions to maps
        for (Iterator iter = binding.topChildIterator(); iter.hasNext();) {
            ElementBase child = (ElementBase)iter.next();
            if (child.type() == ElementBase.INCLUDE_ELEMENT) {

                // recurse to process included binding definitions
                IncludeElement include = (IncludeElement)child;
                if(include.getBinding() != null) {
                    collectTopLevelComponents(include.getBinding(), defaultns,
                                            elementMap, complexTypeMap, simpleTypeMap, bindingMap);
                }

            } else if (child.type() == ElementBase.FORMAT_ELEMENT) {

                // register named formats as simple type conversions
                FormatElement format = (FormatElement)child;
                registerElement(format.getQName(), format, simpleTypeMap);
                bindingMap.put(format, binding);

            } else if (child.type() == ElementBase.MAPPING_ELEMENT) {
                
                // record only abstract mappings with type names, and mappings with names
                MappingElementBase mapping = (MappingElementBase)child;
                bindingMap.put(mapping, binding);
                if (mapping.isAbstract() && mapping.getTypeQName() != null) {

                    // register named abstract mappings as complex type conversions
                    registerElement(mapping.getTypeQName(), mapping,
                                    complexTypeMap);

                } else if (mapping.getName() != null) {

                    // register concrete mappings as element conversions
                    String uri = mapping.getUri();
                    if (uri == null) {
                        uri = findDefaultNS(mapping.topChildIterator(),
                                            defaultns);
                    }
                    elementMap.put(new QName(uri, mapping.getName()), mapping);
                }
            }
        }
    }

    /**
     * Register binding element by qualified name. This converts the qualified name format used by
     * the JiBX binding model to that used by Axis2.
     *
     * @param qname   qualified name in JiBX format (<code>null</code> if none)
     * @param element corresponding element of binding definition
     * @param map     qualified name to element map
     */
    private static void registerElement(org.jibx.runtime.QName qname,
                                        ElementBase element, Map map) {
        if (qname != null) {
            map.put(new QName(qname.getUri(), qname.getName()), element);
        }
    }

    /**
     * Find the default namespace within a list of JiBX binding model elements possibly including
     * namespace definitions. Once a non-namespace definition element is seen in the list, this just
     * returns (since the namespace definitions always come first in JiBX's binding format).
     *
     * @param iter iterator for elements in list
     * @param dns  default namespace if not overridden
     * @return default namespace
     */
    private static String findDefaultNS(Iterator iter, String dns) {
        while (iter.hasNext()) {
            ElementBase child = (ElementBase)iter.next();
            if (child.type() == ElementBase.NAMESPACE_ELEMENT) {
                NamespaceElement namespace = (NamespaceElement)child;
                String defaultName = namespace.getDefaultName();
                if ("elements".equals(defaultName) || "all".equals(defaultName)) {
                    return namespace.getUri();
                }
            } else {
                break;
            }
        }
        return dns;
    }

    /**
     * Inner class for handling prevalidation of include elements only. Unlike the normal JiBX
     * binding definition prevalidation step, this visitor ignores everything except include
     * elements.
     */
    private class IncludePrevalidationVisitor extends ModelVisitor {
        private final ValidationContext m_context;

        private IncludePrevalidationVisitor(ValidationContext vctx) {
            m_context = vctx;
        }

        /* (non-Javadoc)
        * @see org.jibx.binding.model.ModelVisitor#visit(org.jibx.binding.model.ElementBase)
        */
        public boolean visit(IncludeElement node) {
            try {
                // force creation of defintions context for containing binding
                m_context.getFormatDefinitions();
                node.prevalidate(m_context);
            } catch (Throwable t) {
                m_context.addFatal("Error during validation: " +
                        t.getMessage());
                t.printStackTrace();
                return false;
            }
            return true;
        }
    }

    private static class NamedParameterTypeMapper extends JavaTypeMapper {
        /**
         * Return the real parameter name, not a dummy.
         *
         * @param qname
         * @return local part of name
         */
        public String getParameterName(QName qname) {
            return qname.getLocalPart();
        }
    }
}
