'Stream closed' error when an action triggered by xforms-submit-done fails

classic Classic list List threaded Threaded
2 messages Options
Reply | Threaded
Open this post in threaded view
|

'Stream closed' error when an action triggered by xforms-submit-done fails

Adrian Baker-2
I was getting a confusing error on submission:

2006-08-07 12:35:17,306 DEBUG
org.orbeon.oxf.xforms.event.events.XFormsSubmitErrorEvent  -
xforms-submit-error throwable: java.io.IOException: Stream closed
    at java.io.BufferedInputStream.ensureOpen(BufferedInputStream.java:120)
    at java.io.BufferedInputStream.read(BufferedInputStream.java:199)
    at
org.orbeon.oxf.xforms.XFormsModelSubmission.connectionHasContent(XFormsModelSubmission.java:682)
    at
org.orbeon.oxf.xforms.XFormsModelSubmission.createErrorEvent(XFormsModelSubmission.java:628)
    at
org.orbeon.oxf.xforms.XFormsModelSubmission.performDefaultAction(XFormsModelSubmission.java:550)
    at
org.orbeon.oxf.xforms.XFormsContainingDocument.dispatchEvent(XFormsContainingDocument.java:852)
    at
org.orbeon.oxf.xforms.action.actions.XFormsSendAction.execute(XFormsSendAction.java:45)
    at
org.orbeon.oxf.xforms.action.XFormsActionInterpreter.runAction(XFormsActionInterpreter.java:118)
    at
org.orbeon.oxf.xforms.action.actions.XFormsActionAction.execute(XFormsActionAction.java:32)
    at
org.orbeon.oxf.xforms.action.XFormsActionInterpreter.runAction(XFormsActionInterpreter.java:118)
    at
org.orbeon.oxf.xforms.XFormsContainingDocument.runAction(XFormsContainingDocument.java:904)
    at
org.orbeon.oxf.xforms.event.XFormsEventHandlerImpl.handleEvent(XFormsEventHandlerImpl.java:85)
    at
org.orbeon.oxf.xforms.XFormsContainingDocument.dispatchEvent(XFormsContainingDocument.java:833)
    at
org.orbeon.oxf.xforms.XFormsContainingDocument.interpretEvent(XFormsContainingDocument.java:533)
    at
org.orbeon.oxf.xforms.XFormsContainingDocument.executeExternalEvent(XFormsContainingDocument.java:521)
    at
org.orbeon.oxf.xforms.processor.XFormsServer.executeExternalEventPrepareIfNecessary(XFormsServer.java:284)
    at
org.orbeon.oxf.xforms.processor.XFormsServer.doIt(XFormsServer.java:234)

