/**
 * 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.cxf.jaxrs.provider.json;


import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.SequenceInputStream;
import java.io.StringReader;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import org.w3c.dom.Document;

import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.io.CachedOutputStream;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.ext.Nullable;
import org.apache.cxf.jaxrs.provider.AbstractJAXBProvider;
import org.apache.cxf.jaxrs.provider.json.utils.JSONUtils;
import org.apache.cxf.jaxrs.utils.AnnotationUtils;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.jaxrs.utils.HttpUtils;
import org.apache.cxf.jaxrs.utils.InjectionUtils;
import org.apache.cxf.jaxrs.utils.JAXBUtils;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.staxutils.DocumentDepthProperties;
import org.apache.cxf.staxutils.StaxUtils;
import org.apache.cxf.staxutils.W3CDOMStreamWriter;
import org.codehaus.jettison.JSONSequenceTooLargeException;
import org.codehaus.jettison.mapped.Configuration;
import org.codehaus.jettison.mapped.SimpleConverter;
import org.codehaus.jettison.mapped.TypeConverter;
import org.codehaus.jettison.util.StringIndenter;

@Produces({"application/json", "application/*+json" })
@Consumes({"application/json", "application/*+json" })
@Provider
public class JSONProvider<T> extends AbstractJAXBProvider<T>  {
    
    private static final String MAPPED_CONVENTION = "mapped";
    private static final String BADGER_FISH_CONVENTION = "badgerfish";
    private static final String DROP_ROOT_CONTEXT_PROPERTY = "drop.json.root.element";
    private static final String ARRAY_KEYS_PROPERTY = "json.array.keys";
    private static final String ROOT_IS_ARRAY_PROPERTY = "json.root.is.array";
    private static final String DROP_ELEMENT_IN_XML_PROPERTY = "drop.xml.elements";
    private static final String IGNORE_EMPTY_JSON_ARRAY_VALUES_PROPERTY = "ignore.empty.json.array.values";
    static {
        new SimpleConverter();
    }
    
    private ConcurrentHashMap<String, String> namespaceMap = 
        new ConcurrentHashMap<String, String>();
    private boolean serializeAsArray;
    private List<String> arrayKeys;
    private List<String> primitiveArrayKeys;
    private boolean unwrapped;
    private String wrapperName;
    private String namespaceSeparator;
    private Map<String, String> wrapperMap;
    private boolean dropRootElement;
    private boolean dropElementsInXmlStream = true;
    private boolean dropCollectionWrapperElement;
    private boolean ignoreMixedContent;
    private boolean ignoreEmptyArrayValues;
    private boolean writeXsiType = true;
    private boolean readXsiType = true;
    private boolean ignoreNamespaces;
    private String convention = MAPPED_CONVENTION;
    private TypeConverter typeConverter;
    private boolean attributesToElements;
    private boolean writeNullAsString = true;
    private boolean escapeForwardSlashesAlways;
    
    @Override
    public void setAttributesToElements(boolean value) {
        this.attributesToElements = value;
    }
    
    public void setConvention(String value) {
        if (!MAPPED_CONVENTION.equals(value) && !BADGER_FISH_CONVENTION.equals(value)) {
            throw new IllegalArgumentException("Unsupported convention \"" + value);
        }
        convention = value;
    }
    
    public void setConvertTypesToStrings(boolean convert) {
        if (convert) {
            this.setTypeConverter(new SimpleConverter());
        }
    }
    
    public void setTypeConverter(TypeConverter converter) {
        this.typeConverter = converter;
    }
    
    public void setIgnoreNamespaces(boolean ignoreNamespaces) {
        this.ignoreNamespaces = ignoreNamespaces;
    }
    
    @Context
    public void setMessageContext(MessageContext mc) {
        super.setContext(mc);
    }
    
    public void setDropRootElement(boolean drop) {
        this.dropRootElement = drop;
    }
    
    public void setDropCollectionWrapperElement(boolean drop) {
        this.dropCollectionWrapperElement = drop;
    }
    
    public void setIgnoreMixedContent(boolean ignore) {
        this.ignoreMixedContent = ignore;
    }
    
    public void setSupportUnwrapped(boolean unwrap) {
        this.unwrapped = unwrap;
    }
    
    public void setWrapperName(String wName) {
        wrapperName = wName;
    }
    
    public void setWrapperMap(Map<String, String> map) {
        wrapperMap = map;
    }
    
    public void setEnableBuffering(boolean enableBuf) {
        super.setEnableBuffering(enableBuf);
    }
    
    public void setConsumeMediaTypes(List<String> types) {
        super.setConsumeMediaTypes(types);
    }
    
    public void setProduceMediaTypes(List<String> types) {
        super.setProduceMediaTypes(types);
    }
    
    public void setSerializeAsArray(boolean asArray) {
        this.serializeAsArray = asArray;
    }
    
    public void setArrayKeys(List<String> keys) {
        this.arrayKeys = keys;
    }
    
    public void setNamespaceMap(Map<String, String> namespaceMap) {
        this.namespaceMap.putAll(namespaceMap);
    }

    @Override
    public boolean isReadable(Class<?> type, Type genericType, Annotation[] anns, MediaType mt) {
        return super.isReadable(type, genericType, anns, mt) || Document.class.isAssignableFrom(type);    
    }
    
    public T readFrom(Class<T> type, Type genericType, Annotation[] anns, MediaType mt, 
        MultivaluedMap<String, String> headers, InputStream is) 
        throws IOException {
        
        if (isPayloadEmpty(headers)) {
            if (AnnotationUtils.getAnnotation(anns, Nullable.class) != null) {
                return null;
            } else {
                reportEmptyContentLength();
            }
        }
        
        XMLStreamReader reader = null;
        String enc = HttpUtils.getEncoding(mt, "UTF-8");
        try {
            InputStream realStream = getInputStream(type, genericType, is);
            if (Document.class.isAssignableFrom(type)) {
                W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
                reader = createReader(type, realStream, false, enc);
                copyReaderToWriter(reader, writer);
                return type.cast(writer.getDocument());
            }
            boolean isCollection = InjectionUtils.isSupportedCollectionOrArray(type);
            Class<?> theGenericType = isCollection ? InjectionUtils.getActualType(genericType) : type;
            Class<?> theType = getActualType(theGenericType, genericType, anns);
            
            Unmarshaller unmarshaller = createUnmarshaller(theType, genericType, isCollection);
            XMLStreamReader xsr = createReader(type, realStream, isCollection, enc);
            
            Object response = null;
            if (JAXBElement.class.isAssignableFrom(type) 
                || !isCollection && (unmarshalAsJaxbElement
                || jaxbElementClassMap != null && jaxbElementClassMap.containsKey(theType.getName()))) {
                response = unmarshaller.unmarshal(xsr, theType);
            } else {
                response = unmarshaller.unmarshal(xsr);
            }
            if (response instanceof JAXBElement && !JAXBElement.class.isAssignableFrom(type)) {
                response = ((JAXBElement<?>)response).getValue();    
            }
            if (isCollection) {
                response = ((CollectionWrapper)response).getCollectionOrArray(
                               unmarshaller, theType, type, genericType, 
                               org.apache.cxf.jaxrs.utils.JAXBUtils.getAdapter(theGenericType, anns)); 
            } else {
                response = checkAdapter(response, type, anns, false);
            }
            return type.cast(response);
            
        } catch (JAXBException e) {
            handleJAXBException(e, true);
        } catch (XMLStreamException e) {
            if (e.getCause() instanceof JSONSequenceTooLargeException) {
                throw new WebApplicationException(413);
            } else {
                handleXMLStreamException(e, true);
            }
        } catch (WebApplicationException e) {
            throw e;
        } catch (Exception e) {
            throw ExceptionUtils.toBadRequestException(e, null);
        } finally {
            try {
                StaxUtils.close(reader);
            } catch (XMLStreamException e) {
                throw ExceptionUtils.toBadRequestException(e, null);
            }
        }
        // unreachable
        return null;
    }

    protected XMLStreamReader createReader(Class<?> type, InputStream is, boolean isCollection, String enc) 
        throws Exception {
        XMLStreamReader reader = createReader(type, is, enc);
        return isCollection ? new JAXBCollectionWrapperReader(reader) : reader;
    }
    
    protected XMLStreamReader createReader(Class<?> type, InputStream is, String enc) 
        throws Exception {
        XMLStreamReader reader = null;
        if (BADGER_FISH_CONVENTION.equals(convention)) {
            reader = JSONUtils.createBadgerFishReader(is, enc);
        } else {
            reader = JSONUtils.createStreamReader(is, 
                                                  readXsiType, 
                                                  namespaceMap,
                                                  namespaceSeparator,
                                                  primitiveArrayKeys,
                                                  getDepthProperties(), 
                                                  enc);
        }
        reader = createTransformReaderIfNeeded(reader, is);
        
        return reader;
    }
    
    protected InputStream getInputStream(Class<T> cls, Type type, InputStream is) throws Exception {
        if (unwrapped) {
            String rootName = getRootName(cls, type);
            InputStream isBefore = new ByteArrayInputStream(rootName.getBytes());
            String after = "}";
            InputStream isAfter = new ByteArrayInputStream(after.getBytes());
            final InputStream[] streams = new InputStream[]{isBefore, is, isAfter};
            
            Enumeration<InputStream> list = new Enumeration<InputStream>() {
                private int index; 
                public boolean hasMoreElements() {
                    return index < streams.length;
                }

                public InputStream nextElement() {
                    return streams[index++];
                }  
                
            };
            return new SequenceInputStream(list);
        } else {
            return is;
        }
                 
    }
    
    protected String getRootName(Class<T> cls, Type type) throws Exception {
        String name = null;
        if (wrapperName != null) {
            name = wrapperName;
        } else if (wrapperMap != null) {
            name = wrapperMap.get(cls.getName());
        }
        if (name == null) {
            QName qname = getQName(cls, type, null);
            if (qname != null) {
                name = qname.getLocalPart();
                String prefix = qname.getPrefix();
                if (prefix.length() > 0) {
                    name = prefix + "." + name;
                }
            }
        }
        
        if (name == null) {
            throw ExceptionUtils.toInternalServerErrorException(null, null);
        }
        
        return "{\"" + name + "\":";
    }
    
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] anns, MediaType mt) {
        
        return super.isWriteable(type, genericType, anns, mt)
            || Document.class.isAssignableFrom(type);
    }
    
    public void writeTo(T obj, Class<?> cls, Type genericType, Annotation[] anns,  
        MediaType m, MultivaluedMap<String, Object> headers, OutputStream os)
        throws IOException {
        if (os == null) {
            StringBuilder sb = new StringBuilder();
            sb.append("Jettison needs initialized OutputStream");
            if (getContext() != null && getContext().getContent(XMLStreamWriter.class) == null) {
                sb.append("; if you need to customize Jettison output with the custom XMLStreamWriter"
                          + " then extend JSONProvider or when possible configure it directly.");
            }
            throw new IOException(sb.toString());
        }
        XMLStreamWriter writer = null;
        try {
            
            String enc = HttpUtils.getSetEncoding(m, headers, "UTF-8");
            if (Document.class.isAssignableFrom(cls)) {
                writer = createWriter(obj, cls, genericType, enc, os, false);
                copyReaderToWriter(StaxUtils.createXMLStreamReader((Document)obj), writer);
                return;
            }
            if (InjectionUtils.isSupportedCollectionOrArray(cls)) {
                marshalCollection(cls, obj, genericType, enc, os, m, anns);
            } else {
                Object actualObject = checkAdapter(obj, cls, anns, true);
                Class<?> actualClass = obj != actualObject || cls.isInterface() 
                    ? actualObject.getClass() : cls;
                if (cls == genericType) {
                    genericType = actualClass;
                }
                
                marshal(actualObject, actualClass, genericType, enc, os);
            }
            
        } catch (JAXBException e) {
            handleJAXBException(e, false);
        } catch (XMLStreamException e) {
            handleXMLStreamException(e, false);
        } catch (Exception e) {
            throw ExceptionUtils.toInternalServerErrorException(e, null);
        } finally {
            StaxUtils.close(writer);
        }
    }

    protected void copyReaderToWriter(XMLStreamReader reader, XMLStreamWriter writer) 
        throws Exception {
        writer.writeStartDocument();
        StaxUtils.copy(reader, writer);
        writer.writeEndDocument();
    }
    
    protected void marshalCollection(Class<?> originalCls, Object collection, 
                                     Type genericType, String encoding, 
                                     OutputStream os, MediaType m, Annotation[] anns) 
        throws Exception {
        
        Class<?> actualClass = InjectionUtils.getActualType(genericType);
        actualClass = getActualType(actualClass, genericType, anns);
        
        Collection<?> c = originalCls.isArray() ? Arrays.asList((Object[]) collection) 
                                             : (Collection<?>) collection;

        Iterator<?> it = c.iterator();
        
        Object firstObj = it.hasNext() ? it.next() : null;

        String startTag = null;
        String endTag = null;
        if (!dropCollectionWrapperElement) {
            QName qname = null;
            if (firstObj instanceof JAXBElement) {
                JAXBElement<?> el = (JAXBElement<?>)firstObj;
                qname = el.getName();
                actualClass = el.getDeclaredType();
            } else {
                qname = getCollectionWrapperQName(actualClass, genericType, firstObj, false);
            }
            String prefix = "";
            if (!ignoreNamespaces) {
                prefix = namespaceMap.get(qname.getNamespaceURI());
                if (prefix != null) {
                    if (prefix.length() > 0) {
                        prefix += ".";
                    }
                } else if (qname.getNamespaceURI().length() > 0) {
                    prefix = "ns1.";
                }
            }
            prefix = (prefix == null) ? "" : prefix;
            startTag = "{\"" + prefix + qname.getLocalPart() + "\":[";
            endTag = "]}";
        } else if (serializeAsArray) {
            startTag = "[";
            endTag = "]";
        } else {
            startTag = "{";
            endTag = "}";
        }
        
        os.write(startTag.getBytes());
        if (firstObj != null) {
            XmlJavaTypeAdapter adapter = 
                org.apache.cxf.jaxrs.utils.JAXBUtils.getAdapter(firstObj.getClass(), anns);
            marshalCollectionMember(JAXBUtils.useAdapter(firstObj, adapter, true),
                                    actualClass, genericType, encoding, os);
            while (it.hasNext()) {
                os.write(",".getBytes());
                marshalCollectionMember(JAXBUtils.useAdapter(it.next(), adapter, true), 
                                        actualClass, genericType, encoding, os);
            }
        }
        os.write(endTag.getBytes());
    }
    
    protected void marshalCollectionMember(Object obj, Class<?> cls, Type genericType,
                                           String enc, OutputStream os) throws Exception {
        if (obj instanceof JAXBElement) {
            obj = ((JAXBElement<?>)obj).getValue();    
        } else {
            obj = convertToJaxbElementIfNeeded(obj, cls, genericType);
        }
        
        if (obj instanceof JAXBElement && cls != JAXBElement.class) {
            cls = JAXBElement.class;
        }
        Marshaller ms = createMarshaller(obj, cls, genericType, enc);
        marshal(ms, obj, cls, genericType, enc, os, true);
        
    }
    
    protected void marshal(Marshaller ms, Object actualObject, Class<?> actualClass, 
                  Type genericType, String enc, OutputStream os, boolean isCollection) throws Exception {
        OutputStream actualOs = os; 
        
        MessageContext mc = getContext();
        if (mc != null && MessageUtils.isTrue(mc.get(Marshaller.JAXB_FORMATTED_OUTPUT))) {
            actualOs = new CachedOutputStream();    
        }
        XMLStreamWriter writer = createWriter(actualObject, actualClass, genericType, enc, 
                                              actualOs, isCollection);
        if (namespaceMap.size() > 1 || namespaceMap.size() == 1 && !namespaceMap.containsKey(JSONUtils.XSI_URI)) {
            setNamespaceMapper(ms, namespaceMap);
        }
        ms.marshal(actualObject, writer);
        writer.close();
        if (os != actualOs) {
            StringIndenter formatter = new StringIndenter(
                IOUtils.newStringFromBytes(((CachedOutputStream)actualOs).getBytes()));
            Writer outWriter = new OutputStreamWriter(os, enc);
            IOUtils.copy(new StringReader(formatter.result()), outWriter, 2048);
            outWriter.close();
        }
    }
    
    protected XMLStreamWriter createWriter(Object actualObject, Class<?> actualClass, 
        Type genericType, String enc, OutputStream os, boolean isCollection) throws Exception {
        
        if (BADGER_FISH_CONVENTION.equals(convention)) {
            return JSONUtils.createBadgerFishWriter(os, enc);
        }
        boolean dropElementsInXmlStreamProp = getBooleanJsonProperty(DROP_ELEMENT_IN_XML_PROPERTY, 
                                                                     dropElementsInXmlStream);
        
        boolean dropRootNeeded = getBooleanJsonProperty(DROP_ROOT_CONTEXT_PROPERTY, dropRootElement);
        boolean dropRootInXmlNeeded = dropRootNeeded && dropElementsInXmlStreamProp;
        
        QName qname = actualClass == Document.class 
            ? org.apache.cxf.helpers.DOMUtils.getElementQName(((Document)actualObject).getDocumentElement()) 
            : getQName(actualClass, genericType, actualObject);
        if (qname != null && ignoreNamespaces && (isCollection || dropRootInXmlNeeded)) {        
            qname = new QName(qname.getLocalPart());
        }
        
        Configuration config = 
            JSONUtils.createConfiguration(namespaceMap, 
                                          writeXsiType && !ignoreNamespaces,
                                          attributesToElements,
                                          typeConverter);
        if (namespaceSeparator != null) {
            config.setJsonNamespaceSeparator(namespaceSeparator);
        }
        if (!dropElementsInXmlStreamProp && super.outDropElements != null) {
            config.setIgnoredElements(outDropElements);
        }
        if (!writeNullAsString) {
            config.setWriteNullAsString(writeNullAsString);
        }
        boolean ignoreEmpty = getBooleanJsonProperty(IGNORE_EMPTY_JSON_ARRAY_VALUES_PROPERTY, ignoreEmptyArrayValues);
        if (ignoreEmpty) {
            config.setIgnoreEmptyArrayValues(ignoreEmpty);
        }
        
        if (escapeForwardSlashesAlways) {
            config.setEscapeForwardSlashAlways(escapeForwardSlashesAlways);
        }
        
        
        boolean dropRootInJsonStream = dropRootNeeded && !dropElementsInXmlStreamProp;
        if (dropRootInJsonStream) {
            config.setDropRootElement(true);
        }
         
        List<String> theArrayKeys = getArrayKeys();
        boolean rootIsArray = isRootArray(theArrayKeys);
        
        if (ignoreNamespaces && rootIsArray && (theArrayKeys == null || dropRootInJsonStream)) {
            if (theArrayKeys == null) {
                theArrayKeys = new LinkedList<String>();
            } else if (dropRootInJsonStream) {
                theArrayKeys = new LinkedList<String>(theArrayKeys);
            }
            if (qname != null) {
                theArrayKeys.add(qname.getLocalPart());
            }
        }
        
        XMLStreamWriter writer = JSONUtils.createStreamWriter(os, qname, 
             writeXsiType && !ignoreNamespaces, config, rootIsArray, theArrayKeys,
             isCollection || dropRootInXmlNeeded, enc);
        writer = JSONUtils.createIgnoreMixedContentWriterIfNeeded(writer, ignoreMixedContent);
        writer = JSONUtils.createIgnoreNsWriterIfNeeded(writer, ignoreNamespaces, !writeXsiType);
        return createTransformWriterIfNeeded(writer, os, dropElementsInXmlStreamProp);
    }
    
    protected List<String> getArrayKeys() {
        MessageContext mc = getContext();
        if (mc != null) {
            Object prop = mc.get(ARRAY_KEYS_PROPERTY);
            if (prop instanceof List) {
                return CastUtils.cast((List<?>)prop);
            }
        }
        return arrayKeys;
    }
    
    protected boolean isRootArray(List<String> theArrayKeys) {
        return theArrayKeys != null ? true : getBooleanJsonProperty(ROOT_IS_ARRAY_PROPERTY, serializeAsArray);
    }
    
    
    protected boolean getBooleanJsonProperty(String name, boolean defaultValue) {
        MessageContext mc = getContext();
        if (mc != null) {
            Object prop = mc.get(name);
            if (prop != null) {
                return MessageUtils.isTrue(prop);
            }
        }
        return defaultValue;
    }
    
    protected void marshal(Object actualObject, Class<?> actualClass, 
                           Type genericType, String enc, OutputStream os) throws Exception {
        
        actualObject = convertToJaxbElementIfNeeded(actualObject, actualClass, genericType);
        if (actualObject instanceof JAXBElement && actualClass != JAXBElement.class) {
            actualClass = JAXBElement.class;
        }
        
        Marshaller ms = createMarshaller(actualObject, actualClass, genericType, enc);
        marshal(ms, actualObject, actualClass, genericType, enc, os, false);
    }
    
    private QName getQName(Class<?> cls, Type type, Object object) 
        throws Exception {
        QName qname = getJaxbQName(cls, type, object, false);
        if (qname != null) {
            String prefix = getPrefix(qname.getNamespaceURI());
            return new QName(qname.getNamespaceURI(), qname.getLocalPart(), prefix);
        }
        return null;
    }
    
    private String getPrefix(String namespace) {
        String prefix = namespaceMap.get(namespace);
        return prefix == null ? "" : prefix;
    }
    
    public void setWriteXsiType(boolean writeXsiType) {
        this.writeXsiType = writeXsiType;
    }
    
    public void setReadXsiType(boolean readXsiType) {
        this.readXsiType = readXsiType;
    }

    public void setPrimitiveArrayKeys(List<String> primitiveArrayKeys) {
        this.primitiveArrayKeys = primitiveArrayKeys;
    }

    public void setDropElementsInXmlStream(boolean drop) {
        this.dropElementsInXmlStream = drop;
    }

    public void setWriteNullAsString(boolean writeNullAsString) {
        this.writeNullAsString = writeNullAsString;
    }

    public void setIgnoreEmptyArrayValues(boolean ignoreEmptyArrayElements) {
        this.ignoreEmptyArrayValues = ignoreEmptyArrayElements;
    }

    protected DocumentDepthProperties getDepthProperties() {
        DocumentDepthProperties depthProperties = super.getDepthProperties();
        if (depthProperties != null) {
            return depthProperties;
        }
        if (getContext() != null) {
            String totalElementCountStr = (String)getContext().getContextualProperty(
                DocumentDepthProperties.TOTAL_ELEMENT_COUNT);
            String innerElementCountStr = (String)getContext().getContextualProperty(
                DocumentDepthProperties.INNER_ELEMENT_COUNT);
            String elementLevelStr = (String)getContext().getContextualProperty(
                DocumentDepthProperties.INNER_ELEMENT_LEVEL);
            if (totalElementCountStr != null || innerElementCountStr != null || elementLevelStr != null) {
                try {
                    int totalElementCount = totalElementCountStr != null 
                        ? Integer.valueOf(totalElementCountStr) : -1;
                    int elementLevel = elementLevelStr != null ? Integer.valueOf(elementLevelStr) : -1;
                    int innerElementCount = innerElementCountStr != null 
                        ? Integer.valueOf(innerElementCountStr) : -1;
                    return new DocumentDepthProperties(totalElementCount, elementLevel, innerElementCount);
                } catch (Exception ex) {
                    throw ExceptionUtils.toInternalServerErrorException(ex, null);
                }
            }
        }
        return null;
    }

    public void setEscapeForwardSlashesAlways(boolean escape) {
        this.escapeForwardSlashesAlways = escape;
    }

    public void setNamespaceSeparator(String namespaceSeparator) {
        this.namespaceSeparator = namespaceSeparator;
    }
}
