/*******************************************************************************
 * Copyright (c) 2011, 2013 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Blaise Doughan - 2.4 - initial implementation
 ******************************************************************************/
package org.eclipse.persistence.oxm.record;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.List;

import javax.xml.namespace.QName;

import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.core.helper.CoreClassConstants;
import org.eclipse.persistence.internal.core.helper.CoreConversionManager;
import org.eclipse.persistence.internal.oxm.CharacterEscapeHandler;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.NamespaceResolver;
import org.eclipse.persistence.internal.oxm.ObjectBuilder;
import org.eclipse.persistence.internal.oxm.Root;
import org.eclipse.persistence.internal.oxm.XMLBinaryDataHelper;
import org.eclipse.persistence.internal.oxm.XMLMarshaller;
import org.eclipse.persistence.internal.oxm.XPathFragment;
import org.eclipse.persistence.internal.oxm.mappings.Descriptor;
import org.eclipse.persistence.internal.oxm.record.ExtendedContentHandler;
import org.eclipse.persistence.internal.oxm.record.XMLFragmentReader;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;

/**
 * <p>Use this type of MarshalRecord when the marshal target is a Writer and the
 * JSON should not be formatted with carriage returns or indenting.</p>
 * <p><code>
 * XMLContext xmlContext = new XMLContext("session-name");<br>
 * XMLMarshaller xmlMarshaller = xmlContext.createMarshaller();<br>
 * JSONRecord jsonWriterRecord = new JSONWriterRecord();<br>
 * jsonWriterRecord.setWriter(myWriter);<br>
 * xmlMarshaller.marshal(myObject, jsonWriterRecord);<br>
 * </code></p>
 * <p>If the marshal(Writer) and setMediaType(MediaType.APPLICATION_JSON) and
 * setFormattedOutput(false) method is called on XMLMarshaller, then the Writer
 * is automatically wrapped in a JSONWriterRecord.</p>
 * <p><code>
 * XMLContext xmlContext = new XMLContext("session-name");<br>
 * XMLMarshaller xmlMarshaller = xmlContext.createMarshaller();<br>
 * xmlMarshaller.setMediaType(MediaType.APPLICATION_JSON);
 * xmlMarshaller xmlMarshaller.setFormattedOutput(false);<br>
 * xmlMarshaller.marshal(myObject, myWriter);<br>
 * </code></p>
 * @see org.eclipse.persistence.oxm.XMLMarshaller
 */
public class JSONWriterRecord extends MarshalRecord<XMLMarshaller> {

    protected boolean isProcessingCData = false;
    protected static final String NULL="null";
    protected String attributePrefix;
    protected boolean charactersAllowed = false;
    protected CharsetEncoder encoder;
    protected CharacterEscapeHandler characterEscapeHandler;
    protected String callbackName;
    protected Output writer;
    protected Level level;

    public JSONWriterRecord(){
        super();        
    }

    public JSONWriterRecord(OutputStream outputStream) {
        this();
        writer = new OutputStreamOutput(outputStream);
    }

    public JSONWriterRecord(OutputStream outputStream, String callbackName){
        this(outputStream);
        setCallbackName(callbackName);       
    }

    public JSONWriterRecord(Writer writer){
    	this();
    	setWriter(writer);
    }
    
    public JSONWriterRecord(Writer writer, String callbackName){
    	this(writer);
    	setCallbackName(callbackName);    	 
    }

    public void setCallbackName(String callbackName){
    	this.callbackName = callbackName;
    }
    
    /**
     * INTERNAL:
     */
    public void setMarshaller(XMLMarshaller marshaller) {
        super.setMarshaller(marshaller);
        attributePrefix = marshaller.getAttributePrefix();
        encoder = Charset.forName(marshaller.getEncoding()).newEncoder();
        if (marshaller.getValueWrapper() != null) {
            textWrapperFragment = new XPathFragment();
            textWrapperFragment.setLocalName(marshaller.getValueWrapper());
        }
        characterEscapeHandler = marshaller.getCharacterEscapeHandler();
        writer.setMarshaller(marshaller);
    }

        
    /**
     * Handle marshal of an empty collection.  
     * @param xPathFragment
     * @param namespaceResolver
     * @param openGrouping if grouping elements should be marshalled for empty collections
     * @return
     */    
    public boolean emptyCollection(XPathFragment xPathFragment, NamespaceResolver namespaceResolver, boolean openGrouping) {    	
    	 if(marshaller.isMarshalEmptyCollections()){  
    		 super.emptyCollection(xPathFragment, namespaceResolver, true);
            if (null != xPathFragment) {
                startCollection();
                if (!xPathFragment.isSelfFragment()) {
                    openStartElement(xPathFragment, namespaceResolver);
                    if (null != level) {
                        level.setNeedToCloseComplex(false);
                        level.setNeedToOpenComplex(false);
                    }
                    endElement(xPathFragment, namespaceResolver);
                }
                endEmptyCollection();
            }
    		 return true;
    	 }else{
    		 return super.emptyCollection(xPathFragment, namespaceResolver, openGrouping);
    	 }
    }
    