Debugging through, it's due to an action failing which is triggered by
the xforms-submit-done event. Firstly, is this placement of this
dispatchEvent call inside the exception handling block in
XFormsModelSubmission correct?

                // Notify that submission is done
                containingDocument.dispatchEvent(pipelineContext, new
XFormsSubmitDoneEvent(XFormsModelSubmission.this));
            }
        } catch (Exception e) {
            submitErrorEvent = createErrorEvent(connectionResult);
            throw new OXFException("xforms:submission: exception while
serializing XML to instance.", e);
        }

 From what I can see this will try and create an xforms-submit-error
event when there is an exception processing any action triggered by
xforms-submit-done.

Secondly, the error itself is caused because the connectionHasContent
method ends up being called with a BufferedInputStream which wraps a
null input stream (when Dom4jUtils.read is called from  
XFormsModelSubmission a BufferedInputStream wrapping an
org.apache.commons.httpclient.AutoCloseInputStream morphs into a
BufferedInputStream wrapping a null InputStream, presumably when the
close() method is invoked).

The connectionHasContent method could be changed to return false in this
case, but this will probably just mask the real problem (and incorrectly
return false): that connectionResult.resultInputStream is being treated
as a stream which can be read from multiple times. I can see logic in
connectionHasContent to try and avoid this problem by using mark() &
reset(), but this doesn't work when the underlying stream gets closed.

I'd suggest the ConnectionResult class encapsulate the
connectionHasContent boolean, and only set this value when the input
stream is set. Attached is a patch which does this (there's a couple of
classes which need to be changed to use the new getter/setter for the
inputstream which I haven't bothered including).

Adrian

Adrian

/**
 *  Copyright (C) 2005 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.xforms;

import org.apache.log4j.Logger;
import org.dom4j.*;
import org.dom4j.io.DocumentSource;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.pipeline.api.ExternalContext;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.processor.ProcessorUtils;
import org.orbeon.oxf.util.LoggerFactory;
import org.orbeon.oxf.util.NetUtils;
import org.orbeon.oxf.xforms.action.actions.XFormsLoadAction;
import org.orbeon.oxf.xforms.control.controls.XFormsUploadControl;
import org.orbeon.oxf.xforms.event.*;
import org.orbeon.oxf.xforms.event.events.XFormsBindingExceptionEvent;
import org.orbeon.oxf.xforms.event.events.XFormsSubmitDoneEvent;
import org.orbeon.oxf.xforms.event.events.XFormsSubmitErrorEvent;
import org.orbeon.oxf.xforms.event.events.XXFormsSubmissionEvent;
import org.orbeon.oxf.xforms.mip.BooleanModelItemProperty;
import org.orbeon.oxf.xforms.mip.ValidModelItemProperty;
import org.orbeon.oxf.xml.TransformerUtils;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.XMLUtils;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.oxf.xml.dom4j.LocationDocumentResult;
import org.orbeon.saxon.dom4j.NodeWrapper;
import org.orbeon.saxon.om.DocumentInfo;
import org.orbeon.saxon.om.NodeInfo;

import javax.xml.transform.Transformer;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.net.URI;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Represents an XForms model submission instance.
 *
 * TODO: This badly needs to be modularized instead of being a soup of "ifs"!
 */
public class XFormsModelSubmission implements XFormsEventTarget, XFormsEventHandlerContainer {

        public final static Logger logger = LoggerFactory.createLogger(XFormsModelSubmission.class);

    public static final String DEFAULT_TEXT_READING_ENCODING = "iso-8859-1";

    private final XFormsContainingDocument containingDocument;
    private final String id;
    private final XFormsModel model;
    private final Element submissionElement;
    private boolean submissionElementExtracted = false;

    // Event handlers
    private final List eventHandlers;

    private String avtAction; // required
    private String resolvedAction;
    private String method; // required

    private boolean validate = true;
    private boolean relevant = true;

    private String version;
    private boolean indent;
    private String mediatype;
    private String encoding;
    private boolean omitxmldeclaration;
    private Boolean standalone;
    private String cdatasectionelements;

    private String replace = XFormsConstants.XFORMS_SUBMIT_REPLACE_ALL;
    private String replaceInstanceId;
    private String separator = ";";
    private String includenamespaceprefixes;

    private String avtXXFormsUsername;
    private String resolvedXXFormsUsername;
    private String avtXXFormsPassword;
    private String resolvedXXFormsPassword;

    public XFormsModelSubmission(XFormsContainingDocument containingDocument, String id, Element submissionElement, XFormsModel model) {
        this.containingDocument = containingDocument;
        this.id = id;
        this.submissionElement = submissionElement;
        this.model = model;

        // Extract event handlers
        eventHandlers = XFormsEventHandlerImpl.extractEventHandlers(containingDocument, this, submissionElement);
    }

    public XFormsContainingDocument getContainingDocument() {
        return containingDocument;
    }

    public Element getSubmissionElement() {
        return submissionElement;
    }

    private void extractSubmissionElement() {
        if (!submissionElementExtracted) {

            avtAction = submissionElement.attributeValue("action");
            method = submissionElement.attributeValue("method");
            method = Dom4jUtils.qNameToexplodedQName(Dom4jUtils.extractAttributeValueQName(submissionElement, "method"));

            validate = !"false".equals(submissionElement.attributeValue("validate"));
            relevant = !"false".equals(submissionElement.attributeValue("relevant"));

            version = submissionElement.attributeValue("version");

            if (submissionElement.attributeValue("indent") != null) {
                indent = Boolean.valueOf(submissionElement.attributeValue("indent")).booleanValue();
            }
            mediatype = submissionElement.attributeValue("mediatype");
            encoding = submissionElement.attributeValue("encoding");
            if (submissionElement.attributeValue("omitxmldeclaration") != null) {
                omitxmldeclaration = Boolean.valueOf(submissionElement.attributeValue("omit-xml-declaration")).booleanValue();
            }
            if (submissionElement.attributeValue("standalone") != null) {
                standalone = new Boolean(submissionElement.attributeValue("standalone"));
            }

            cdatasectionelements = submissionElement.attributeValue("cdata-section-elements");
            if (submissionElement.attributeValue("replace") != null) {
                replace = submissionElement.attributeValue("replace");

                if (replace.equals("instance")) {
                    replaceInstanceId = XFormsUtils.namespaceId(containingDocument, submissionElement.attributeValue("instance"));
                }
            }
            if (submissionElement.attributeValue("separator") != null) {
                separator = submissionElement.attributeValue("separator");
            }
            includenamespaceprefixes = submissionElement.attributeValue("includenamespaceprefixes");

            // Extension: username and password
            avtXXFormsUsername = submissionElement.attributeValue(XFormsConstants.XXFORMS_USERNAME_QNAME);
            avtXXFormsPassword = submissionElement.attributeValue(XFormsConstants.XXFORMS_PASSWORD_QNAME);

            // Remember that we did this
            submissionElementExtracted = true;
        }
    }

    private boolean isMethodOptimizedLocalSubmission() {
        return method.startsWith(XMLUtils.buildExplodedQName(XFormsConstants.XXFORMS_NAMESPACE_URI, ""))
                && (XFormsSubmissionUtils.isGet(method) || XFormsSubmissionUtils.isPost(method) || XFormsSubmissionUtils.isPut(method));
    }

    public String getEffectiveId() {
        return id;
    }

    public LocationData getLocationData() {
        return (LocationData) submissionElement.getData();
    }

    public XFormsEventHandlerContainer getParentContainer() {
        return model;
    }

    public List getEventHandlers() {
        return eventHandlers;
    }

    public void performDefaultAction(PipelineContext pipelineContext, XFormsEvent event) {
        final String eventName = event.getEventName();

        if (XFormsEvents.XFORMS_SUBMIT.equals(eventName) || XFormsEvents.XXFORMS_SUBMIT.equals(eventName)) {
            // 11.1 The xforms-submit Event
            // Bubbles: Yes / Cancelable: Yes / Context Info: None

            containingDocument.setGotSubmission(true);

            boolean isDeferredSubmissionSecondPass = false;
            XFormsSubmitErrorEvent submitErrorEvent = null;
            try {
                // Make sure submission element info is extracted
                extractSubmissionElement();

                final boolean isReplaceAll = replace.equals(XFormsConstants.XFORMS_SUBMIT_REPLACE_ALL);
                final boolean isReplaceInstance = replace.equals(XFormsConstants.XFORMS_SUBMIT_REPLACE_INSTANCE);

                final boolean isHandlingOptimizedGet = XFormsUtils.isOptimizeGetAllSubmission() && XFormsSubmissionUtils.isGet(method) && isReplaceAll;

                //noinspection UnnecessaryLocalVariable
                final boolean isDeferredSubmission = isReplaceAll;
                final boolean isDeferredSubmissionFirstPass = isDeferredSubmission && XFormsEvents.XFORMS_SUBMIT.equals(eventName) && !isHandlingOptimizedGet;
                isDeferredSubmissionSecondPass = isDeferredSubmission && !isDeferredSubmissionFirstPass;

                final XFormsControls xformsControls = containingDocument.getXFormsControls();

                // Get node to submit
                final Node currentNode;
                {
                    // TODO FIXME: the submission element is not a control, so we shouldn't use XFormsControls.
                    // "The default value is '/'."
                    final String refAttribute = (submissionElement.attributeValue("ref") != null)
                            ? submissionElement.attributeValue("ref") : "/";
                    xformsControls.resetBindingContext();
                    xformsControls.pushBinding(pipelineContext, refAttribute, null, null, model.getEffectiveId(), null, submissionElement,
                            Dom4jUtils.getNamespaceContextNoDefault(submissionElement));

                    // Check that we have a current node and that it is pointing to a document or an element
                    final NodeInfo currentNodeInfo = xformsControls.getCurrentSingleNode();

                    if (currentNodeInfo == null)
                        throw new OXFException("Empty single-node binding on xforms:submission for submission id: " + id);

                    if (!(currentNodeInfo instanceof DocumentInfo || currentNodeInfo.getNodeKind() == org.w3c.dom.Document.ELEMENT_NODE)) {
                        throw new OXFException("xforms:submission: single-node binding must refer to a document node or an element.");
                    }

                    // For now, we can't submit a read-only instance (but we could in the future)
                    if (!(currentNodeInfo instanceof NodeWrapper))
                        throw new OXFException("xforms:submission: submitting a read-only instance is not yet implemented.");

                    currentNode = (Node) ((NodeWrapper) currentNodeInfo).getUnderlyingNode();
                }

                // Evaluate AVTs
                // TODO FIXME: the submission element is not a control, so we shouldn't use XFormsControls.
                resolvedAction = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, xformsControls, submissionElement, avtAction);
                resolvedXXFormsUsername = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, xformsControls, submissionElement, avtXXFormsUsername);
                resolvedXXFormsPassword = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, xformsControls, submissionElement, avtXXFormsPassword);

                final XFormsInstance currentInstance = xformsControls.getCurrentInstance();

                final Document initialDocumentToSubmit;
                if (!isDeferredSubmissionSecondPass) {
                    // Create document to submit
                    final Document backupInstanceDocument = currentInstance.getInstanceDocument();
                    try {
                        initialDocumentToSubmit = createDocumentToSubmit(currentNode, currentInstance);
                        currentInstance.setInstanceDocument(initialDocumentToSubmit, false);

                        // Revalidate instance
                        model.doRevalidate(pipelineContext);
                        // TODO: Check if the validation state can really change. If so, find a solution.
                        // "no notification events are marked for dispatching due to this operation"

                        // Check that there are no validation errors
                        final boolean instanceSatisfiesValidRequired = isDocumentSatisfiesValidRequired(initialDocumentToSubmit);
                        if (!instanceSatisfiesValidRequired) {
//                            {
//                                currentInstance.readOut();
//                            }
                            if (logger.isDebugEnabled()) {
                                final LocationDocumentResult documentResult = new LocationDocumentResult();
                                final TransformerHandler identity = TransformerUtils.getIdentityTransformerHandler();
                                identity.setResult(documentResult);
                                currentInstance.read(identity);
                                final String documentString = Dom4jUtils.domToString(documentResult.getDocument());

                                logger.debug("XForms - instance document or subset thereof cannot be submitted:\n" + documentString);
                            }
                            throw new OXFException("xforms:submission: instance to submit does not satisfy valid and/or required model item properties.");
                        }
                    } finally {
                        currentInstance.setInstanceDocument(backupInstanceDocument, false);
                    }
                } else {
                    initialDocumentToSubmit = null;
                }

                // Deferred submission: end of the first pass
                if (isDeferredSubmissionFirstPass) {
                    // When replace="all", we wait for the submission of an XXFormsSubmissionEvent from the client
                    containingDocument.setClientActiveSubmission(this);
                    return;
                }

                final Document documentToSubmit;
                if (isDeferredSubmissionSecondPass) {
                    // Handle uploaded files if any
                    final Element filesElement = (event instanceof XXFormsSubmissionEvent) ? ((XXFormsSubmissionEvent) event).getFilesElement() : null;
                    if (filesElement != null) {
                        for (Iterator i = filesElement.elements().iterator(); i.hasNext();) {
                            final Element parameterElement = (Element) i.next();
                            final String name = parameterElement.element("name").getTextTrim();

                            final Element valueElement = parameterElement.element("value");
                            final String value = valueElement.getTextTrim();

                            // An empty value likely means that the user did not select a file. In this case, we don't
                            // want to override an existing value in the instance. Clearing the value in the instance
                            // will have to be done by other means, like a "clear file" button.
                            if (value.length() == 0)
                                continue;

                            final String paramValueType = Dom4jUtils.qNameToexplodedQName(Dom4jUtils.extractAttributeValueQName(valueElement, XMLConstants.XSI_TYPE_QNAME));

                            final String filename = parameterElement.element("filename").getTextTrim();
                            final String mediatype = parameterElement.element("content-type").getTextTrim();
                            final String size = parameterElement.element("content-length").getTextTrim();

                            final XFormsUploadControl uploadControl
                                    = (XFormsUploadControl) containingDocument.getObjectById(pipelineContext, name);

                            if (uploadControl != null)
                            { // in case of xforms:repeat, the name of the template will not match an existing control
                                // Set value into the instance
                                xformsControls.setBinding(pipelineContext, uploadControl);
                                {
                                    final NodeInfo currentSingleNode = xformsControls.getCurrentSingleNode();
                                    XFormsInstance.setValueForNodeInfo(pipelineContext, currentSingleNode, value, paramValueType);
                                }

                                // Handle filename if any
                                if (uploadControl.getFilenameElement() != null) {
                                    xformsControls.pushBinding(pipelineContext, uploadControl.getFilenameElement());
                                    final NodeInfo currentSingleNode = xformsControls.getCurrentSingleNode();
                                    XFormsInstance.setValueForNodeInfo(pipelineContext, currentSingleNode, filename, null);
                                    xformsControls.popBinding();
                                }

                                // Handle mediatype if any
                                if (uploadControl.getMediatypeElement() != null) {
                                    xformsControls.pushBinding(pipelineContext, uploadControl.getMediatypeElement());
                                    final NodeInfo currentSingleNode = xformsControls.getCurrentSingleNode();
                                    XFormsInstance.setValueForNodeInfo(pipelineContext, currentSingleNode, mediatype, null);
                                    xformsControls.popBinding();
                                }

                                // Handle file size if any
                                if (uploadControl.getSizeElement() != null) {
                                    xformsControls.pushBinding(pipelineContext, uploadControl.getSizeElement());
                                    final NodeInfo currentSingleNode = xformsControls.getCurrentSingleNode();
                                    XFormsInstance.setValueForNodeInfo(pipelineContext, currentSingleNode, size, null);
                                    xformsControls.popBinding();
                                }
                            }
                        }
                    }

                    // Create document to submit
                    final Document backupInstanceDocument = currentInstance.getInstanceDocument();
                    try {
                        documentToSubmit = createDocumentToSubmit(currentNode, currentInstance);
                        currentInstance.setInstanceDocument(documentToSubmit, false);

                        // Revalidate instance
                        model.doRevalidate(pipelineContext);
                        // sent out. Check if the validation state can really change. If so, find a
                        // solution.
                        // "no notification events are marked for dispatching due to this operation"

                        // Check that there are no validation errors
                        final boolean instanceSatisfiesValidRequired = isDocumentSatisfiesValidRequired(documentToSubmit);
                        if (!instanceSatisfiesValidRequired) {
    //                        currentInstance.readOut();// FIXME: DEBUG
                            throw new OXFException("xforms:submission: instance to submit does not satisfy valid and/or required model item properties.");
                        }
                    } finally {
                        currentInstance.setInstanceDocument(backupInstanceDocument, false);
                    }
                } else {
                    // Don't recreate document
                    documentToSubmit = initialDocumentToSubmit;
                }

                // Fire xforms-submit-serialize
                containingDocument.dispatchEvent(pipelineContext,
                XFormsEventFactory.createEvent(XFormsEvents.XFORMS_SUBMIT_SERIALIZE, XFormsModelSubmission.this));
                // TODO: what follows must be executed as the default action of xforms-submit-serialize

                // Serialize
                // To support: application/xml, application/x-www-form-urlencoded, multipart/related, multipart/form-data
                final byte[] serializedInstance;
                final String serializedInstanceString;
                {
                    if (XFormsSubmissionUtils.isPost(method) || XFormsSubmissionUtils.isPut(method)) {

                        try {
                            final Transformer identity = TransformerUtils.getIdentityTransformer();
                            TransformerUtils.applyOutputProperties(identity,
                                    "xml", version, null, null, encoding, omitxmldeclaration, standalone, indent, 4);

                            // TODO: use cdata-section-elements

                            final ByteArrayOutputStream os = new ByteArrayOutputStream();
                            identity.transform(new DocumentSource(documentToSubmit), new StreamResult(os));
                            serializedInstance = os.toByteArray();
                        } catch (Exception e) {
                            throw new OXFException("xforms:submission: exception while serializing instance to XML.", e);
                        }
                        serializedInstanceString = null;

                    } else if (XFormsSubmissionUtils.isGet(method) || XFormsSubmissionUtils.isDelete(method)) {

                        // Perform "application/x-www-form-urlencoded" serialization
                        serializedInstanceString = createWwwFormUrlEncoded(documentToSubmit);
                        serializedInstance = null;

                    } else if (method.equals("multipart-post")) {
                        // TODO
                        throw new OXFException("xforms:submission: submission method not yet implemented: " + method);
                    } else if (method.equals("form-data-post")) {
                        // TODO
                        throw new OXFException("xforms:submission: submission method not yet implemented: " + method);
                    } else if (method.equals("urlencoded-post")) {
                        throw new OXFException("xforms:submission: deprecated submission method requested: " + method);
                    } else {
                        throw new OXFException("xforms:submission: invalid submission method requested: " + method);
                    }
                }

                final ExternalContext externalContext = (ExternalContext) pipelineContext.getAttribute(PipelineContext.EXTERNAL_CONTEXT);


                // Result information
                ConnectionResult connectionResult = null;
                try {
                    if (isReplaceInstance && resolvedAction.startsWith("test:")) {
                        // Test action

                        if (serializedInstance == null)
                            throw new OXFException("Action 'test:' can only be used with POST method.");

                        connectionResult = new ConnectionResult(null);
                        connectionResult.resultCode = 200;
                        connectionResult.resultHeaders = new HashMap();
                        connectionResult.resultMediaType = "application/xml";
                        connectionResult.dontHandleResponse = false;
                        connectionResult.resultInputStream = new ByteArrayInputStream(serializedInstance);

                    } else if (isHandlingOptimizedGet) {
                        // GET with replace="all": we can optimize and tell the client to just load the URL
                        connectionResult = doOptimizedGet(pipelineContext, serializedInstanceString);
                    } else if (!NetUtils.urlHasProtocol(resolvedAction)
                               && (externalContext.getRequest().getContainerType().equals("portlet")
                                    || (externalContext.getRequest().getContainerType().equals("servlet")
                                        && (XFormsUtils.isOptimizeLocalSubmission() || isMethodOptimizedLocalSubmission())
                                        &&  isReplaceAll))) {

                        // This is an "optimized" submission, i.e. one that does not use an actual
                        // protocol handler to access the resource

                        // NOTE: Optimizing with include() for servlets doesn't allow detecting
                        // errors caused by the included resource, so we don't allow this for now.

                        // NOTE: For portlets, paths are served directly by the portlet, NOT as
                        // resources.

                        // Current limitations:
                        // o Portlets cannot access resources outside the portlet except by using absolute URLs
                        // o Servlets cannot access resources on the same serer but not in the current application
                        //   except by using absolute URLs

                        final URI resolvedURI = XFormsUtils.resolveURI(submissionElement, resolvedAction);
                        connectionResult = XFormsSubmissionUtils.doOptimized(pipelineContext, externalContext,
                                this, method, resolvedURI.toString(), mediatype, isReplaceAll,
                                serializedInstance, serializedInstanceString);

                    } else {
                        // This is a regular remote submission going through a protocol handler

                        // Absolute URLs or absolute paths are allowed to a local servlet
                        final String resolvedURL = XFormsUtils.resolveURL(containingDocument, pipelineContext, submissionElement, false, resolvedAction);
                        connectionResult = XFormsSubmissionUtils.doRegular(pipelineContext, externalContext,
                                method, resolvedURL, resolvedXXFormsUsername, resolvedXXFormsPassword, mediatype, isReplaceAll,
                                serializedInstance, serializedInstanceString);
                    }

                    if (!connectionResult.dontHandleResponse) {
                        // Handle response
                        if (connectionResult.resultCode >= 200 && connectionResult.resultCode < 300) {// accept any success code (in particular "201 Resource Created")
                            // Sucessful response
                            if (connectionResult.hasContent) {
                                // There is a body

                                if (isReplaceAll) {
                                    // When we get here, we are in a mode where we need to send the reply
                                    // directly to an external context, if any.

                                    // "the event xforms-submit-done is dispatched"
                                    containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this));

                                    final ExternalContext.Response response = externalContext.getResponse();

                                    // Forward headers to response
                                    if (connectionResult.resultHeaders != null) {
                                        for (Iterator i = connectionResult.resultHeaders.entrySet().iterator(); i.hasNext();) {
                                            final Map.Entry currentEntry = (Map.Entry) i.next();
                                            final String headerName = (String) currentEntry.getKey();
                                            final String headerValue = (String) currentEntry.getValue();

                                            // NOTE: We only get one header value per name
                                            if (headerName != null && headerValue != null) {
                                                response.addHeader(headerName, headerValue);
                                            }

//                                            if (headerName != null && headerValues != null) {
//                                                for (Iterator j = headerValues.iterator(); j.hasNext();) {
//                                                    response.addHeader(headerName, (String) j.next());
//                                                }
//                                            }
                                        }
                                    }

                                    // Forward content to response
                                    NetUtils.copyStream(connectionResult.resultInputStream, response.getOutputStream());

                                } else if (isReplaceInstance) {

                                    if (ProcessorUtils.isXMLContentType(connectionResult.resultMediaType)) {
                                        // Handling of XML media type
                                        try {
                                            // Read stream into Document
                                            final Document resultingInstanceDocument = Dom4jUtils.read(connectionResult.resultInputStream);

                                            // Set new instance document to replace the one submitted
                                            final XFormsInstance replaceInstance = (replaceInstanceId == null) ? currentInstance : model.getInstance(replaceInstanceId);
                                            if (replaceInstance == null) {
                                                containingDocument.dispatchEvent(pipelineContext, new XFormsBindingExceptionEvent(XFormsModelSubmission.this));
                                            } else {

                                                // Set new instance
                                                replaceInstance.setInstanceDocument(resultingInstanceDocument, true);

                                                // Mark all values as changed so that refresh sends appropriate events
                                                XFormsUtils.markAllValuesChanged(replaceInstance);

                                                // Handle new instance and associated events
                                                model.handleNewInstanceDocuments(pipelineContext);

                                                // Notify that submission is done
                                                containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this));
                                            }
                                        } catch (Exception e) {
                                            submitErrorEvent = createErrorEvent(connectionResult);
                                            throw new OXFException("xforms:submission: exception while serializing XML to instance.", e);
                                        }
                                    } else {
                                        // Other media type
                                        submitErrorEvent = createErrorEvent(connectionResult);
                                        throw new OXFException("Body received with non-XML media type for replace=\"instance\": " + connectionResult.resultMediaType);
                                    }
                                } else if (replace.equals(XFormsConstants.XFORMS_SUBMIT_REPLACE_NONE)) {
                                    // Just notify that processing is terminated
                                    containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this));
                                } else {
                                    submitErrorEvent = createErrorEvent(connectionResult);
                                    throw new OXFException("xforms:submission: invalid replace attribute: " + replace);
                                }

                            } else {
                                // There is no body, notify that processing is terminated
                                containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this));
                            }
                        } else if (connectionResult.resultCode == 302 || connectionResult.resultCode == 301) {
                            // Got a redirect

                            final ExternalContext.Response response = externalContext.getResponse();

                            // Forward headers to response
                            // TODO: this is duplicated from above
                            if (connectionResult.resultHeaders != null) {
                                for (Iterator i = connectionResult.resultHeaders.entrySet().iterator(); i.hasNext();) {
                                    final Map.Entry currentEntry = (Map.Entry) i.next();
                                    final String headerName = (String) currentEntry.getKey();
                                    final List headerValues = (List) currentEntry.getValue();

                                    if (headerName != null && headerValues != null) {
                                        for (Iterator j = headerValues.iterator(); j.hasNext();) {
                                            response.addHeader(headerName, (String) j.next());
                                        }
                                    }
                                }
                            }

                            // Forward redirect
                            response.setStatus(connectionResult.resultCode);

                        } else {
                            // Error code received
                            submitErrorEvent = createErrorEvent(connectionResult);
                            throw new OXFException("Error code received when submitting instance: " + connectionResult.resultCode);
                        }
                    }
                } finally {
                    // Clean-up
                    if (connectionResult != null) {
                        connectionResult.close();
                    }
                }
            } catch (Throwable e) {
                if (isDeferredSubmissionSecondPass && XFormsUtils.isOptimizePostAllSubmission()) {
                    // It doesn't serve any purpose here to dispatch an event, so we just propagate the exception
                    throw new OXFException(e);
                } else {
                    // Any exception will cause an error event to be dispatched
                    if (submitErrorEvent == null)
                        submitErrorEvent = new XFormsSubmitErrorEvent(XFormsModelSubmission.this, resolvedAction);

                    submitErrorEvent.setThrowable(e);
                    containingDocument.dispatchEvent(pipelineContext, submitErrorEvent);
                }
            }

        } else if (XFormsEvents.XFORMS_BINDING_EXCEPTION.equals(eventName)) {
            // The default action for this event results in the following: Fatal error.
            throw new OXFException("Binding exception.");
        }
    }

    private XFormsSubmitErrorEvent createErrorEvent(ConnectionResult connectionResult) throws IOException {
        XFormsSubmitErrorEvent submitErrorEvent = null;
        if (connectionResult.hasContent) {
            if (ProcessorUtils.isXMLContentType(connectionResult.resultMediaType)) {
                // XML content-type
                // TODO: XForms 1.1 may mandate that we always try to parse the body as XML first
                // Read stream into Document
                final DocumentInfo responseBody
                        = TransformerUtils.readTinyTree(connectionResult.resultInputStream, connectionResult.resourceURI);

                submitErrorEvent = new XFormsSubmitErrorEvent(XFormsModelSubmission.this, resolvedAction);
                submitErrorEvent.setBodyDocument(responseBody);
            } else if (ProcessorUtils.isTextContentType(connectionResult.resultMediaType)) {
                // Text content-type
                // Read stream into String
                final String charset;
                {
                    final String connectionCharset = NetUtils.getContentTypeCharset(connectionResult.resultMediaType);
                    if (connectionCharset != null)
                        charset = connectionCharset;
                    else
                        charset = DEFAULT_TEXT_READING_ENCODING;
                }
                final Reader reader = new InputStreamReader(connectionResult.resultInputStream, charset);
                final String responseBody = NetUtils.readStreamAsString(reader);
                submitErrorEvent = new XFormsSubmitErrorEvent(XFormsModelSubmission.this, resolvedAction);
                submitErrorEvent.setBodyString(responseBody);
            } else {
                // This is binary
                // Don't store anything for now
            }
        }
        return submitErrorEvent;
    }

