/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor.generator;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.*;
import org.dom4j.io.DocumentSource;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.http.Headers;
import org.orbeon.oxf.pipeline.api.ExternalContext;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.processor.ProcessorImpl;
import org.orbeon.oxf.processor.ProcessorInputOutputInfo;
import org.orbeon.oxf.processor.ProcessorOutput;
import org.orbeon.oxf.processor.impl.DigestState;
import org.orbeon.oxf.processor.impl.DigestTransformerOutputImpl;
import org.orbeon.oxf.properties.Properties;
import org.orbeon.oxf.properties.PropertySet;
import org.orbeon.oxf.util.NetUtils;
import org.orbeon.oxf.util.SystemUtils;
import org.orbeon.oxf.xml.*;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.oxf.xml.dom4j.NonLazyUserDataDocument;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;

/**
 * The Request generator works like this:
 *
 * o Read config DOM (or obtain it from state cache)
 * o Obtain request DOM
 *   o Read whole request as DOM (or obtain it from context cache)
 *     o If content-type is multipart/form-data, special handling takes place.
 *       o File items are read with commons upload, either in memory if small or on disk if large
 *       o Only references to the file items are stored in the DOM
 *     o A special marker is put in place for the request body
 *   o Filter the DOM request based on config
 *   o Serialize the DOM into SAX
 *     o References to file items are treated specially during the serialization process
 *       o Small files are serialized, and the xs:base64Binary type is set
 *       o Large files are referenced, and the xs:anyURI type is set
 *     o The body marker, if not filtered, causes the body of the request to be read
 *       o Small files are serialized, and the xs:base64Binary type is set
 *       o Large files are referenced, and the xs:anyURI type is set
 *
 *   NOTE: The Request generator attempts to cache based on the content of the request, but it
 *   never caches if there is an upload or if a request body is requested.
 *
 *   FIXME: The upload code checks that the total request body is not larger than the maximum
 *   upload size specified. But If many small files are uploaded, can we potentially use a lot of
 *   memory?
 */
public class RequestGenerator extends ProcessorImpl {

    public static final String REQUEST_CONFIG_NAMESPACE_URI = "http://orbeon.org/oxf/xml/request-config";
    private static final String REQUEST_PRIVATE_NAMESPACE_URI = "http://orbeon.org/oxf/xml/request-private";

    // Maximum upload size
    private static final int DEFAULT_MAX_UPLOAD_SIZE = 1024 * 1024;
    private static final String MAX_UPLOAD_SIZE_PROPERTY = "max-upload-size";

    // Maximum size kept in memory
    private static final int DEFAULT_MAX_UPLOAD_MEMORY_SIZE = 10 * 1024;
    private static final String MAX_UPLOAD_MEMORY_SIZE_PROPERTY = "max-upload-memory-size";

    private static final String INCLUDE_ELEMENT = "include";
    private static final String EXCLUDE_ELEMENT = "exclude";

    private static final String FILE_ITEM_ELEMENT = "request:file-item";
    private static final String PARAMETER_NAME_ATTRIBUTE = "parameter-name";
    private static final String PARAMETER_POSITION_ATTRIBUTE = "parameter-position";
    private static final String REQUEST_GENERATOR_CONTEXT = "request-generator-context";

    private static final String BODY_REQUEST_ATTRIBUTE = "orbeon.request.body.url";