    public void forceValueWrapper(){
        charactersAllowed = false;
    }
    
    /**
     * Return the Writer that the object will be marshalled to.
     * @return The marshal target.
     */
    public Writer getWriter() {
        return writer.getWriter();
    }

    /**
     * Set the Writer that the object will be marshalled to.
     * @param writer The marshal target.
     */
    public void setWriter(Writer writer) {
        this.writer = new WriterOutput(writer);
    }

    public void namespaceDeclaration(String prefix, String namespaceURI){
    }
    
    public void defaultNamespaceDeclaration(String defaultNamespace){
    }
    
    /**
     * INTERNAL:
     */
    public void startDocument(String encoding, String version) {
        try {
             if(null != level) {
                 if(level.isFirst()) {
                     level.setFirst(false);
                 } else {
                	 writeListSeparator();                     
                 }
             }else if(callbackName != null){
            	 startCallback();              
             }
             level = new Level(true, false, level);
             
             writer.write('{');
         } catch (IOException e) {
            throw XMLMarshalException.marshalException(e);
        }
    }
    
    protected void writeListSeparator() throws IOException{
        writer.write(',');
    }
    
    protected void writeSeparator() throws IOException{        
        writer.write(Constants.COLON);        
    }
    
    /**
     * INTERNAL:
     * @throws IOException 
     */
    protected void startCallback() throws IOException{
       if(callbackName != null){
          writer.write(callbackName);
          writer.write('(');
       }
    }

