/*
 * 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.rmi.databind;

import org.apache.axis2.rmi.Configurator;
import org.apache.axis2.rmi.exception.MetaDataPopulateException;
import org.apache.axis2.rmi.exception.SchemaGenerationException;
import org.apache.axis2.rmi.exception.XmlParsingException;
import org.apache.axis2.rmi.metadata.AttributeField;
import org.apache.axis2.rmi.metadata.ElementField;
import org.apache.axis2.rmi.metadata.Operation;
import org.apache.axis2.rmi.metadata.Parameter;
import org.apache.axis2.rmi.metadata.Type;
import org.apache.axis2.rmi.metadata.impl.TypeImpl;
import org.apache.axis2.rmi.types.MapType;
import org.apache.axis2.rmi.util.Constants;
import org.apache.axis2.rmi.util.JavaTypeToQNameMap;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Calendar;
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;

public class XmlStreamParser {

    private Configurator configurator;
    private Map qNameToTypeMap;
    private SimpleTypeHandler simpleTypeHandler;
    private Class simpleTypeHandlerClass;
    private boolean readerPriorAccess;
    private boolean priorAccessDetermined = false;

    public XmlStreamParser(Map processedTypeMap,
                           Configurator configurator,
                           Map schemaMap) {
        this.configurator = configurator;
        this.simpleTypeHandler = this.configurator.getSimpleTypeHandler();
        this.simpleTypeHandlerClass = this.simpleTypeHandler.getClass();
        try {
            populateQNameToTypeMap(processedTypeMap, schemaMap);
        } catch (MetaDataPopulateException e) {
            // TODO: what to do this exceptions is not going to happen
        } catch (SchemaGenerationException e) {
            // TODO: what to do this exceptions is not going to happen
        }
    }

    private void populateQNameToTypeMap(Map processedTypeMap, Map schemaMap)
            throws MetaDataPopulateException,
            SchemaGenerationException {
        // first add all the proceced type map values
        Set defaultTypeMapKeys = JavaTypeToQNameMap.getKeys();
        Class typeClass;
        for (Iterator iter = defaultTypeMapKeys.iterator(); iter.hasNext();) {
            typeClass = (Class) iter.next();
            if (!processedTypeMap.containsKey(typeClass)) {
                Type newType = new TypeImpl(typeClass);
                processedTypeMap.put(typeClass, newType);
                newType.populateMetaData(this.configurator, processedTypeMap);
                newType.generateSchema(this.configurator, schemaMap);
            }
        }
        this.qNameToTypeMap = new HashMap();
        Type type = null;
        for (Iterator iter = processedTypeMap.values().iterator(); iter.hasNext();) {
            type = (Type) iter.next();
            this.qNameToTypeMap.put(type.getXmlType().getQname(), type);
        }

    }

    public Object getOutputObject(XMLStreamReader reader_in,
                                  Operation operation)
            throws XMLStreamException, XmlParsingException {
        Object returnObject = null;
        
        determinePriorAccess(reader_in);
        StatefulXMLStreamReader reader = getStatefulReader(reader_in);
        
        advanceToFirstElement(reader);

        // first check whether we have got the correct input element or not
        if (reader.getLocalName().equals(operation.getOutPutElement().getName()) &&
                reader.getNamespaceURI().equals(operation.getOutPutElement().getNamespace())) {
            reader.next();
            if (operation.getOutputParameter() != null) {
                // i.e this is not a void return type
                returnObject = getObjectForParameter(reader, operation.getOutputParameter());
            }
        }
        return returnObject;
    }

    public Object[] getInputParameters(XMLStreamReader reader_in,
                                       Operation operation)
            throws XMLStreamException,
            XmlParsingException {

        determinePriorAccess(reader_in);
        StatefulXMLStreamReader reader = getStatefulReader(reader_in);
        
        List returnObjects = new ArrayList();
        advanceToFirstElement(reader);

        // first check whether we have got the correct input element or not
        if (reader.getLocalName().equals(operation.getInputElement().getName()) &&
                reader.getNamespaceURI().equals(operation.getInputElement().getNamespace())) {
            // point the reader to parameters
            reader.next();
            Parameter parameter = null;
            List inputParameters = operation.getInputParameters();
            QName parameterQName = null;
            int startDepth = reader.getDepth();
            
            for (Iterator iter = inputParameters.iterator(); iter.hasNext();) {
                parameter = (Parameter) iter.next();
                parameterQName = new QName(parameter.getNamespace(), parameter.getName());
                returnObjects.add(getObjectForParameter(reader, parameter));
                // if the reader is at the end of this parameter
                // then we move it to next element.
                if (reader.isEndElement() && reader.getName().equals(parameterQName)
                        && (readerPriorAccess || reader.getDepth() == startDepth - 1)){
                    reader.next();
                }
            }

        } else {
            throw new XmlParsingException("Unexpected Subelement " + reader.getName() + " but " +
                    "expected " + operation.getInputElement().getName());
        }
        return returnObjects.toArray();
    }

    /**
     * parameter has the same logic as the attribute. so reader pre and post conditions same.
     *
     * @param reader
     * @param parameter
     * @return
     * @throws XMLStreamException
     * @throws XmlParsingException
     */

    public Object getObjectForParameter(XMLStreamReader reader_in,
                                        Parameter parameter)
            throws XMLStreamException,
            XmlParsingException {

        QName parameterQName = new QName(parameter.getNamespace(), parameter.getName());
        
        determinePriorAccess(reader_in);
        StatefulXMLStreamReader reader = getStatefulReader(reader_in);
        
        return getObjectForElement(reader,
                parameterQName,
                parameter.getType(),
                parameter.isArray(),
                parameter.getElement().isNillable(),
                parameter.getElement().isMinOccurs0(),
                parameter.getClassType(),
                parameter.getJavaClass());

    }

    /**
     * when calls to this method reader must point to the start of the type.
     * if it is a simple type it points the the text
     * if it is a complex type it points to the start element of the first attribute. and we returning
     * from the method it should point to the end element of the last parameter.
     *
     * @param reader
     * @param type
     * @return
     * @throws XMLStreamException
     * @throws XmlParsingException
     */


    public Object getObjectForType(XMLStreamReader reader_in,
                                   Type type)
            throws XMLStreamException,
            XmlParsingException {

        try {
            determinePriorAccess(reader_in);
            StatefulXMLStreamReader reader = getStatefulReader(reader_in);
            
            Object returnObject = null;
            if (type.getXmlType().isSimpleType()) {
                // this is a simple known type for us
                // constructor should be able to invoke with the string.
                if (type.getJavaClass().equals(Object.class)) {
                    returnObject = new Object();
                } else {
                    // find the object for this string using converter util classes
                    returnObject = getSimpleTypeObject(type, reader, reader.getText());
                }
            } else {
                // first we have to point to the reader to the beginning for the element
                while (!reader.isStartElement() && !reader.isEndElement()) {
                    reader.next();
                }
                int startDepth = reader.getDepth();
                // this is a complex type
                returnObject = type.getJavaClass().newInstance();
                // we have to get all the elementField and populate them
                List elementFields = type.getAllElementFields();
                ElementField elementField;
                Object elementFieldObject;
                QName elementFieldQName = null;

                for (Iterator iter = elementFields.iterator(); iter.hasNext();) {
                    elementField = (ElementField) iter.next();
                    elementFieldObject = getObjectForElementField(reader, elementField);
                    elementFieldQName = new QName(elementField.getNamespace(), elementField.getName());
                    if (elementFieldObject != null) {
                        elementField.getSetterMethod().invoke(returnObject, new Object[]{elementFieldObject});
                    }
                    // if the reader is at the end of this elementField
                    // then we move it to next element.
                    if (reader.isEndElement() && reader.getName().equals(elementFieldQName) 
                            && (readerPriorAccess || reader.getDepth() == startDepth - 1)){
                        reader.next();
                    }

                }

            }
            return returnObject;
        } catch (InstantiationException e) {
            throw new XmlParsingException("Constructor invoking exception for type " + type.getName());
        } catch (IllegalAccessException e) {
            throw new XmlParsingException("Constructor invoking exception for type " + type.getName());
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            throw new XmlParsingException("Constructor invoking exception for type " + type.getName());
        }

    }

    private Object getSimpleTypeObject(Type type, StatefulXMLStreamReader reader, String value)
            throws  XmlParsingException {
        Object returnObject;
        String methodName = null;
        try {
            methodName = getMethodName(type.getJavaClass().getName());
            Method  methodToInvoke = this.simpleTypeHandlerClass.getMethod(methodName,new Class[]{String.class});
            returnObject = methodToInvoke.invoke(this.simpleTypeHandler,new Object[]{value});
        } catch (NoSuchMethodException e) {
            throw new XmlParsingException("Can not invoke the converter util class method " + methodName, e);
        } catch (IllegalAccessException e) {
            throw new XmlParsingException("Can not invoke the converter util class method " + methodName, e);
        } catch (InvocationTargetException e) {
            throw new XmlParsingException("Can not invoke the converter util class method " + methodName, e);
        }
        return returnObject;
    }

    private String getMethodName(String className) {
        // first handle some exceptional classes
        if (className.equals(Integer.class.getName())) {
            return "convertToInt";
        } else if (className.equals(Calendar.class.getName())) {
            return "convertToDateTime";
        }
        if (className.indexOf(".") > -1) {
            className = className.substring(className.lastIndexOf(".") + 1);
        } else {
            // this is a primitive type class
            // so capitalize the first letter
            className = className.substring(0, 1).toUpperCase() + className.substring(1);
        }
        return "convertTo" + className;
    }

    /**
     * give the relevant object for elementField.
     *
     * @param reader
     * @param elementField
     * @return
     * @throws XMLStreamException
     * @throws XmlParsingException
     */

    private Object getObjectForElementField(StatefulXMLStreamReader reader,
                                            ElementField elementField)
            throws XMLStreamException,
            XmlParsingException {
        QName elementFieldQName = new QName(elementField.getNamespace(), elementField.getName());
        return getObjectForElement(reader,
                elementFieldQName,
                elementField.getType(),
                elementField.isArray(),
                elementField.getElement().isNillable(),
                elementField.getElement().isMinOccurs0(),
                elementField.getClassType(),
                elementField.getPropertyDescriptor().getPropertyType());

    }

    /**
     * element parser corresponds to parse the element (for an attribute or parameter) correctly.
     * when calling to this method reader must point to the start element of the element. and when returning
     * it should point to the corresponding end element of the element.
     * if the element is an array it may point to the start element of the next element.
     *
     * @param reader
     * @param elementQName
     * @param elementType
     * @param isArray
     * @param classType
     * @param javaClass - if this is a list javaClass for the list
     * @return
     * @throws XMLStreamException
     * @throws XmlParsingException
     */

    private Object getObjectForElement(StatefulXMLStreamReader reader,
                                       QName elementQName,
                                       Type elementType,
                                       boolean isArray,
                                       boolean isNillable,
                                       boolean isMinOccurs0,
                                       int classType,
                                       Class javaClass)
            throws XMLStreamException,
            XmlParsingException {
        // first we have to point to the reader to the beginning for the element
        while (!reader.isStartElement() && !reader.isEndElement()) {
            reader.next();
        }

        // first validate the attribute
        if (reader.getName().equals(elementQName)) {

            String nillble = reader.getAttributeValue(Constants.URI_DEFAULT_SCHEMA_XSI, "nil");
            // actual element type may be different from the element type given
            // if extensions has used.
            Type actualElementType = elementType;
            QName typeQName = getTypeQName(reader);
            if ((typeQName != null) && !elementType.getXmlType().getQname().equals(typeQName)) {
                // i.e this is an extension type
                if (this.qNameToTypeMap.containsKey(typeQName)) {
                    actualElementType = (Type) this.qNameToTypeMap.get(typeQName);
                } else {
                    throw new XmlParsingException("Unknown type found ==> " + typeQName);
                }
            }
            // point to the complex type elements
            if (isArray) {
                Collection objectsCollection = getCollectionObject(classType, javaClass);

                // read the first element
                if ("true".equals(nillble) || "1".equals(nillble)) {
                    // this is a nill attribute
                    while (!reader.isEndElement()) {
                        reader.next();
                    }
                    if (isNillable){
                       objectsCollection.add(null);
                    } else {
                       throw new XmlParsingException("Element " + elementQName + " can not be null");
                    }

                } else {

                    Object returnObject = getElementObjectFromReader(actualElementType, reader);
                    objectsCollection.add(returnObject);

                    // we have to move the cursor until the end element of this attribute
                    while (!reader.isEndElement() || !reader.getName().equals(elementQName)) {
                        reader.next();
                    }
                }
                boolean loop = true;
                while (loop) {
                    while (!reader.isEndElement()) {
                        reader.next();
                    }
                    reader.next();
                    // now we are at the end element of the first element
                    while (!reader.isStartElement() && !reader.isEndElement()) {
                        reader.next();
                    }

                    // in this step if it is an end element we have found an end element
                    // so have to exit from the loop
                    if (reader.isEndElement()) {
                        loop = false;
                    } else {
                        // now it should be in a start element
                        // check whether still we read the original element attributes. otherwise return
                        if (reader.getName().equals(elementQName)) {
                            nillble = reader.getAttributeValue(Constants.URI_DEFAULT_SCHEMA_XSI, "nil");
                            // since this is a new element we check for extensions
                            actualElementType = elementType;
                            typeQName = getTypeQName(reader);
                            if ((typeQName != null) && !elementType.getXmlType().getQname().equals(typeQName)) {
                                // i.e this is an extension type
                                if (this.qNameToTypeMap.containsKey(typeQName)) {
                                    actualElementType = (Type) this.qNameToTypeMap.get(typeQName);
                                } else {
                                    throw new XmlParsingException("Unknown type found ==> " + typeQName);
                                }
                            }
                            if ("true".equals(nillble) || "1".equals(nillble)) {
                                // this is a nill attribute
                                while (!reader.isEndElement()) {
                                    reader.next();
                                }
                                if (isNillable) {
                                    objectsCollection.add(null);
                                } else {
                                    throw new XmlParsingException("Element " + elementQName + " can not be null");
                                }
                            } else {
                                Object returnObject = getElementObjectFromReader(actualElementType, reader);
                                objectsCollection.add(returnObject);

                                // we have to move the cursor until the end element of this attribute
                                while (!reader.isEndElement() || !reader.getName().equals(elementQName)) {
                                    reader.next();
                                }
                            }
                        } else {
                            loop = false;
                        }

                    }
                }

                // this is very important in handling primitives
                // they can not have null values so if array then we have to return the
                // array object is null
                // for other also it is convenient to assume like that.
                if ((Constants.OTHER_TYPE & classType) == Constants.OTHER_TYPE){
                    // i.e this is not a collection type
                    List objectsList = (List) objectsCollection;
                    if ((objectsCollection.size() == 0) ||
                            ((objectsCollection.size() == 1) && (objectsList.get(0) == null))) {
                        return null;
                    } else {
                        // create an array with the original element type
                        Object objectArray = Array.newInstance(elementType.getJavaClass(), objectsCollection.size());
                        for (int i = 0; i < objectsCollection.size(); i++) {
                            Array.set(objectArray, i, objectsList.get(i));
                        }
                        return objectArray;
                    }
                } else if ((Constants.COLLECTION_TYPE & classType) == Constants.COLLECTION_TYPE){
                     if ((objectsCollection.size() == 0) ||
                             ((objectsCollection.size() == 1) && (objectsCollection.iterator().next() == null))){
                         return null;
                     } else {
                         return objectsCollection;
                     }

                } else if ((Constants.MAP_TYPE & classType) == Constants.MAP_TYPE){

                    if ((objectsCollection.size() == 0) ||
                            ((objectsCollection.size() == 1) && (objectsCollection.iterator().next() == null))) {
                        return null;

                    } else {
                        List mapObjectsList = (List) objectsCollection;
                        MapType mapType = null;
                        Map mapObject = null;
                        if (javaClass.isInterface()) {
                            mapObject = new HashMap();
                        } else {
                            try {
                                mapObject = (Map) javaClass.newInstance();
                            } catch (InstantiationException e) {
                                throw new XmlParsingException("Can not instantiate the java class " + javaClass.getName(), e);
                            } catch (IllegalAccessException e) {
                                throw new XmlParsingException("Can not instantiate the java class " + javaClass.getName(), e);
                            }

                        }
                        for (Iterator iter = mapObjectsList.iterator(); iter.hasNext();) {
                            mapType = (MapType) iter.next();
                            mapObject.put(mapType.getKey(), mapType.getValue());
                        }

                        return mapObject;
                    }

                } else {
                    throw new XmlParsingException("Unknow class type " + classType);
                }

            } else {
                if ("true".equals(nillble) || "1".equals(nillble)) {
                    // this is a nill attribute
                    while (!reader.isEndElement()) {
                        reader.next();
                    }
                    reader.next();
                    if (isNillable) {
                        return null;
                    } else {
                        throw new XmlParsingException("Element " + elementQName + " can not be null");
                    }

                } else {

                    Object returnObject = getElementObjectFromReader(actualElementType, reader);
                    // we have to move the cursor until the end element of this attribute
                    while (!reader.isEndElement() || !reader.getName().equals(elementQName)) {
                        reader.next();
                    }
                    return returnObject;
                }
            }
        } else {
            if (isMinOccurs0) {
                return null;
            } else {
                throw new XmlParsingException("Unexpected Subelement " + reader.getName() + " but " +
                        "expected " + elementQName.getLocalPart());
            }
        }
    }

    private Object getElementObjectFromReader(Type elementType,
                                              StatefulXMLStreamReader reader)
            throws XmlParsingException, XMLStreamException {
        Object returnObject = null;
        if (RMIBean.class.isAssignableFrom(elementType.getJavaClass())) {
            // this is an rmi bean
            // so invoke the static parse method
            try {
                Method parseMethod = elementType.getJavaClass().getMethod("parse", new Class[]{XMLStreamReader.class, XmlStreamParser.class});
                returnObject = parseMethod.invoke(null, new Object[]{reader, this});
            } catch (NoSuchMethodException e) {
                throw new XmlParsingException("parse method has not been implemented correctly for the rmi bean "
                        + elementType.getJavaClass().getName(), e);
            } catch (IllegalAccessException e) {
                throw new XmlParsingException("can not access parse method of the rmi bean "
                        + elementType.getJavaClass().getName(), e);
            } catch (InvocationTargetException e) {
                throw new XmlParsingException("can not invoke parse method of the rmi bean "
                        + elementType.getJavaClass().getName(), e);
            }
        } else {
            // read the attributes.
            Map javaMethodToValueMap = getJavaMethodValueHashMap(elementType, reader);
            reader.next();
            returnObject = getObjectForType(reader, elementType);
            populateObjectAttributes(javaMethodToValueMap, returnObject);
        }

        return returnObject;
    }

    private void populateObjectAttributes(Map javaMethodToValueMap, Object returnObject) throws XmlParsingException {
        Method javaMehtod;
        try {
            for (Iterator iter = javaMethodToValueMap.keySet().iterator();iter.hasNext();){
                javaMehtod = (Method) iter.next();
                javaMehtod.invoke(returnObject,new Object[]{javaMethodToValueMap.get(javaMehtod)});
            }
        } catch (IllegalAccessException e) {
            throw new XmlParsingException("Can not set the attribute value");
        } catch (InvocationTargetException e) {
            throw new XmlParsingException("Can not set the attribute value");
        }
    }

    private Map getJavaMethodValueHashMap(Type actualElementType, StatefulXMLStreamReader reader)
            throws XmlParsingException {
        AttributeField attributeField;
        String attributeVlaue;
        Object attributeObject;
        Map javaMethodToValueMap = new HashMap();
        for (Iterator iter = actualElementType.getAllAttributeFields().iterator();iter.hasNext();){
            attributeField = (AttributeField) iter.next();
            attributeVlaue = reader.getAttributeValue(attributeField.getNamespace(),
                                                    attributeField.getName());
            if (attributeVlaue != null) {
                attributeObject = getSimpleTypeObject(attributeField.getType(), reader, attributeVlaue);
                javaMethodToValueMap.put(attributeField.getSetterMethod(), attributeObject);
            } else if (attributeField.isRequried()){
                throw new XmlParsingException("Required attribute " + attributeField.getName() + " is missing");
            }

        }
        return javaMethodToValueMap;
    }

    /**
     * returns the collection object according to the type.
     * @param classType
     * @param javaClass
     * @return
     * @throws XmlParsingException
     */

    private Collection getCollectionObject(int classType,
                                           Class javaClass)
            throws XmlParsingException {

        Collection objectsCollection = null;
        try {
            if ((Constants.OTHER_TYPE & classType) == Constants.OTHER_TYPE) {
                // i.e this is not a list or a map
                objectsCollection = new ArrayList();
            } else {
                if (javaClass.isInterface()) {
                    if ((Constants.LIST_TYPE & classType) == Constants.LIST_TYPE) {
                       objectsCollection = new ArrayList();
                    } else if ((Constants.SET_TYPE & classType) == Constants.SET_TYPE) {
                       objectsCollection = new HashSet();
                    } else if ((Constants.COLLECTION_TYPE & classType) == Constants.COLLECTION_TYPE) {
                       objectsCollection = new ArrayList();
                    } else if ((Constants.MAP_TYPE & classType) == Constants.MAP_TYPE) {
                       objectsCollection = new ArrayList();
                    }
                } else {
                    if ((Constants.COLLECTION_TYPE & classType) == Constants.COLLECTION_TYPE) {
                        objectsCollection = (Collection) javaClass.newInstance();
                    } else if ((Constants.MAP_TYPE & classType) == Constants.MAP_TYPE) {
                        objectsCollection = new ArrayList();
                    }
                }
            }

        } catch (InstantiationException e) {
            throw new XmlParsingException("Problem with instanciating the element class " +
                        javaClass.getName() , e);
        } catch (IllegalAccessException e) {
            throw new XmlParsingException("Problem with instanciating the element class " +
                        javaClass.getName() , e);
        }
        return objectsCollection;
    }

    /**
     * reader must be at the start of the element
     *
     * @param reader
     * @return qName for the type attribute
     */
    private QName getTypeQName(StatefulXMLStreamReader reader) {
        QName typeQName = null;
        String typeValue = reader.getAttributeValue(Constants.URI_2001_SCHEMA_XSI, "type");
        if ((typeValue != null) && !typeValue.equals("")) {
            int index = typeValue.indexOf(":");
            String nsPrefix = "";
            if (index > -1) {
                nsPrefix = typeValue.substring(0, index);
            }

            String localPart = typeValue.substring(index + 1);
            typeQName = new QName(reader.getNamespaceURI(nsPrefix), localPart);
        }
        return typeQName;
    }

    /** 
     * Determine if the reader has been advanced beyond its initial location, prior 
     * to the methods within XmlStreamParser being called.
     * 
     * @param reader The XMLStreamReader on which the XmlStreamParser is acting.
     * @return true if the reader has been advanced beyond its initial location, false otherwise
     */
    private boolean determinePriorAccess(XMLStreamReader reader) {
        if (!priorAccessDetermined) {
            if (reader.getLocation().getCharacterOffset() == 0)
                readerPriorAccess = false;
            else
                readerPriorAccess = true;

            priorAccessDetermined = true;
        }
        return readerPriorAccess;
    }

    /**
     * Provide a mechanism (a stack) to track the current state of the XMLStream
     * 
     * @param reader_in the XMLStreamReader that the XmlStreamParser is acting on
     * @return a StatefulXMLStreamReader to monitor the state of the XMLStreamReader
     */
    private StatefulXMLStreamReader getStatefulReader(XMLStreamReader reader_in) {
        if (reader_in instanceof StatefulXMLStreamReader)            
            return (StatefulXMLStreamReader) reader_in;
        else
            return new StatefulXMLStreamReader(reader_in, readerPriorAccess);
    }
    
    /**
     * Advance the reader to point to the beginning of the element
     * @param reader the StatefulXMLStreamReader we are using to parse the XMLStream
     */
    
    private void advanceToFirstElement(StatefulXMLStreamReader reader) throws XMLStreamException {
    
        if (readerPriorAccess) {
            while (!reader.isStartElement() && !reader.isEndElement() && reader.hasNext()) {
                reader.next();
            }
        } else {
            while (reader.getDepth() == 0 && reader.hasNext()) {
                reader.next();
            }
        }
    }
}
