# StaxValueDeserializer.java

    /*
     * 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.qi4j.valueserialization.stax;

    import java.io.InputStream;
    import java.io.StringReader;
    import java.io.StringWriter;
    import java.math.BigDecimal;
    import java.math.BigInteger;
    import java.util.Collection;
    import java.util.Map;
    import javax.xml.stream.XMLEventReader;
    import javax.xml.stream.XMLInputFactory;
    import javax.xml.stream.XMLStreamException;
    import javax.xml.stream.events.XMLEvent;
    import javax.xml.transform.Transformer;
    import javax.xml.transform.TransformerFactory;
    import javax.xml.transform.dom.DOMResult;
    import javax.xml.transform.stream.StreamSource;
    import org.apache.commons.lang.StringEscapeUtils;
    import org.qi4j.api.injection.scope.Service;
    import org.qi4j.api.injection.scope.Structure;
    import org.qi4j.api.service.ServiceReference;
    import org.qi4j.api.structure.Application;
    import org.qi4j.api.structure.Module;
    import org.qi4j.api.value.ValueDeserializer;
    import org.qi4j.api.value.ValueSerializationException;
    import org.qi4j.functional.Function;
    import org.qi4j.spi.value.ValueDeserializerAdapter;
    import org.w3c.dom.Document;
    import org.w3c.dom.Element;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;

    /**
     * ValueDeserializer reading Values from XML documents.
     */
    public class StaxValueDeserializer
        extends ValueDeserializerAdapter<XMLEventReader, Node>
    {

        private final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
        private final TransformerFactory transformerFactory = TransformerFactory.newInstance();

        public StaxValueDeserializer( @Structure Application application,
                                      @Structure Module module,
                                      @Service ServiceReference<ValueDeserializer> serviceRef )
        {
            super( application, module, serviceRef );
            // Input Factory setup
            inputFactory.setProperty( "javax.xml.stream.isValidating", Boolean.FALSE );
            inputFactory.setProperty( "javax.xml.stream.isNamespaceAware", Boolean.FALSE );
            inputFactory.setProperty( "javax.xml.stream.supportDTD", Boolean.FALSE );
            inputFactory.setProperty( "javax.xml.stream.isCoalescing", Boolean.TRUE );
            inputFactory.setProperty( "javax.xml.stream.isReplacingEntityReferences", Boolean.FALSE );
        }

        @Override
        protected XMLEventReader adaptInput( InputStream input )
            throws Exception
        {
            return inputFactory.createXMLEventReader( input, "UTF-8" );
        }

        @Override
        protected Object readPlainValue( XMLEventReader input )
            throws Exception
        {
            if( !input.hasNext() )
            {
                return null;
            }
            XMLEvent nextEvent = input.nextEvent();
            if( nextEvent.getEventType() == XMLEvent.START_ELEMENT
                && "null".equals( nextEvent.asStartElement().getName().getLocalPart() ) )
            {
                input.nextTag();
                return null;
            }
            if( nextEvent.getEventType() != XMLEvent.CHARACTERS )
            {
                throw new ValueSerializationException( "Expected characters but got: " + nextEvent );
            }
            String stringValue = nextEvent.asCharacters().getData();
            return detectAndConvertStringValue( stringValue );
        }

        @Override
        protected <T> Collection<T> readArrayInCollection( XMLEventReader input,
                                                           Function<XMLEventReader, T> deserializer,
                                                           Collection<T> collection )
            throws Exception
        {
            if( !input.hasNext() )
            {
                return null;
            }
            XMLEvent nextTag = input.nextTag();
            if( nextTag.isStartElement() && "null".equals( nextTag.asStartElement().getName().getLocalPart() ) )
            {
                input.nextTag();
                return null;
            }
            if( !nextTag.isStartElement() || !"array".equals( nextTag.asStartElement().getName().getLocalPart() ) )
            {
                throw new ValueSerializationException( "Expected an <array/> but got: " + nextTag );
            }
            WHILE:
            while( input.hasNext() )
            {
                XMLEvent currentTag = input.nextTag();
                if( currentTag.isEndElement() )
                {
                    String endElementName = currentTag.asEndElement().getName().getLocalPart();
                    switch( endElementName )
                    {
                        case "array":
                            break WHILE;
                        case "value":
                            continue;
                    }
                }
                if( !"value".equals( currentTag.asStartElement().getName().getLocalPart() ) )
                {
                    throw new ValueSerializationException( "Expected a <value/> but got: " + currentTag );
                }
                T item = deserializer.map( input );
                collection.add( item );
            }
            return collection;
        }

        @Override
        protected <K, V> Map<K, V> readMapInMap( XMLEventReader input,
                                                 Function<XMLEventReader, K> keyDeserializer,
                                                 Function<XMLEventReader, V> valueDeserializer,
                                                 Map<K, V> map )
            throws Exception
        {
            if( !input.hasNext() )
            {
                return null;
            }
            XMLEvent nextTag = input.nextTag();
            if( nextTag.isStartElement() && "null".equals( nextTag.asStartElement().getName().getLocalPart() ) )
            {
                input.nextTag();
                return null;
            }
            if( !nextTag.isStartElement() || !"array".equals( nextTag.asStartElement().getName().getLocalPart() ) )
            {
                throw new ValueSerializationException( "Expected an <array/> but got: " + nextTag );
            }
            XMLEvent currentTag = input.nextTag(); // <object>
            while( !currentTag.isEndElement() || !"array".equals( currentTag.asEndElement().getName().getLocalPart() ) )
            {
                if( !currentTag.isStartElement() || !"object".equals( currentTag.asStartElement().getName().getLocalPart() ) )
                {
                    throw new ValueSerializationException( "Expected an <object/> but got: " + nextTag );
                }
                currentTag = input.nextTag(); // <field>
                K key = null;
                V value = null;
                while( !currentTag.isEndElement() || !"object".equals( currentTag.asEndElement().getName().getLocalPart() ) )
                {
                    input.nextTag(); // <name>
                    String keyOrValue = input.nextEvent().asCharacters().getData();
                    input.nextTag(); // </name>
                    input.nextTag(); // <value>
                    switch( keyOrValue )
                    {
                        case "key":
                            key = keyDeserializer.map( input );
                            break;
                        case "value":
                            value = valueDeserializer.map( input );
                            break;
                        default:
                            readObjectTree( input );
                            break;
                    }
                    input.nextTag(); // </value>
                    input.nextTag(); // </field>
                    currentTag = input.nextTag();
                }
                if( key != null )
                {
                    map.put( key, value );
                }
                currentTag = input.nextTag();
            }
            return map;
        }

        @Override
        protected Node readObjectTree( XMLEventReader input )
            throws Exception
        {
            XMLEvent peek = input.peek();
            if( peek.isStartElement() && "null".equals( peek.asStartElement().getName().getLocalPart() ) )
            {
                input.nextTag();// <null>
                input.nextTag();// </null>
                return null;
            }
            String elementBody = readElementBody( input );
            Transformer transformer = transformerFactory.newTransformer();
            DOMResult domResult = new DOMResult();
            transformer.transform( new StreamSource( new StringReader( elementBody ) ), domResult );
            return ( (Document) domResult.getNode() ).getDocumentElement();

        }

        private static String readElementBody( XMLEventReader input )
            throws XMLStreamException
        {
            StringWriter buf = new StringWriter( 1024 );
            int depth = 0;
            while( input.hasNext() )
            {
                // peek event
                XMLEvent xmlEvent = input.peek();
                if( xmlEvent.isStartElement() )
                {
                    ++depth;
                }
                else if( xmlEvent.isEndElement() )
                {
                    --depth;
                    // reached END_ELEMENT tag?
                    // break loop, leave event in stream
                    if( depth < 0 )
                    {
                        break;
                    }
                }
                // consume event
                xmlEvent = input.nextEvent();
                // print out event
                xmlEvent.writeAsEncodedUnicode( buf );
            }
            return buf.getBuffer().toString();
        }

        @Override
        protected Object asSimpleValue( Node inputNode )
            throws Exception
        {
            if( inputNode == null )
            {
                return null;
            }
            if( inputNode.getNodeType() == Node.ELEMENT_NODE && "null".equals( inputNode.getLocalName() ) )
            {
                return null;
            }
            if( inputNode.getNodeType() != Node.TEXT_NODE && inputNode.getNodeType() != Node.CDATA_SECTION_NODE )
            {
                throw new ValueSerializationException( "Expected a TEXT or CDATA node but got " + inputNode );
            }
            String stringValue = inputNode.getNodeValue();
            return detectAndConvertStringValue( stringValue );
        }

        @Override
        protected boolean isObjectValue( Node inputNode )
            throws Exception
        {
            if( inputNode == null )
            {
                return false;
            }
            if( "object".equals( inputNode.getLocalName() ) )
            {
                return true;
            }
            if( !"value".equals( inputNode.getLocalName() ) )
            {
                return false;
            }
            return getDirectChildNode( inputNode, "object" ) != null;
        }

        @Override
        protected boolean objectHasField( Node inputNode, String key )
            throws Exception
        {
            if( inputNode == null )
            {
                return false;
            }
            Node objectNode;
            if( "value".equals( inputNode.getLocalName() ) )
            {
                objectNode = getDirectChildNode( inputNode, "object" );
            }
            else
            {
                objectNode = inputNode;
            }
            if( objectNode == null )
            {
                return false;
            }
            if( !"object".equals( objectNode.getLocalName() ) )
            {
                throw new ValueSerializationException( "Expected an object value but got: " + objectNode );
            }
            return getObjectFieldNode( objectNode, key ) != null;
        }

        @Override
        @SuppressWarnings( "unchecked" )
        protected <T> T getObjectFieldValue( Node inputNode,
                                             String key,
                                             Function<Node, T> valueDeserializer )
            throws Exception
        {
            if( inputNode == null )
            {
                return null;
            }
            Node objectNode;
            if( "value".equals( inputNode.getLocalName() ) )
            {
                objectNode = getDirectChildNode( inputNode, "object" );
            }
            else
            {
                objectNode = inputNode;
            }
            if( objectNode == null )
            {
                return null;
            }
            if( !"object".equals( objectNode.getLocalName() ) )
            {
                throw new ValueSerializationException( "Expected an object value but got: " + objectNode );
            }
            Node fieldNode = getObjectFieldNode( objectNode, key );
            if( fieldNode == null )
            {
                return null;
            }
            Node valueElement = getDirectChildNode( fieldNode, "value" );
            Node valueNode = valueElement.getFirstChild();
            if( valueNode == null )
            {
                return (T) "";
            }
            if( valueNode.getNodeType() == Node.ELEMENT_NODE && "null".equals( valueNode.getLocalName() ) )
            {
                return null;
            }
            T value = valueDeserializer.map( valueNode );
            return value;
        }

        @Override
        protected <T> void putArrayNodeInCollection( Node inputNode,
                                                     Function<Node, T> deserializer,
                                                     Collection<T> collection )
            throws Exception
        {
            if( inputNode == null )
            {
                return;
            }
            if( !( inputNode instanceof Element ) )
            {
                throw new ValueSerializationException( "Expected an Element but got " + inputNode );
            }
            NodeList arrayValues = inputNode.getChildNodes();
            for( int arrayValuesIndex = 0; arrayValuesIndex < arrayValues.getLength(); arrayValuesIndex++ )
            {
                Node arrayValue = arrayValues.item( arrayValuesIndex );
                T value = deserializer.map( arrayValue.getFirstChild() );
                collection.add( value );
            }
        }

        @Override
        protected <K, V> void putArrayNodeInMap( Node inputNode,
                                                 Function<Node, K> keyDeserializer,
                                                 Function<Node, V> valueDeserializer, Map<K, V> map )
            throws Exception
        {
            if( inputNode == null )
            {
                return;
            }
            if( !"array".equals( inputNode.getLocalName() ) )
            {
                throw new ValueSerializationException( "Expected an <array/> but got " + inputNode );
            }
            NodeList entriesNodes = inputNode.getChildNodes();
            for( int idx = 0; idx < entriesNodes.getLength(); idx++ )
            {
                Node entryNode = entriesNodes.item( idx );
                K key = getObjectFieldValue( entryNode, "key", keyDeserializer );
                V value = getObjectFieldValue( entryNode, "value", valueDeserializer );
                if( key != null )
                {
                    map.put( key, value );
                }
            }
        }

        @Override
        protected <V> void putObjectNodeInMap( Node inputNode,
                                               Function<Node, V> valueDeserializer,
                                               Map<String, V> map )
            throws Exception
        {
            if( inputNode == null )
            {
                return;
            }
            if( !"object".equals( inputNode.getLocalName() ) )
            {
                throw new ValueSerializationException( "Expected an <object/> but got " + inputNode );
            }
            NodeList fieldsNodes = inputNode.getChildNodes();
            for( int idx = 0; idx < fieldsNodes.getLength(); idx++ )
            {
                Node fieldNode = fieldsNodes.item( idx );
                String key = getDirectChildNode( fieldNode, "name" ).getTextContent();
                if( key != null && key.length() > 0 )
                {
                    V value = getObjectFieldValue( inputNode, key, valueDeserializer );
                    map.put( key, value );
                }
            }
        }

        @SuppressWarnings( "AssignmentToMethodParameter" )
        private Object detectAndConvertStringValue( String stringValue )
        {
            if( stringValue == null || stringValue.length() == 0 )
            {
                return "";
            }
            stringValue = StringEscapeUtils.unescapeXml( stringValue );
            if( stringValue.matches( "[+-]?\\d+(\\.\\d+)?([eE][+-]?\\d+(\\.\\d+)?)?" ) )
            {
                if( stringValue.indexOf( '.' ) != -1 )
                {
                    return new BigDecimal( stringValue );
                }
                return new BigInteger( stringValue );
            }
            if( "true".equalsIgnoreCase( stringValue ) || "false".equalsIgnoreCase( stringValue ) )
            {
                return Boolean.parseBoolean( stringValue );
            }
            return stringValue;
        }

        private static Node getObjectFieldNode( Node inputNode, String key )
        {
            if( inputNode == null )
            {
                return null;
            }
            if( !( inputNode instanceof Element ) )
            {
                throw new ValueSerializationException( "Excpected an Element but got " + inputNode );
            }
            NodeList fieldNodes = inputNode.getChildNodes();
            for( int idx = 0; idx < fieldNodes.getLength(); idx++ )
            {
                Node fieldNode = fieldNodes.item( idx );
                Node nameNode = getDirectChildNode( fieldNode, "name" );
                if( nameNode != null && key.equals( nameNode.getTextContent() ) )
                {
                    return fieldNode;
                }
            }
            return null;
        }

        private static Node getDirectChildNode( Node parent, String name )
        {
            for( Node child = parent.getFirstChild(); child != null; child = child.getNextSibling() )
            {
                if( name.equals( child.getNodeName() ) )
                {
                    return child;
                }
            }
            return null;
        }
    }