/*
 * 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.datasource.jaxb;



import java.util.ArrayList;
import java.util.List;

import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * In some cases, we want to marshal an array of objects as a series
 * of elements instead of a single element containing items.
 * 
 * Unfortunately there is no way to tell the JAXB marshal method to 
 * marshal an array or List as a series of elements.   
 * 
 * Instead, we use the JAXB marshal method to output a single element
 * with items and then filter the write events to transform it into
 * a series of elements.
 * 
 * Thus
 *     <myElement>
 *        <item>Hello</item>
 *        <item>World</item>
 *     </myElement>
 * Becomes
 *     <myElement>Hello</myElement>
 *     <myElement>World</myElement>
 *     
 * Special care is taken to ensure that namespace declarations are property preserved.
 */
public class XMLStreamWriterArrayFilter implements XMLStreamWriter {
    private static final Log log = LogFactory.getLog(XMLStreamWriterArrayFilter.class);
    XMLStreamWriter writer;
    int depth = 0;
    boolean isBuffering = true;
    List<List<String>> bufferedCommands = new ArrayList<List<String>>();
    
    // The extensive debug was used during development but is not necessary in production.
    // Change DEBUG_ENABLED = log.isDebugEnabled();
    // to get extensive debug output.
    private static boolean DEBUG_ENABLED = false; 
    
    private static final String XSI_URI =  "http://www.w3.org/2001/XMLSchema-instance";
   

    public XMLStreamWriterArrayFilter(XMLStreamWriter writer) 
        throws XMLStreamException {
        super();
        if (DEBUG_ENABLED) {
            log.debug("XMLStreamWriterArrayFilter " + writer);
        }
        this.writer = writer;
    }
    