//    private Document readConnectionAsDocument(InputStream inputStream) throws TransformerException, IOException {
//
//        final ByteArrayOutputStream resultByteArrayOutputStream = new ByteArrayOutputStream();
//        NetUtils.copyStream(inputStream, resultByteArrayOutputStream);
//        byte[] submissionResponse = resultByteArrayOutputStream.toByteArray();
//
//        final Transformer identity = TransformerUtils.getIdentityTransformer();
//        final LocationDocumentResult documentResult = new LocationDocumentResult();
//        identity.transform(new StreamSource(new ByteArrayInputStream(submissionResponse)), documentResult);
//        return documentResult.getDocument();
//    }

    private ConnectionResult doOptimizedGet(PipelineContext pipelineContext, String serializedInstanceString) {
        final String actionString = resolvedAction + ((resolvedAction.indexOf('?') == -1) ? "?" : "") + serializedInstanceString;
        final String resultURL = XFormsLoadAction.resolveLoadValue(containingDocument, pipelineContext, submissionElement, true, actionString, null, null);
        final ConnectionResult connectionResult = new ConnectionResult(resultURL);
        connectionResult.dontHandleResponse = true;
        return connectionResult;
    }

    private Document createDocumentToSubmit(final Node currentNode, final XFormsInstance currentInstance) {

        // "A node from the instance data is selected, based on attributes on the submission
        // element. The indicated node and all nodes for which it is an ancestor are considered for
        // the remainder of the submit process. "
        final Document documentToSubmit;
        if (currentNode instanceof Element) {
            // Create subset of document
            documentToSubmit = Dom4jUtils.createDocument((Element) currentNode);
        } else {
            // Use entire instance document
            documentToSubmit = Dom4jUtils.createDocument(currentInstance.getInstanceDocument().getRootElement());
        }

        if (relevant) {
            // "Any node which is considered not relevant as defined in 6.1.4 is removed."
            final Node[] nodeToDetach = new Node[1];
            do {
                // NOTE: This is not very efficient, but at least we avoid NPEs that we would get by
                // detaching elements within accept(). Should implement a more efficient algorithm to
                // prune non-relevant nodes.
                nodeToDetach[0] = null;
                documentToSubmit.accept(new VisitorSupport() {

                    public final void visit(Element element) {
                        checkInstanceData(element);
                    }

                    public final void visit(Attribute attribute) {
                        checkInstanceData(attribute);
                    }

                    private final void checkInstanceData(Node node) {
                        if (nodeToDetach[0] == null) {
                            final InstanceData instanceData = XFormsUtils.getInstanceDataUpdateInherited(node);
                            // Check "relevant" MIP and remove non-relevant nodes
                            {
                                final BooleanModelItemProperty relevantMIP = instanceData.getInheritedRelevant();
                                if (relevantMIP != null && !relevantMIP.get())
                                    nodeToDetach[0] = node;
                            }
                        }
                    }
                });
                if (nodeToDetach[0] != null)
                    nodeToDetach[0].detach();

            } while (nodeToDetach[0] != null);
        }

        // TODO: handle includenamespaceprefixes
        return documentToSubmit;
    }

    private String createWwwFormUrlEncoded(final Document document) {

        final StringBuffer sb = new StringBuffer();
        document.accept(new VisitorSupport() {
            public final void visit(Element element) {
                // We only care about elements

                final List children = element.elements();
                if (children == null || children.size() == 0) {
                    // Only consider leaves
                    final String text = element.getText();
                    if (text != null && text.length() > 0) {
                        // Got one!
                        final String localName = element.getName();

                        if (sb.length() > 0)
                            sb.append(separator);

                        try {
                            sb.append(URLEncoder.encode(localName, "utf-8"));
                            sb.append('=');
                            sb.append(URLEncoder.encode(text, "utf-8"));
                            // TODO: check if line breaks will be correcly encoded as "%0D%0A"
                        } catch (UnsupportedEncodingException e) {
                            // Should not happen: utf-8 must be supported
                            throw new OXFException(e);
                        }
                    }
                }
            }
        });

        return sb.toString();
    }

    private boolean isDocumentSatisfiesValidRequired(final Document documentToSubmit) {
        final boolean[] instanceSatisfiesValidRequired = new boolean[]{true};
        if (validate) {
            documentToSubmit.accept(new VisitorSupport() {

                public final void visit(Element element) {
                    final InstanceData instanceData = XFormsUtils.getLocalInstanceData(element);
                    final boolean valid = checkInstanceData(instanceData);

                    instanceSatisfiesValidRequired[0] &= valid;

                    if (!valid && logger.isDebugEnabled()) {
                        logger.debug("Found invalid element: " + element.getQName() + ", value:" + element.getText());
                    }
                }

                public final void visit(Attribute attribute) {
                    final InstanceData instanceData = XFormsUtils.getLocalInstanceData(attribute);
                    final boolean valid = checkInstanceData(instanceData);

                    instanceSatisfiesValidRequired[0] &= valid;

                    if (!valid && logger.isDebugEnabled()) {
                        logger.debug("Found invalid attribute: " + attribute.getQName() + ", value:" + attribute.getValue());
                    }
                }

                private final boolean checkInstanceData(InstanceData instanceData) {
                    // Check "valid" MIP
                    {
                        final BooleanModelItemProperty validMIP = instanceData.getValid();
                        if (validMIP != null && !validMIP.get())
                            return false;
                    }
                    // Check "required" MIP
                    {
                        final ValidModelItemProperty requiredMIP = instanceData.getRequired();
                        if (requiredMIP != null && requiredMIP.get() && requiredMIP.getStringValue().length() == 0) {
                            // Required and empty
                            return false;
                        }
                    }
                    return true;
                }
            });
        }
        return instanceSatisfiesValidRequired[0];
    }

    public static class ConnectionResult {
        public boolean dontHandleResponse;
        public int resultCode;
        public String resultMediaType;
        public Map resultHeaders;
        public final String resourceURI;
       
        private InputStream resultInputStream;
        private boolean hasContent;

        public ConnectionResult(final String resourceURI) {
            this.resourceURI = resourceURI;
        }
       
        public InputStream getResultInputStream() {
        return resultInputStream;
        }
       
        public void setResultInputStream(final InputStream resultInputStream) throws IOException {
        this.resultInputStream = resultInputStream;
        setHasContentFlag();
 
        }
       
        private void setHasContentFlag() throws IOException {
            if (resultInputStream == null) {
                hasContent = false;
            } else {
                if (!resultInputStream.markSupported())
                    this.resultInputStream = new BufferedInputStream(resultInputStream);

                resultInputStream.mark(1);
                hasContent = resultInputStream.read() != -1;
                resultInputStream.reset();
            }      
        }
       
        public void close() {}
    }
}

