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 |
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 |
Free forum by Nabble | Edit this page |