    public RequestGenerator() {
        addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, REQUEST_CONFIG_NAMESPACE_URI));
        addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DATA));
    }

    @Override
    public ProcessorOutput createOutput(String name) {
        final ProcessorOutput output = new DigestTransformerOutputImpl(RequestGenerator.this, name) {
            public void readImpl(final PipelineContext pipelineContext, XMLReceiver xmlReceiver) {
                final State state = (State) getFilledOutState(pipelineContext);
                // Transform the resulting document into SAX

                TransformerUtils.sourceToSAX(new DocumentSource(state.requestDocument), new ForwardingXMLReceiver(xmlReceiver) {
                    @Override
                    public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException {
                        try {
                            if (REQUEST_PRIVATE_NAMESPACE_URI.equals(uri)) {
                                // Special treatment for this element
                                if (FILE_ITEM_ELEMENT.equals(qName)) {
                                    // Marker for file item

                                    final String parameterName = attributes.getValue(PARAMETER_NAME_ATTRIBUTE);
                                    final int parameterPosition = Integer.parseInt(attributes.getValue(PARAMETER_POSITION_ATTRIBUTE));
                                    final FileItem fileItem = (FileItem) ((Object[]) getRequest(pipelineContext).getParameterMap().get(parameterName))[parameterPosition];

                                    final AttributesImpl newAttributes = new AttributesImpl();
                                    super.startPrefixMapping(XMLConstants.XSI_PREFIX, XMLConstants.XSI_URI);
                                    super.startPrefixMapping(XMLConstants.XSD_PREFIX, XMLConstants.XSD_URI);
                                    newAttributes.addAttribute(XMLConstants.XSI_URI, "type", "xsi:type", "CDATA",
                                            useBase64(pipelineContext, fileItem) ? XMLConstants.XS_BASE64BINARY_QNAME.getQualifiedName(): XMLConstants.XS_ANYURI_QNAME.getQualifiedName());
                                    super.startElement("", "value", "value", newAttributes);
                                    writeFileItem(pipelineContext, fileItem, state.isSessionScope, useBase64(pipelineContext, fileItem), getXMLReceiver());
                                    super.endElement("", "value", "value");
                                    super.endPrefixMapping(XMLConstants.XSD_PREFIX);
                                    super.endPrefixMapping(XMLConstants.XSI_PREFIX);
                                }
                            } else if (localname.equals("body") && uri.equals("")) {
                                // Marker for request body

                                // Read InputStream into FileItem object, if not already present

                                // We do this so we can read the body multiple times, if needed.
                                // For large files, there will be a performance hit. If we knew
                                // we didn't need to read it multiple times, we could avoid
                                // saving the stream, but practically, it can happen, and it is
                                // convenient.
                                final Context context = getContext(pipelineContext);
                                if (context.bodyFileItem != null || getRequest(pipelineContext).getInputStream() != null) {

                                    final ExternalContext.Request request = getRequest(pipelineContext);

                                    if (context.bodyFileItem == null) {
                                        final FileItem fileItem = new DiskFileItemFactory(getMaxMemorySizeProperty(), SystemUtils.getTemporaryDirectory()).createItem("dummy", "dummy", false, null);
                                        pipelineContext.addContextListener(new PipelineContext.ContextListenerAdapter() {
                                            public void contextDestroyed(boolean success) {
                                                fileItem.delete();
                                            }
                                        });
                                        final OutputStream outputStream = fileItem.getOutputStream();
                                        NetUtils.copyStream(request.getInputStream(), outputStream);
                                        outputStream.close();
                                        context.bodyFileItem = fileItem;
                                    }
                                    // Serialize the stream into the body element
                                    final AttributesImpl newAttributes = new AttributesImpl();
                                    super.startPrefixMapping(XMLConstants.XSI_PREFIX, XMLConstants.XSI_URI);
                                    super.startPrefixMapping(XMLConstants.XSD_PREFIX, XMLConstants.XSD_URI);
                                    newAttributes.addAttribute(XMLConstants.XSI_URI, "type", "xsi:type", "CDATA",
                                            useBase64(pipelineContext, context.bodyFileItem) ? XMLConstants.XS_BASE64BINARY_QNAME.getQualifiedName(): XMLConstants.XS_ANYURI_QNAME.getQualifiedName());
                                    super.startElement(uri, localname, qName, newAttributes);
                                    final String uriOrNull = writeFileItem(pipelineContext, context.bodyFileItem, state.isSessionScope, useBase64(pipelineContext, context.bodyFileItem), getXMLReceiver());
                                    super.endElement(uri, localname, qName);
                                    super.endPrefixMapping(XMLConstants.XSD_PREFIX);
                                    super.endPrefixMapping(XMLConstants.XSI_PREFIX);

                                    // If the body is available as a URL, store it into the pipeline context.
                                    // This is done so that native code can access the body even if it has been read
                                    // already. Possibly, this could be handled more transparently by ExternalContext,
                                    // so that Request.getInputStream() works even upon multiple reads.
                                    // NOTE 2013-05-30: We used to store this into the request, but request attributes
                                    // are forwarded by LocalRequest. This means that a forwarded-to request might get
                                    // the wrong body! Instead, we now use PipelineContext, which is scoped to be per
                                    // request. Again, if ExternalContext was handling this, we could just leave it to
                                    // ExternalContext.
                                    if (uriOrNull != null)
                                        pipelineContext.setAttribute(BODY_REQUEST_ATTRIBUTE, uriOrNull);
                                }
                            } else {
                                super.startElement(uri, localname, qName, attributes);
                            }
                        } catch (IOException e) {
                            throw new OXFException(e);
                        }
                    }
                    @Override
                    public void endElement(String uri, String localname, String qName) throws SAXException {
                        if (REQUEST_PRIVATE_NAMESPACE_URI.equals(uri) || localname.equals("body") && uri.equals("")) {
                            // Ignore end element
                        } else {
                            super.endElement(uri, localname, qName);
                        }
                    }
                });
            }

            protected boolean fillOutState(PipelineContext pipelineContext, DigestState digestState) {
                final State state = (State) digestState;
                if (state.requestDocument == null) {
                    // Read config document
                    final Document config = readCacheInputAsDOM4J(pipelineContext, INPUT_CONFIG);

                    // Try to find stream-type attribute
                    final QName streamTypeQName = Dom4jUtils.extractAttributeValueQName(config.getRootElement(), "stream-type");
                    if (streamTypeQName != null && !(streamTypeQName.equals(XMLConstants.XS_BASE64BINARY_QNAME) || streamTypeQName.equals(XMLConstants.XS_ANYURI_QNAME)))
                        throw new OXFException("Invalid value for stream-type attribute: " + streamTypeQName.getQualifiedName());
                    state.requestedStreamType = streamTypeQName;
                    state.isSessionScope = "session".equals(config.getRootElement().attributeValue("stream-scope"));

                    // Read and store request
                    state.requestDocument = readRequestAsDOM4J(pipelineContext, config);

                    // Check if the body was requested
                    state.bodyRequested = XPathUtils.selectSingleNode(state.requestDocument, "/*/body") != null;
                }
                final Context context = getContext(pipelineContext);
                return !context.hasUpload && !state.bodyRequested;
            }

            protected byte[] computeDigest(PipelineContext pipelineContext, DigestState digestState) {
                final State state = (State) digestState;
                return DigestContentHandler.getDigest(new DocumentSource(state.requestDocument));
            }
        };
        addOutput(name, output);
        return output;
    }

    /**
     * Check whether a specific FileItem must be generated as Base64.
     */
    private boolean useBase64(PipelineContext pipelineContext, FileItem fileItem) {
        State state = (State) getState(pipelineContext);

        return (state.requestedStreamType == null && fileItem.isInMemory())
                || (state.requestedStreamType != null && state.requestedStreamType.equals(XMLConstants.XS_BASE64BINARY_QNAME));
    }

    public static String writeFileItem(PipelineContext pipelineContext, FileItem fileItem, boolean isSessionScope, boolean useBase64, ContentHandler contentHandler) throws SAXException {
        if (!isFileItemEmpty(fileItem)) {
            if (useBase64) {
                writeBase64FileItem(fileItem, contentHandler);
                return null;
            } else
                return writeURLFileItem(pipelineContext, fileItem, isSessionScope, contentHandler);
        } else
            return null;
    }

    public static void writeBase64FileItem(FileItem fileItem, ContentHandler contentHandler) throws SAXException {
        // The content of the file is streamed to the output (xs:base64Binary)
        InputStream fileItemInputStream = null;
        try {
            fileItemInputStream = fileItem.getInputStream();
            SAXUtils.inputStreamToBase64Characters(fileItemInputStream, contentHandler);
        } catch (IOException e) {
            throw new OXFException(e);
        } finally {
            if (fileItemInputStream != null) {
                try {
                    fileItemInputStream.close();
                } catch (IOException e) {
                    throw new OXFException(e);
                }
            }
        }
    }

    public static String urlForFileItem(FileItem fileItem) throws SAXException {
        // Only a reference to the file is output (xs:anyURI)
        final DiskFileItem diskFileItem = (DiskFileItem) fileItem;
        final String uriExpiringWithRequest;
        if (!fileItem.isInMemory()) {
            // File must exist on disk since isInMemory() returns false
            final File file = diskFileItem.getStoreLocation();
            uriExpiringWithRequest = file.toURI().toString();
        } else {
            // File does not exist on disk, must convert
            // NOTE: Conversion occurs every time this method is called. Not optimal.
            try {
                uriExpiringWithRequest = NetUtils.inputStreamToAnyURI(fileItem.getInputStream(), NetUtils.REQUEST_SCOPE, logger);
            } catch (IOException e) {
                throw new OXFException(e);
            }
        }

        return uriExpiringWithRequest;
    }

    public static String writeURLFileItem(PipelineContext pipelineContext, FileItem fileItem, boolean isSessionScope, ContentHandler contentHandler) throws SAXException {

        final String uriExpiringWithRequest = urlForFileItem(fileItem);

        // If the content is meant to expire with the session, and we haven't yet renamed the file, then do this here.
        final String uriExpiringWithScope;
        if (isSessionScope) {
            final String tempSessionURI = getContext(pipelineContext).getSessionURIForRequestURI(uriExpiringWithRequest);
            if (tempSessionURI == null) {
                uriExpiringWithScope = NetUtils.renameAndExpireWithSession(uriExpiringWithRequest, logger).toURI().toString();
                getContext(pipelineContext).putSessionURIForRequestURI(uriExpiringWithRequest, uriExpiringWithScope);
            } else
                uriExpiringWithScope = tempSessionURI;
        } else
            uriExpiringWithScope = uriExpiringWithRequest;

        final char[] chars = uriExpiringWithScope.toCharArray();
        contentHandler.characters(chars, 0, chars.length);

        return uriExpiringWithRequest;
    }

    private Document readRequestAsDOM4J(PipelineContext pipelineContext, Node config) {
        // Get complete request document from pipeline context, or create it if not there
        final Context context = getContext(pipelineContext);
        if (context.wholeRequest == null)
            context.wholeRequest = readWholeRequestAsDOM4J(getRequest(pipelineContext), context);
        final Document result = (Document) context.wholeRequest.clone();

        // Filter the request based on the config input
        filterRequestDocument(result, config);

        return result;
    }

    private static void addTextElement(Element element, String elementName, String text) {
        if (text != null)
            element.addElement(elementName).addText(text);
    }

    public static Document readWholeRequestAsDOM4J(final ExternalContext.Request request, final Context context) {

        final Document document = new NonLazyUserDataDocument();
        final Element requestElement = document.addElement("request");

        addTextElement(requestElement, "container-type", request.getContainerType());
        addTextElement(requestElement, "container-namespace", request.getContainerNamespace());
        addTextElement(requestElement, "character-encoding", request.getCharacterEncoding());
        addTextElement(requestElement, Headers.ContentLengthLower(), Integer.toString(request.getContentLength()));
        {
            final String contentType = request.getContentType();
            addTextElement(requestElement, Headers.ContentTypeLower(), (contentType == null) ? "" : contentType);
        }
        addParameters(context, requestElement, request);
        addBody(requestElement);
        addTextElement(requestElement, "protocol", request.getProtocol());
        addTextElement(requestElement, "remote-addr", request.getRemoteAddr());
        // TODO: Handle this differently as it can take a long time to work
        addTextElement(requestElement, "remote-host", request.getRemoteHost());
        addTextElement(requestElement, "scheme", request.getScheme());
        addTextElement(requestElement, "server-name", request.getServerName());
        addTextElement(requestElement, "server-port", Integer.toString(request.getServerPort()));
        addTextElement(requestElement, "is-secure", Boolean.toString(request.isSecure()));
        addTextElement(requestElement, "auth-type", request.getAuthType());
        addTextElement(requestElement, "context-path", request.getContextPath());
        addHeaders(requestElement, request);
        addAttributes(requestElement, request);
        addTextElement(requestElement, "method", request.getMethod());
        addTextElement(requestElement, "path-info", request.getPathInfo());
        addTextElement(requestElement, "path-translated", request.getPathTranslated());
        addTextElement(requestElement, "query-string", request.getQueryString());
        addTextElement(requestElement, "remote-user", request.getRemoteUser());
        addTextElement(requestElement, "requested-session-id", request.getRequestedSessionId());
        addTextElement(requestElement, "request-uri", request.getRequestURI());
        addTextElement(requestElement, "request-url", request.getRequestURL());
        addTextElement(requestElement, "servlet-path", request.getServletPath());
        addTextElement(requestElement, "request-path", request.getRequestPath());

        // Portlet-specific features
        if (request.getWindowState() != null)
            addTextElement(requestElement, "window-state", request.getWindowState());
        if (request.getPortletMode() != null)
            addTextElement(requestElement, "portlet-mode", request.getPortletMode());

        return document;
    }

    private ExternalContext.Request getRequest(PipelineContext context) {
        ExternalContext externalContext = (ExternalContext) context.getAttribute(PipelineContext.EXTERNAL_CONTEXT);
        if (externalContext == null)
            throw new OXFException("Missing external context");
        return externalContext.getRequest();
    }

    private String MARK_ATTRIBUTE = "mark";
    private String MARK_ATTRIBUTE_VALUE = "true";

    private void markAncestors(Element element) {
        if (element.attribute(MARK_ATTRIBUTE) == null) {
            element.addAttribute(MARK_ATTRIBUTE, MARK_ATTRIBUTE_VALUE);
            Element parent = element.getParent();
            if (parent != null)
                markAncestors(parent);
        }
    }

    private void markDescendants(Element element) {
        for (Iterator i = element.elementIterator(); i.hasNext();) {
            Element e = (Element) i.next();
            if (e.attribute(MARK_ATTRIBUTE) == null)
                e.addAttribute(MARK_ATTRIBUTE, MARK_ATTRIBUTE_VALUE);
            markDescendants(e);
        }
    }

    private void prune(Element element) {
        Attribute attribute = element.attribute(MARK_ATTRIBUTE);
        if (attribute == null) {
            element.detach();
        } else {
            element.remove(attribute);
            final List<Element> elementsToRemove = new ArrayList<Element>();
            for (Iterator i = element.elementIterator(); i.hasNext();) {
                final Element e = (Element) i.next();
                attribute = e.attribute(MARK_ATTRIBUTE);
                if (attribute == null)
                    elementsToRemove.add(e);
                else
                    prune(e);
            }
            for (final Element elementToRemove : elementsToRemove)
                elementToRemove.detach();
        }
    }


    private Document filterRequestDocument(Document requestDocument, Node config) {

        // We always want to keep the root element (even if nothing is selected)
        requestDocument.getRootElement().addAttribute(MARK_ATTRIBUTE, MARK_ATTRIBUTE_VALUE);

        for (Iterator i = XPathUtils.selectIterator(config, "/config/*"); i.hasNext();) {
            Element includeExcludeElement = (Element) i.next();
            LocationData locationData = (LocationData) includeExcludeElement.getData();
            String includeExcludeXPath = includeExcludeElement.getText();
            if (includeExcludeElement.getName().equals(INCLUDE_ELEMENT)) {
                for (Iterator j = XPathUtils.selectIterator(requestDocument, includeExcludeXPath); j.hasNext();) {
                    Element e = referencedElement((Node) j.next(), locationData, includeExcludeXPath);
                    markAncestors(e);
                    markDescendants(e);
                }
            } else if (includeExcludeElement.getName().equals(EXCLUDE_ELEMENT)) {
                for (Iterator j = XPathUtils.selectIterator(requestDocument, includeExcludeXPath); j.hasNext();) {
                    Element e = referencedElement((Node) j.next(), locationData, includeExcludeXPath);
                    e.detach();
                }
            }
        }

        // Prune unnecessary nodes
        prune(requestDocument.getRootElement());

        return requestDocument;
    }

    private Element referencedElement(Node node, LocationData locationData, String includeExcludeXPath) {
        if (node instanceof Element) {
            return (Element) node;
        } else if (node instanceof Document) {
            return ((Document) node).getRootElement();
        } else {
            throw new ValidationException("XPath expression '" + includeExcludeXPath + "' cannot reference a '"
                    + node.getClass().getName() + "'", locationData);
        }
    }

    /**
     * Add parameters to the request element. The parameters are also all stored in the pipeline context if they are not
     * already present. The parameter map supports Object[], which can contain String but also FileItem objects.
     */
    protected static void addParameters(Context context, Element requestElement, final ExternalContext.Request request) {
        // Obtain parameters from external context
        final Map<String, Object[]> parametersMap = request.getParameterMap();
        // Check if there is at least one file upload and set this information in the pipeline context
        if (context != null) {
            for (final Object[] values : parametersMap.values()) {
                for (Object value : values) {
                    if (value instanceof FileItem) {
                        context.hasUpload = true;
                        break;
                    }
                }
            }
        }
        // Add parameters elements
        addElements(requestElement, parametersMap, "parameters", "parameter");
    }

    protected static void addAttributes(Element requestElement, final ExternalContext.Request request) {
        // Add attributes elements
        addElements(requestElement, request.getAttributesMap(), "attributes", "attribute");
    }

    protected static void addHeaders(Element requestElement, ExternalContext.Request request) {
        addElements(requestElement, request.getHeaderValuesMap(), "headers", "header");
    }

    protected static void addElements(Element requestElement, Map<String, ?> map, String name1, String name2) {
        if (map.size() >= 0) {
            final Element parametersElement = requestElement.addElement(name1);
            for (final String name : map.keySet()) {

                final Element parameterElement = parametersElement.addElement(name2);
                // Always create the name element
                parameterElement.addElement("name").addText(name);

                final Object entryValue = map.get(name);
                final Object[] values;
                if (entryValue instanceof Object[]) {
                    values = (Object[]) entryValue;
                } else {
                    values = new Object[] { entryValue };
                }

                for (int j = 0; j < values.length; j++) {
                    final Object value = values[j];

                    if (value instanceof String) {
                        // Simple String parameter
                        parameterElement.addElement("value").addText((String) value);
                    } else if (value instanceof FileItem) {
                        // Retrieve the FileItem (only for parameters)
                        final FileItem fileItem = (FileItem) value;

                        // Set meta-information element
                        if (fileItem.getName() != null)
                            parameterElement.addElement("filename").addText(fileItem.getName());
                        if (fileItem.getContentType() != null)
                            parameterElement.addElement(Headers.ContentTypeLower()).addText(fileItem.getContentType());
                        parameterElement.addElement(Headers.ContentLengthLower()).addText(Long.toString(fileItem.getSize()));

                        if (!isFileItemEmpty(fileItem)) {
                            // Create private placeholder element with parameter name as attribute
                            final Element fileItemElement = parameterElement.addElement(FILE_ITEM_ELEMENT, REQUEST_PRIVATE_NAMESPACE_URI);
                            fileItemElement.addAttribute(PARAMETER_NAME_ATTRIBUTE, name);
                            fileItemElement.addAttribute(PARAMETER_POSITION_ATTRIBUTE, Integer.toString(j));
                        } else {
                            // Just generate an empty "value" element
                            parameterElement.addElement("value");
                        }
                    } else {
                        // ignore (needed in case of attributes, which can be any Java object)
                    }
                }
            }
        }
    }

    private static boolean isFileItemEmpty(FileItem fileItem) {
        return fileItem.getSize() <= 0 && StringUtils.isBlank(fileItem.getName());
    }

    protected static void addBody(Element requestElement) {
        // This just adds a placeholder element
        requestElement.addElement("body");
    }

    @Override
    public void reset(PipelineContext context) {
        setState(context, new State());
    }

    private static class State extends DigestState {
        public QName requestedStreamType;
        public boolean bodyRequested;
        public Document requestDocument;
        public boolean isSessionScope;
    }

    private static Context getContext(PipelineContext pipelineContext) {
        Context context = (Context) pipelineContext.getAttribute(REQUEST_GENERATOR_CONTEXT);
        if (context == null) {
            context = new Context();
            pipelineContext.setAttribute(REQUEST_GENERATOR_CONTEXT, context);
        }
        return context;
    }

    /**
     * This context is kept in the PipelineContext so that if multiple Request generators are run,
     * common information is reused.
     */
    private static class Context {
        public Document wholeRequest;
        public boolean hasUpload;
        public FileItem bodyFileItem;

        // Hold mapping from URI expiring w/ request (provided by Request object) to URI expiring w/ session (created here)
        private Map<String, String> uriMap;

        public String getSessionURIForRequestURI(String requestURI) {
            return (uriMap == null) ? null : uriMap.get(requestURI);
        }

        public void putSessionURIForRequestURI(String requestURI, String sessionURI) {
            if (uriMap == null)
                uriMap = new HashMap<String, String>();

            uriMap.put(requestURI, sessionURI);
        }
    }

    public static int getMaxSizeProperty() {
        PropertySet propertySet = Properties.instance().getPropertySet(XMLConstants.REQUEST_PROCESSOR_QNAME);
        Integer maxSizeProperty = propertySet.getInteger(RequestGenerator.MAX_UPLOAD_SIZE_PROPERTY);
        return (maxSizeProperty != null) ? maxSizeProperty.intValue() : RequestGenerator.DEFAULT_MAX_UPLOAD_SIZE;
    }

    public static int getMaxMemorySizeProperty() {
        PropertySet propertySet = org.orbeon.oxf.properties.Properties.instance().getPropertySet(XMLConstants.REQUEST_PROCESSOR_QNAME);
        Integer maxMemorySizeProperty = propertySet.getInteger(RequestGenerator.MAX_UPLOAD_MEMORY_SIZE_PROPERTY);
        return (maxMemorySizeProperty != null) ? maxMemorySizeProperty.intValue() : RequestGenerator.DEFAULT_MAX_UPLOAD_MEMORY_SIZE;
    }

    public static String getRequestBody(PipelineContext pipelineContext) {
        final Object result = pipelineContext.getAttribute(BODY_REQUEST_ATTRIBUTE);
        return (result instanceof String) ? ((String) result) : null;
    }
}