class ResponseAdapter implements ExternalContext.Response {

    private Object nativeResponse;

    private int status = 200;
    private String contentType;

    private StringWriter stringWriter;
    private PrintWriter printWriter;
    private LocalByteArrayOutputStream byteStream;

    private InputStream inputStream;

    public ResponseAdapter(Object nativeResponse) {
        this.nativeResponse = nativeResponse;
    }

    public int getResponseCode() {
        return status;
    }

    public String getContentType() {
        return contentType;
    }

    public Map getHeaders() {
        return null;
    }

    public InputStream getInputStream() {
        if (inputStream == null) {
            if (stringWriter != null) {
                throw new OXFException("ResponseAdapter.getInputStream() does not yet support content written with getWriter().");
            } else if (byteStream != null) {
                inputStream = new ByteArrayInputStream(byteStream.getByteArray(), 0, byteStream.size());
            }
        }

        return inputStream;
    }

    public void addHeader(String name, String value) {
    }

    public boolean checkIfModifiedSince(long lastModified, boolean allowOverride) {
        return true;
    }

    public String getCharacterEncoding() {
        return null;
    }

    public String getNamespacePrefix() {
        return null;
    }

    public OutputStream getOutputStream() throws IOException {
        if (byteStream == null)
            byteStream = new LocalByteArrayOutputStream();
        return byteStream;
    }