    /**
     * INTERNAL:
     */
    public void endDocument() {
        try {
        	closeComplex();
        	if(null != level && null == level.getPreviousLevel()){
        		endCallback();
        	}            
            level = level.getPreviousLevel();
        } catch (IOException e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    /**
     * INTERNAL:
     */
    public void openStartElement(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) {
        try {
             if(level.isFirst()) {
                 level.setFirst(false);
             } else {
                 writer.write(',');                    
             }
            if(xPathFragment.nameIsText()){
                if(level.isCollection() && level.isEmptyCollection()) {                	
                    writer.write('[');                    
                    level.setEmptyCollection(false);
                    level.setNeedToOpenComplex(false);
                    charactersAllowed = true;                    
                    level = new Level(true, true, level);
                    return;
                }
            }
            
            if(level.needToOpenComplex){
                   writer.write('{');
                   level.needToOpenComplex = false;
                   level.needToCloseComplex = true;
           }
          
           //write the key unless this is a a non-empty collection
           if(!(level.isCollection() && !level.isEmptyCollection())){
        	   writeKey(xPathFragment);
        	   //if it is the first thing in the collection also add the [
    		   if(level.isCollection() && level.isEmptyCollection()){
                    writer.write('[');
               	    level.setEmptyCollection(false);        		   
        	   }
           }
     		    
            
            charactersAllowed = true;
            level = new Level(true, true, level);
        } catch (IOException e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    /**
     * INTERNAL:
     */
    public void element(XPathFragment frag) {
    }

    /**
     * INTERNAL:
     */
    public void attribute(String namespaceURI, String localName, String qName, String value) {
        XPathFragment xPathFragment = new XPathFragment();
        xPathFragment.setNamespaceURI(namespaceURI);
        xPathFragment.setAttribute(true);
        xPathFragment.setLocalName(localName);

        openStartElement(xPathFragment, namespaceResolver);
        characters(null, value, null, false, true);
        endElement(xPathFragment, namespaceResolver);
    }

    /**
     * INTERNAL:
     */
    public void attribute(XPathFragment xPathFragment, NamespaceResolver namespaceResolver, String value) {
        attribute(xPathFragment, namespaceResolver, value, null);
    }

    /**
     * INTERNAL:
     * override so we don't iterate over namespaces when startPrefixMapping doesn't do anything
     */
    public void startPrefixMappings(NamespaceResolver namespaceResolver) {
    }

    /**
     * INTERNAL:
     * override so we don't iterate over namespaces when endPrefixMapping doesn't do anything
     */
    public void endPrefixMappings(NamespaceResolver namespaceResolver) {
    }

    /**
     * INTERNAL:
     */
    public void closeStartElement() {}

    /**
     * INTERNAL:
     */
    public void endElement(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) {
        try{
            if(null != level) {
                if(level.needToOpenComplex){
                    writer.write('{');
                    closeComplex();
                } else if(level.needToCloseComplex){
                    closeComplex();
                }
                charactersAllowed = false;
                level = level.getPreviousLevel();
            }
        } catch (IOException e) {
             throw XMLMarshalException.marshalException(e);
        }
    }

    protected void closeComplex() throws IOException {
        writer.write('}');
    }

    @Override
    public void startCollection() {
        if(null == level) {
            try {
            	startCallback();
                writer.write('[');
                level = new Level(true, false, level);
            } catch(IOException e) {
                throw XMLMarshalException.marshalException(e);
            }
        } else {
            level.setCollection(true);
            level.setEmptyCollection(true);
            charactersAllowed = false;
        }
    }
    
    protected void endEmptyCollection(){
    	endCollection();
    }

    protected void endCallback() throws IOException{
    	if(callbackName != null){
     	   writer.write(')');
     	   writer.write(';');
        }
    }
    
    @Override
    public void endCollection() {
        try {
            if(level != null && null == level.getPreviousLevel()) {
                writer.write(']');
                endCallback();
            } else {
                if(level != null && level.isCollection() && !level.isEmptyCollection()) {
                    writer.write(']');
                }
            }
            level.setCollection(false);
        } catch (IOException e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    /**
     * INTERNAL:
     */
    public void characters(String value) {
    	characters(value, true, false);
    }
    /**
     * INTERNAL:
     */
     public void characters(String value, boolean isString, boolean isAttribute) {
    	  boolean textWrapperOpened = false;
          if(!charactersAllowed){    
        	   if(textWrapperFragment != null){
        		   openStartElement(textWrapperFragment, namespaceResolver);
        		   textWrapperOpened = true;
        	   }
    	   }
    	 
           level.setNeedToOpenComplex(false);
           try {
        	   if(isString){
                      writer.write('"');
                      writeValue(value, isAttribute);
                      writer.write('"');
        	   }else{
        		   writer.write(value);
        	   }
        	     
           } catch (IOException e) {
               throw XMLMarshalException.marshalException(e);
           }        
           if(textWrapperOpened){    
          	   if(textWrapperFragment != null){
               	   endElement(textWrapperFragment, namespaceResolver);
          	   }
      	   }           
     }

     public void attribute(XPathFragment xPathFragment, NamespaceResolver namespaceResolver,  Object value, QName schemaType){
         if(xPathFragment.getNamespaceURI() != null && xPathFragment.getNamespaceURI() == javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI){
             return;
         }
         xPathFragment.setAttribute(true);
         openStartElement(xPathFragment, namespaceResolver);
         characters(schemaType, value, null, false, true);
         endElement(xPathFragment, namespaceResolver);
     }

     public void characters(QName schemaType, Object value, String mimeType, boolean isCDATA){    	   
        characters(schemaType, value, mimeType, isCDATA, false);
     }
     
     public void characters(QName schemaType, Object value, String mimeType, boolean isCDATA, boolean isAttribute) {
         if(mimeType != null) {
        	 if(value instanceof List){
         		value = XMLBinaryDataHelper.getXMLBinaryDataHelper().getBytesListForBinaryValues(//
                         (List)value, marshaller, mimeType);
         	}else{

             value = XMLBinaryDataHelper.getXMLBinaryDataHelper().getBytesForBinaryValue(//
                     value, marshaller, mimeType).getData();
         	}
         }         
         if(schemaType != null && Constants.QNAME_QNAME.equals(schemaType)){
             String convertedValue = getStringForQName((QName)value);
             characters((String)convertedValue);
         } else if(value.getClass() == String.class){        	
             //if schemaType is set and it's a numeric or boolean type don't treat as a string
             if(schemaType != null && isNumericOrBooleanType(schemaType)){
                 String convertedValue = ((String) ((ConversionManager) session.getDatasourcePlatform().getConversionManager()).convertObject(value, CoreClassConstants.STRING, schemaType));
                 characters(convertedValue, false, isAttribute);
             }else if(isCDATA){
                 cdata((String)value);
             }else{
                 characters((String)value);
             }
        }else{
            ConversionManager conversionManager = getConversionManager();
            String convertedValue = (String) conversionManager.convertObject(value, CoreClassConstants.STRING, schemaType);
            Class theClass = conversionManager.javaType(schemaType);

            if(schemaType == null || theClass == null){
                if(value.getClass() == CoreClassConstants.BOOLEAN || CoreClassConstants.NUMBER.isAssignableFrom(value.getClass())){
                	characters(convertedValue, false, isAttribute);
                }else{
                    characters(convertedValue);

                }
            }else if(schemaType != null && !isNumericOrBooleanType(schemaType)){
                //if schemaType exists and is not boolean or number do write quotes
                characters(convertedValue);
            } else if(isCDATA){
                cdata(convertedValue);
            }else{
            	characters(convertedValue, false, isAttribute);
            }
        }
         charactersAllowed = false;
       
     }

     
     private boolean isNumericOrBooleanType(QName schemaType){
         if(schemaType == null){
             return false;
         }else if(schemaType.equals(Constants.BOOLEAN_QNAME)
                 || schemaType.equals(Constants.INTEGER_QNAME)
                 || schemaType.equals(Constants.INT_QNAME)
                 || schemaType.equals(Constants.BYTE_QNAME)
                 || schemaType.equals(Constants.DECIMAL_QNAME)
                 || schemaType.equals(Constants.FLOAT_QNAME)
                 || schemaType.equals(Constants.DOUBLE_QNAME)
                 || schemaType.equals(Constants.SHORT_QNAME)
                 || schemaType.equals(Constants.LONG_QNAME)
                 || schemaType.equals(Constants.NEGATIVE_INTEGER_QNAME)
                 || schemaType.equals(Constants.NON_NEGATIVE_INTEGER_QNAME)
                 || schemaType.equals(Constants.NON_POSITIVE_INTEGER_QNAME)
                 || schemaType.equals(Constants.POSITIVE_INTEGER_QNAME)
                 || schemaType.equals(Constants.UNSIGNED_BYTE_QNAME)
                 || schemaType.equals(Constants.UNSIGNED_INT_QNAME)
                 || schemaType.equals(Constants.UNSIGNED_LONG_QNAME)
                 || schemaType.equals(Constants.UNSIGNED_SHORT_QNAME)
         ){
             return true;
         }
         return false;
     }

    /**
     * INTERNAL:
     */
     public void namespaceDeclarations(NamespaceResolver namespaceResolver) {
     }

    /**
     * INTERNAL:
     */
     public void nilComplex(XPathFragment xPathFragment, NamespaceResolver namespaceResolver){
         XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver);
         closeStartGroupingElements(groupingFragment);
         openStartElement(xPathFragment, namespaceResolver);
         characters(NULL, false, false);
         endElement(xPathFragment, namespaceResolver);
     }

    /**
     * INTERNAL:
     */
     public void nilSimple(NamespaceResolver namespaceResolver){
         XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver);         
         characters(NULL, false, false);
         closeStartGroupingElements(groupingFragment);
     }

     /**
      * Used when an empty simple value should be written
      * @since EclipseLink 2.4
      */
     public void emptySimple(NamespaceResolver namespaceResolver){
         nilSimple(namespaceResolver);
     }
     
     public void emptyAttribute(XPathFragment xPathFragment,NamespaceResolver namespaceResolver){
         XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver);
         openStartElement(xPathFragment, namespaceResolver);
         characters(NULL, false, false);
         endElement(xPathFragment, namespaceResolver);
         closeStartGroupingElements(groupingFragment);
     }

     /**
      * Used when an empty complex item should be written
      * @since EclipseLink 2.4
      */
     public void emptyComplex(XPathFragment xPathFragment, NamespaceResolver namespaceResolver){
         XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver);
         closeStartGroupingElements(groupingFragment);
         openStartElement(xPathFragment, namespaceResolver);
         endElement(xPathFragment, namespaceResolver);
     }

     /**
      * INTERNAL:
      */
     @Override
     public void marshalWithoutRootElement(ObjectBuilder treeObjectBuilder, Object object, Descriptor descriptor, Root root, boolean isXMLRoot){
    	 if(treeObjectBuilder != null){
             addXsiTypeAndClassIndicatorIfRequired(descriptor, null, descriptor.getDefaultRootElementField(), root, object, isXMLRoot, true);
             treeObjectBuilder.marshalAttributes(this, object, session);
    	 }         
      }
      
    /**
     * INTERNAL:
     */
    public void cdata(String value) {
        characters(value);
    }

    /**
     * INTERNAL:
	 * The character used to separate the prefix and uri portions when namespaces are present 
     * @since 2.4
     */
    public char getNamespaceSeparator(){    	
    	return marshaller.getNamespaceSeparator();
    }
    
    /**
     * INTERNAL:
     * The optional fragment used to wrap the text() mappings
     * @since 2.4
     */
    public XPathFragment getTextWrapperFragment() {
        return textWrapperFragment;
    }

    protected void writeKey(XPathFragment xPathFragment) throws IOException {
        super.openStartElement(xPathFragment, namespaceResolver);
        writer.write('"');
        if(xPathFragment.isAttribute() && attributePrefix != null){
            writer.writeAttributePrefix();
        }

        if(isNamespaceAware()){
            if(xPathFragment.getNamespaceURI() != null){
                String prefix = null;
                if(getNamespaceResolver() !=null){
                    prefix = getNamespaceResolver().resolveNamespaceURI(xPathFragment.getNamespaceURI());
                } else if(namespaceResolver != null){
                    prefix = namespaceResolver.resolveNamespaceURI(xPathFragment.getNamespaceURI());
                }
                if(prefix != null && !prefix.equals(Constants.EMPTY_STRING)){
                    writer.write(prefix);
                    writer.writeNamespaceSeparator();
                }
            }
        }

        writer.writeLocalName(xPathFragment);
        writer.write('"');
        
        writeSeparator();
    }

    /**
     * INTERNAL:
     */
    protected void writeValue(String value, boolean isAttribute) {
        try {        	        	   
               if (characterEscapeHandler != null) {
                   writer.writeResultFromCharEscapeHandler(value, isAttribute);
                   return;
               }
        	
        	  char[] chars = value.toCharArray();
              for (int x = 0, charsSize = chars.length; x < charsSize; x++) {
                  char character = chars[x];
                  switch (character){
                      case '"' : {
                          writer.write("\\\"");
                          break;
                      }
                      case '\b': {
                          writer.write("\\b");
                          break;
                      }
                      case '\f': {
                          writer.write("\\f");
                          break;
                      }
                      case '\n': {
                          writer.write("\\n");
                          break;
                      }
                      case '\r': {
                          writer.write("\\r");
                          break;
                      }
                      case '\t': {
                          writer.write("\\t");
                          break;
                      }
                      case '\\': {
                          writer.write("\\\\");
                          break;
                      }
                      default: {
                          if(Character.isISOControl(character) || !encoder.canEncode(character)){
                              writer.write("\\u");
                              String hex = Integer.toHexString(character).toUpperCase();
                              for(int i=hex.length(); i<4; i++){
                                  writer.write("0");
                              }
                              writer.write(hex);
                          }else{
                              writer.write(character);
                          }
                      }
                  }
              }
        } catch (IOException e) {
            throw XMLMarshalException.marshalException(e);
        }
    }
    
    protected String getStringForQName(QName qName){
        if(null == qName) {
            return null;
        }
        CoreConversionManager xmlConversionManager = getSession().getDatasourcePlatform().getConversionManager();

        return (String) xmlConversionManager.convertObject(qName, String.class);       
    }

    /**
     * Receive notification of a node.
     * @param node The Node to be added to the document
     * @param namespaceResolver The NamespaceResolver can be used to resolve the
     * namespace URI/prefix of the node
     */
    public void node(Node node, NamespaceResolver namespaceResolver, String uri, String name) {
        if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
            Attr attr = (Attr) node;
            String resolverPfx = null;
            if (getNamespaceResolver() != null) {
                resolverPfx = this.getNamespaceResolver().resolveNamespaceURI(attr.getNamespaceURI());
            }
            String namespaceURI = attr.getNamespaceURI();
            // If the namespace resolver contains a prefix for the attribute's URI,
            // use it instead of what is set on the attribute
            if (resolverPfx != null) {
                attribute(attr.getNamespaceURI(), Constants.EMPTY_STRING, resolverPfx+Constants.COLON+attr.getLocalName(), attr.getNodeValue());
            } else {
                attribute(attr.getNamespaceURI(), Constants.EMPTY_STRING, attr.getName(), attr.getNodeValue());
                // May need to declare the URI locally
                if (attr.getNamespaceURI() != null) {
                    attribute(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, Constants.EMPTY_STRING, javax.xml.XMLConstants.XMLNS_ATTRIBUTE + Constants.COLON + attr.getPrefix(), attr.getNamespaceURI());
                    this.getNamespaceResolver().put(attr.getPrefix(), attr.getNamespaceURI());
                }
            }
        } else if (node.getNodeType() == Node.TEXT_NODE) {
        	characters(node.getNodeValue(), false, false);
        } else {
            try {
            	JSONWriterRecordContentHandler wrcHandler = new JSONWriterRecordContentHandler();
            	
                XMLFragmentReader xfragReader = new XMLFragmentReader(namespaceResolver);
                xfragReader.setContentHandler(wrcHandler);
                xfragReader.setProperty("http://xml.org/sax/properties/lexical-handler", wrcHandler);
                xfragReader.parse(node, uri, name);
            } catch (SAXException sex) {
                throw XMLMarshalException.marshalException(sex);
            }
        }
    }

    @Override
    public boolean isWrapperAsCollectionName() {
        return marshaller.isWrapperAsCollectionName();
    }

    @Override
    public void flush() {
        try {
            writer.flush();
        } catch(IOException e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    /**
     * This class will typically be used in conjunction with an XMLFragmentReader.
     * The XMLFragmentReader will walk a given XMLFragment node and report events
     * to this class - the event's data is then written to the enclosing class'
     * writer.
     *
     * @see org.eclipse.persistence.internal.oxm.record.XMLFragmentReader
     */
    protected class JSONWriterRecordContentHandler implements ExtendedContentHandler, LexicalHandler {

    	JSONWriterRecordContentHandler() {
        }

        // --------------------- CONTENTHANDLER METHODS --------------------- //
        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
            	XPathFragment xPathFragment = new XPathFragment(localName);
            	xPathFragment.setNamespaceURI(namespaceURI);
            	openStartElement(xPathFragment, namespaceResolver);
            	handleAttributes(atts);
        }

        public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        	XPathFragment xPathFragment = new XPathFragment(localName);
        	xPathFragment.setNamespaceURI(namespaceURI);
        	
        	JSONWriterRecord.this.endElement(xPathFragment, namespaceResolver);        
        }

        public void startPrefixMapping(String prefix, String uri) throws SAXException {
        }

        public void characters(char[] ch, int start, int length) throws SAXException {
            String characters = new String (ch, start, length);
            characters(characters);
        }

        public void characters(CharSequence characters) throws SAXException {        	
        	JSONWriterRecord.this.characters(characters.toString());      
        }

        // --------------------- LEXICALHANDLER METHODS --------------------- //
        public void comment(char[] ch, int start, int length) throws SAXException {
        }

        public void startCDATA() throws SAXException {
            isProcessingCData = true;
        }

        public void endCDATA() throws SAXException {
            isProcessingCData = false;
        }

        // --------------------- CONVENIENCE METHODS --------------------- //
           protected void handleAttributes(Attributes atts) {
            for (int i=0, attsLength = atts.getLength(); i<attsLength; i++) {
                String qName = atts.getQName(i);
                if((qName != null && (qName.startsWith(javax.xml.XMLConstants.XMLNS_ATTRIBUTE + Constants.COLON) || qName.equals(javax.xml.XMLConstants.XMLNS_ATTRIBUTE)))) {
                    continue;
                }
                attribute(atts.getURI(i), atts.getLocalName(i), qName, atts.getValue(i));
            }
        }

        protected void writeComment(char[] chars, int start, int length) {        
        }
       
        protected void writeCharacters(char[] chars, int start, int length) {
        	try {
				characters(chars, start, length);
			} catch (SAXException e) {
                throw XMLMarshalException.marshalException(e);
			}           
        }
        // --------------- SATISFY CONTENTHANDLER INTERFACE --------------- //
        public void endPrefixMapping(String prefix) throws SAXException {}
        public void processingInstruction(String target, String data) throws SAXException {}
        public void setDocumentLocator(Locator locator) {}
        public void startDocument() throws SAXException {}
        public void endDocument() throws SAXException {}
        public void skippedEntity(String name) throws SAXException {}
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {}

        // --------------- SATISFY LEXICALHANDLER INTERFACE --------------- //
        public void startEntity(String name) throws SAXException {}
        public void endEntity(String name) throws SAXException {}
        public void startDTD(String name, String publicId, String systemId) throws SAXException {}
        public void endDTD() throws SAXException {}
		@Override
		public void setNil(boolean isNil) {}

    }

    /**
     * Instances of this class are used to maintain state about the current
     * level of the JSON message being marshalled.
     */
    protected static class Level {

        private boolean first;
        private boolean collection;
        private boolean emptyCollection;
        private boolean needToOpenComplex;
        private boolean needToCloseComplex;
        private Level previousLevel;
 
        public Level(boolean value, boolean needToOpen) {
            this.first = value;
            needToOpenComplex = needToOpen;
        }

        public Level(boolean value, boolean needToOpen, Level previousLevel) {
            this(value, needToOpen);
            this.previousLevel = previousLevel;
        }

        public boolean isNeedToOpenComplex() {
            return needToOpenComplex;
        }

        public void setNeedToOpenComplex(boolean needToOpenComplex) {
            this.needToOpenComplex = needToOpenComplex;
        }

        public boolean isNeedToCloseComplex() {
            return needToCloseComplex;
        }

        public void setNeedToCloseComplex(boolean needToCloseComplex) {
            this.needToCloseComplex = needToCloseComplex;
        }

        public boolean isEmptyCollection() {
            return emptyCollection;
        }

        public void setEmptyCollection(boolean emptyCollection) {
            this.emptyCollection = emptyCollection;
        }

        public boolean isFirst() {
            return first;
        }

        public void setFirst(boolean value) {
            this.first = value;
        }

        public boolean isCollection() {
            return collection;
        }

        public void setCollection(boolean collection) {
            this.collection = collection;
        }

        public Level getPreviousLevel() {
            return previousLevel;
        }

    }

    protected static interface Output {

        public void flush() throws IOException;

        public XMLMarshaller getMarshaller();

        public OutputStream getOutputStream();

        public Writer getWriter();

        public void setMarshaller(XMLMarshaller marshaller);

        public void write(char character) throws IOException;

        public void write(String text) throws IOException;

        public void writeAttributePrefix() throws IOException;

        public void writeCR() throws IOException;

        public void writeLocalName(XPathFragment xPathFragment) throws IOException;

        public void writeNamespaceSeparator() throws IOException;

        public void writeResultFromCharEscapeHandler(String value, boolean isAttribute);
 
    }

    protected static class OutputStreamOutput implements Output {

        private static final int BUFFER_SIZE = 512;

        private byte[] attributePrefix;
        private byte[] buffer = new byte[BUFFER_SIZE];
        private int bufferIndex = 0;
        private CharacterEscapeHandler characterEscapeHandler;
        private byte[] cr = Constants.cr().getBytes(Constants.DEFAULT_CHARSET);
        private XMLMarshaller marshaller;
        private char namespaceSeparator;
        private OutputStream outputStream;

        protected OutputStreamOutput(OutputStream writer) {
            this.outputStream = writer;
        }

        @Override
        public void flush() throws IOException {
            outputStream.write(buffer, 0, bufferIndex);
            bufferIndex = 0;
            outputStream.flush();
        }

        @Override
        public XMLMarshaller getMarshaller() {
            return marshaller;
        }

        @Override
        public OutputStream getOutputStream() {
            return outputStream;
        }

        @Override
        public Writer getWriter() {
             return null;
        }

        @Override
        public void setMarshaller(XMLMarshaller marshaller) {
            this.marshaller = marshaller;
            String attributePrefix = marshaller.getAttributePrefix();
            if(null != attributePrefix) {
                this.attributePrefix = attributePrefix.getBytes(Constants.DEFAULT_CHARSET);
            }
            this.characterEscapeHandler = marshaller.getCharacterEscapeHandler();
            this.namespaceSeparator = marshaller.getNamespaceSeparator();
        }

        private void write(byte[] bytes) {
            int bytesLength = bytes.length;
            if(bufferIndex + bytesLength >= BUFFER_SIZE) {
                try {
                    outputStream.write(buffer, 0, bufferIndex);
                    bufferIndex = 0;
                    if(bytesLength > BUFFER_SIZE) {
                        outputStream.write(bytes);
                        return;
                    }
                } catch(IOException e) {
                    throw XMLMarshalException.marshalException(e);
                }
            }
            System.arraycopy(bytes, 0, buffer, bufferIndex, bytes.length);
            bufferIndex += bytesLength;
        }

        @Override
        public void write(char character) throws IOException {
            if (character > 0x7F) {
                if(character > 0x7FF) {
                    if((character >= Character.MIN_HIGH_SURROGATE) && (character <= Character.MAX_LOW_SURROGATE)) {
                        int uc = ((character & 0x3ff) << 10);
                        // 11110zzz
                        write((byte)(0xF0 | ((uc >> 18))));
                        // 10zzyyyy
                        write((byte)(0x80 | ((uc >> 12) & 0x3F)));
                        // 10yyyyxx
                        write((byte)(0x80 | ((uc >> 6) & 0x3F)));
                        // 10xxxxxx
                        write((byte)(0x80 + (uc & 0x3F)));
                        return;
                    } else {
                        // 1110yyyy
                        write((byte)(0xE0 + (character >> 12)));
                    }
                    // 10yyyyxx
                    write((byte)(0x80 + ((character >> 6) & 0x3F)));
                } else {
                    // 110yyyxx
                    write((byte)(0xC0 + (character >> 6)));
                }
                write((byte)(0x80 + (character & 0x3F)));
            } else {
                write((byte) character);
            }
        }

        private void write(byte b) {
            if(bufferIndex == BUFFER_SIZE) {
                try {
                    outputStream.write(buffer, 0, BUFFER_SIZE);
                    bufferIndex = 0;
                } catch(IOException e) {
                    throw XMLMarshalException.marshalException(e);
                }
            }
            buffer[bufferIndex++] = b;
        }

        @Override
        public void write(String text) throws IOException {
            write(text.getBytes(Constants.DEFAULT_CHARSET));
        }

        @Override
        public void writeAttributePrefix() throws IOException {
            write(attributePrefix);
        }

        @Override
        public void writeCR() throws IOException {
            write(cr);
        }

        @Override
        public void writeLocalName(XPathFragment xPathFragment) throws IOException {
            write(xPathFragment.getLocalNameBytes());
        }

        @Override
        public void writeNamespaceSeparator() throws IOException {
            write(namespaceSeparator);
        }

        @Override
        public void writeResultFromCharEscapeHandler(String value, boolean isAttribute) {
            try {
                CharArrayWriter out = new CharArrayWriter();
                characterEscapeHandler.escape(value.toCharArray(), 0, value.length(), isAttribute, out);
                byte[] bytes = out.toString().getBytes();
                write(bytes);
                out.close();
            } catch (IOException e) {
                throw XMLMarshalException.marshalException(e);
            }
        }

    }

    private static class WriterOutput implements Output {

        private String attributePrefix;
        private CharacterEscapeHandler characterEscapeHandler;
        private String cr = Constants.cr();
        private XMLMarshaller marshaller;
        private char namespaceSeparator;
        private Writer writer;

        @Override
        public void flush() throws IOException {
            writer.flush();
        }

        protected WriterOutput(Writer writer) {
            this.writer = writer;
        }

        public XMLMarshaller getMarshaller() {
            return marshaller;
        }

        @Override
        public OutputStream getOutputStream() {
            return null;
        }

        @Override
        public Writer getWriter() {
            return writer;
        }
    
        @Override
        public void setMarshaller(XMLMarshaller marshaller) {
            this.marshaller = marshaller;
            this.attributePrefix = marshaller.getAttributePrefix();
            this.characterEscapeHandler = marshaller.getCharacterEscapeHandler();
            this.namespaceSeparator = marshaller.getNamespaceSeparator();
        }

        @Override
        public void writeAttributePrefix() throws IOException {
            writer.write(attributePrefix);
        }
    
        @Override
        public void write(char character) throws IOException {
            writer.write(character);
        }
    
        @Override
        public void write(String text) throws IOException {
            writer.write(text);
        }
    
        public void writeCR() throws IOException {
            writer.write(cr);
        }

        @Override
        public void writeLocalName(XPathFragment xPathFragment) throws IOException {
            writer.write(xPathFragment.getLocalName());
        }
    
        @Override
        public void writeNamespaceSeparator() throws IOException {
            writer.write(namespaceSeparator);
        }
    
        @Override
        public void writeResultFromCharEscapeHandler(String value, boolean isAttribute) {
            try {
                characterEscapeHandler.escape(value.toCharArray(), 0, value.length(), isAttribute, writer);
            } catch (IOException e) {
                throw XMLMarshalException.marshalException(e);
            }
        }
    
    }

}