    public void close() throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("close");
        }
        if (writer != null) {
            writer.close();
        }
    }

    public void flush() throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("flush");
        }
        if (writer != null) {
            writer.flush();
        }
    }

    public NamespaceContext getNamespaceContext() {  
        if (DEBUG_ENABLED) {
            log.debug("getNamespaceContext");
        }
        return writer.getNamespaceContext();
    }

    public String getPrefix(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("getPrefix " + arg0);
        }
        return writer.getPrefix(arg0);
    }

    public Object getProperty(String arg0) throws IllegalArgumentException {
        if (DEBUG_ENABLED) {
            log.debug("getProperty " + arg0);
        }
        return writer.getProperty(arg0);
    }

    public void setDefaultNamespace(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("setDefaultNamespace " + arg0);
        }
        writer.setDefaultNamespace(arg0);
    }

    public void setNamespaceContext(NamespaceContext arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("setNamespaceContext " + arg0);
        }
        writer.setNamespaceContext(arg0);
    }

    public void setPrefix(String arg0, String arg1) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("setPrefix " + arg0 + " " + arg1);
        }
        writer.setPrefix(arg0, arg1);
    }

    public void writeAttribute(String prefix, String uri, String localName, String value) 
    throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeAttribute " + prefix + " " + uri + " " + localName + " " + value);
        }
        writer.writeAttribute(prefix, uri, localName, value);
    }

    public void writeAttribute(String arg0, String arg1, String arg2) 
        throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeAttribute " + arg0 + " " + arg1 + " " + arg2 );
        }
        writer.writeAttribute(arg0, arg1, arg2);
    }

    public void writeAttribute(String arg0, String arg1) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeAttribute " + arg0 + " " + arg1);
        }
        writer.writeAttribute(arg0, arg1);
    }

    public void writeCData(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeCData " + arg0 );
        }
        writer.writeCData(arg0);
    }

    public void writeCharacters(char[] arg0, int arg1, int arg2) 
    throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeCharacters " + arg0 + " " + arg1 + " " + arg2 );
        }
        writer.writeCharacters(arg0, arg1, arg2);
    }

    public void writeCharacters(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeCharacters " + arg0);
        }
        writer.writeCharacters(arg0);
    }

    public void writeComment(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeComment " + arg0 );
        }
        writer.writeComment(arg0);
    }

    public void writeDefaultNamespace(String uri) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeDefaultNamespace (" + uri + ")"  );
        }
      
        if (isBuffering) {
            if (DEBUG_ENABLED) {
                log.debug("  Supress writeDefaultNamespace on top element");
            }
        } else if (depth >= 2) {
            writer.writeDefaultNamespace(uri);
        }
    }

    public void writeDTD(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeDTD " + arg0  );
        }
        writer.writeDTD(arg0);
    }

    public void writeEmptyElement(String arg0, String arg1, String arg2) 
    throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeEmptyElement" + arg0 + " " + arg1 + " " + arg2 );
        }
        writeStartElement(arg0, arg1, arg2);
        writeEndElement();
    }

    public void writeEmptyElement(String arg0, String arg1) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeEmptyElement " + arg0 + " " + arg1);
        }
        writeStartElement(arg0, arg1);
        writeEndElement();
    }

    public void writeEmptyElement(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeEmptyElement " + arg0 );
        }
        writeStartElement(arg0);
        writeEndElement();
    }

    public void writeEndDocument() throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeEndDocument " );
        }
        writer.writeEndDocument();
    }

    public void writeEndElement() throws XMLStreamException {
        depth--;
        if (DEBUG_ENABLED) {
            log.debug("writeEndElement "  );
        }
        if (depth != 0) {
            writer.writeEndElement();
        } else {
            if (DEBUG_ENABLED) {
                log.debug("  Suppress writeEndElement for the top element..but performing a flush");
            }
            writer.flush();
        }
    }

    public void writeEntityRef(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeEntityRef " + arg0 );
        }
        writer.writeEntityRef(arg0);
    }

    public void writeNamespace(String prefix, String uri) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeNamespace (" + prefix + ") (" + uri + ")" );
        }
        
        // The namespaces are buffered while reading the root element.
        // They will be written out by writeCommands when the child
        // elements are encountered.
        if (isBuffering) {
            if ("".equals(prefix)) {
                if (DEBUG_ENABLED) {
                    log.debug("  Supress default write namespace on top element to avoid collision");
                }
            } else {
                List<String> command = new ArrayList<String>();
                command.add("writeNamespace");
                command.add(prefix);
                command.add(uri);
                bufferCommand(command);
            }
        } else if (depth == 2 && XSI_URI.equals(uri)  && "xsi".equals(prefix)) {
            // The operation element already has an xsi namespace declaration;
            // thus this one is redundant and only makes the message larger.
            if (DEBUG_ENABLED) {
                log.debug("  Supressing xsi namespace declaration on array item");
            }
        } else if (depth >= 2) {
            writer.writeNamespace(prefix, uri);
        } 
    }

    public void writeProcessingInstruction(String arg0, String arg1) 
    throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeProcessingInstruction " + arg0 + " " + arg1  );
        }
        writer.writeProcessingInstruction(arg0, arg1);
    }

    public void writeProcessingInstruction(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeProcessingInstruction " + arg0);
        }
        writer.writeProcessingInstruction(arg0);
    }

    public void writeStartDocument() throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeStartDocument " );
        }
        writer.writeStartDocument();
    }

    public void writeStartDocument(String arg0, String arg1) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeStartDocument " + arg0 + " " + arg1 );
        }
        writer.writeStartDocument(arg0, arg1);
    }

    public void writeStartDocument(String arg0) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeStartDocument " + arg0  );
        }
        writer.writeStartDocument(arg0);
    }

    public void writeStartElement(String arg0, String arg1, String arg2) 
    throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeStartElement " + arg0 + " " + arg1 + " " + arg2 );
        }
        depth++;
        if (depth > 1) {
            isBuffering = false;
        }
        
        // The start element is buffered when we encounter the
        // outermost element.  The buffered event is written 
        // (instead of the child element tag).
        if (isBuffering) {
            List<String> command = new ArrayList<String>();
            command.add("writeStartElement");
            command.add(arg0);
            command.add(arg1);
            command.add(arg2);
            bufferCommand(command);
        } else if (depth == 2) {
            writeCommands();
        } else {
            writer.writeStartElement(arg0, arg1, arg2);
        }
        
    }

    public void writeStartElement(String arg0, String arg1) throws XMLStreamException {
        if (DEBUG_ENABLED) {
            log.debug("writeStartElement " + arg0 + " " + arg1);
        }
        depth++;
        if (depth > 1) {
            isBuffering = false;
        }
        
        // The start element is buffered when we encounter the
        // outermost element.  The buffered event is written 
        // (instead of the child element tag).
        if (isBuffering) {
            List<String> command = new ArrayList<String>();
            command.add("writeStartElement");
            command.add(arg0);
            command.add(arg1);
            bufferCommand(command);
        } else if (depth == 2) {
            writeCommands();
        } else {
            writer.writeStartElement(arg0, arg1);
        }
    }

    public void writeStartElement(String arg0) throws XMLStreamException {
        
        if (DEBUG_ENABLED) {
            log.debug("writeStartElement " + arg0 );
        }
        depth++;
        if (depth > 1) {
            isBuffering = false;
        }
        
        
        // The start element is buffered when we encounter the
        // outermost element.  The buffered event is written 
        // (instead of the child element tag).
        if (isBuffering) {
            List<String> command = new ArrayList<String>();
            command.add("writeStartElement");
            command.add(arg0);
            bufferCommand(command);
        } else if (depth == 2) {
            writeCommands();
        } else {
            writer.writeStartElement(arg0);
        }
    }
    
    void bufferCommand(List<String> command) {
        if (DEBUG_ENABLED) {
            log.debug("  Buffering command " + command);
        }
        bufferedCommands.add(command);
    }
    
    void writeCommands() throws XMLStreamException {
        for (int i=0;i<bufferedCommands.size(); i++) {
            List<String> command = bufferedCommands.get(i);
            String m = command.get(0);
            if ("writeNamespace".equals(m)) {
                if (DEBUG_ENABLED) {
                    log.debug("  Sending buffered writeNamespace " + command.get(1) + 
                            "  " + command.get(2));
                }
                writer.writeNamespace(command.get(1), command.get(2));
            } else if ("writeStartElement".equals(m)) {
                int len = command.size();
                if (len == 2) {
                    if (DEBUG_ENABLED) {
                        log.debug("  Sending buffered writeStartElement " + command.get(1));
                    }
                    writer.writeStartElement(command.get(1));
                } else if (len ==3) {
                    if (DEBUG_ENABLED) {
                        log.debug("  Sending buffered writeStartElement " + command.get(1) +
                                "  " + command.get(2));
                    }
                    writer.writeStartElement(command.get(1), command.get(2));
                } else if (len == 4) {
                    if (DEBUG_ENABLED) {
                        log.debug("  Sending buffered writeStartElement " + command.get(1) +
                                "  " + command.get(2) + " " + command.get(3));
                    }
                    writer.writeStartElement(command.get(1), command.get(2), command.get(3));
                }
            }
        }
    }      
    
}