    public PrintWriter getWriter() throws IOException {
        if (stringWriter == null) {
            stringWriter = new StringWriter();
            printWriter = new PrintWriter(stringWriter);
        }
        return printWriter;
    }

    public boolean isCommitted() {
        return false;
    }

    public void reset() {
    }

    public String rewriteActionURL(String urlString) {
        return null;
    }

    public String rewriteRenderURL(String urlString) {
        return null;
    }

    public String rewriteResourceURL(String urlString, boolean absolute) {
        return null;
    }

    public void sendError(int sc) throws IOException {
        this.status = sc;
    }

    public void sendRedirect(String pathInfo, Map parameters, boolean isServerSide, boolean isExitPortal) throws IOException {
    }

    public void setCaching(long lastModified, boolean revalidate, boolean allowOverride) {
    }

    public void setContentLength(int len) {
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public void setHeader(String name, String value) {
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public void setTitle(String title) {
    }

    private static class LocalByteArrayOutputStream extends ByteArrayOutputStream {
        public byte[] getByteArray() {
            return buf;
        }
    }

    public Object getNativeResponse() {
        return nativeResponse;
    }
}


--
You receive this message as a subscriber of the [hidden email] mailing list.
To unsubscribe: mailto:[hidden email]
For general help: mailto:[hidden email]?subject=help
ObjectWeb mailing lists service home page: http://www.objectweb.org/wws
Reply | Threaded
Open this post in threaded view
|

Re: 'Stream closed' error when an action triggered by xforms-submit-done fails

Erik Bruchez
Administrator
Adrian,

Thanks for looking into this!

> Firstly, is this placement of this
> dispatchEvent call inside the exception handling block in
> XFormsModelSubmission correct?

I changed this behavior: at the end of a submission, we must either send
xforms-submit-error or xforms-submit-done. An exception caused by
xforms-submit-done must not cause xforms-submit-error to be sent!

> I'd suggest the ConnectionResult class encapsulate the
> connectionHasContent boolean, and only set this value when the input
> stream is set. Attached is a patch which does this (there's a couple of
> classes which need to be changed to use the new getter/setter for the
> inputstream which I haven't bothered including).

Great, I have included those changes. I have only done minimal testing,
so it would be great if you could grab the latest CVS or nightly build
and see if all the changes made it all right and work with your application.

-Erik

--
Orbeon - XForms Everywhere:
http://www.orbeon.com/blog/



--
You receive this message as a subscriber of the [hidden email] mailing list.
To unsubscribe: mailto:[hidden email]
For general help: mailto:[hidden email]?subject=help
ObjectWeb mailing lists service home page: http://www.objectweb.org/wws