Attached is a fairly
straightforward patch to XFormsModelSubmission et al to support the
XForms 1.1 error-type attribute
(http://www.w3.org/TR/xforms11/#submit-evt-submit).
This event is particularly useful for distinguishing between submission failures caused by invalid data (user error) or a failure of the submission target (system error). Previously I had been using the xforms-submit-serialize to set a flag to identify the two cases, but came across a case where this was broken by @serialize="false" on the <xf:submission>, and rather than update my rather fiddly workaround it was quicker to implement the proper solution. Previously the XFormsSubmitError event was being created throughout the body of the performDefaultAction() method (always just before an exception is thrown), as well as in a catch block in the bottom. I felt a bit uneasy about spreading the creation around like this, and changed this so the event is created solely in the catch block at the bottom, since there is always an exception thrown if the submission fails. 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.common.ValidationException; 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.*; import org.orbeon.oxf.xforms.event.events.XFormsSubmitErrorEvent.ErrorType; import org.orbeon.oxf.xforms.processor.XFormsServer; 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.oxf.xml.dom4j.ExtendedLocationData; import org.orbeon.saxon.dom4j.NodeWrapper; import org.orbeon.saxon.functions.FunctionLibrary; import org.orbeon.saxon.om.DocumentInfo; import org.orbeon.saxon.om.NodeInfo; import org.orbeon.saxon.om.FastStringBuffer; 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.URL; 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); 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 avtActionOrResource; // required unless there is a nested xforms:resource element private String resolvedActionOrResource; private String method; // required private boolean validate = true; private boolean relevant = true; private boolean serialize = 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 xxfReplaceInstanceId; private String separator = ";"; private String includenamespaceprefixes; private String avtXXFormsUsername; private String resolvedXXFormsUsername; private String avtXXFormsPassword; private String resolvedXXFormsPassword; private String avtXXFormsReadonly; private String resolvedXXFormsReadonly; private String avtXXFormsShared; private String resolvedXXFormsShared; private String avtXXFormsTarget; private String resolvedXXFormsTarget; private boolean xxfShowProgress; private boolean fURLNorewrite; 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; } public boolean isXxfShowProgress() { return xxfShowProgress; } public String getReplace() { return replace; } public String getResolvedXXFormsTarget() { return resolvedXXFormsTarget; } private void extractSubmissionElement() { if (!submissionElementExtracted) { avtActionOrResource = submissionElement.attributeValue("resource"); if (avtActionOrResource == null) // @resource has precedence over @action avtActionOrResource = submissionElement.attributeValue("action"); if (avtActionOrResource == null) { // TODO: For XForms 1.1, support @resource and nested xforms:resource throw new XFormsSubmissionException("xforms:submission: action attribute or resource attribute is missing.", "processing xforms:submission attributes"); } method = submissionElement.attributeValue("method"); method = Dom4jUtils.qNameToexplodedQName(Dom4jUtils.extractAttributeValueQName(submissionElement, "method")); validate = !"false".equals(submissionElement.attributeValue("validate")); relevant = !"false".equals(submissionElement.attributeValue("relevant")); serialize = !"false".equals(submissionElement.attributeValue("serialize")); 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")); xxfReplaceInstanceId = XFormsUtils.namespaceId(containingDocument, submissionElement.attributeValue(XFormsConstants.XXFORMS_INSTANCE_QNAME)); } } if (submissionElement.attributeValue("separator") != null) { separator = submissionElement.attributeValue("separator"); } includenamespaceprefixes = submissionElement.attributeValue("includenamespaceprefixes"); // Extension attributes avtXXFormsUsername = submissionElement.attributeValue(XFormsConstants.XXFORMS_USERNAME_QNAME); avtXXFormsPassword = submissionElement.attributeValue(XFormsConstants.XXFORMS_PASSWORD_QNAME); avtXXFormsReadonly = submissionElement.attributeValue(XFormsConstants.XXFORMS_READONLY_ATTRIBUTE_QNAME); avtXXFormsShared = submissionElement.attributeValue(XFormsConstants.XXFORMS_SHARED_QNAME); avtXXFormsTarget = submissionElement.attributeValue(XFormsConstants.XXFORMS_TARGET_QNAME); // Whether we must show progress or not xxfShowProgress = !"false".equals(submissionElement.attributeValue(XFormsConstants.XXFORMS_SHOW_PROGRESS_QNAME)); // Whether or not to rewrite URLs fURLNorewrite = XFormsUtils.resolveUrlNorewrite(submissionElement); // 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(XFormsContainingDocument containingDocument) { return model; } public List getEventHandlers(XFormsContainingDocument containingDocument) { 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 isDeferredSubmissionSecondPassReplaceAll = false; boolean submitDone = false; final long submissionStartTime = XFormsServer.logger.isDebugEnabled() ? System.currentTimeMillis() : 0; // Make sure submission element info is extracted extractSubmissionElement(); try { 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 && avtXXFormsUsername == null; // can't optimize if there are authentication credentials final XFormsControls xformsControls = containingDocument.getXFormsControls(); // Get current node for xforms:submission final NodeInfo currentNodeInfo; { // 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)); currentNodeInfo = xformsControls.getCurrentSingleNode(); // Check that we have a current node and that it is pointing to a document or an element if (currentNodeInfo == null) throw new XFormsSubmissionException("Empty single-node binding on xforms:submission for submission id: " + id, "getting submission single-node binding", XFormsSubmitErrorEvent.ErrorType.NO_DATA); if (!(currentNodeInfo instanceof DocumentInfo || currentNodeInfo.getNodeKind() == org.w3c.dom.Document.ELEMENT_NODE)) { throw new XFormsSubmissionException("xforms:submission: single-node binding must refer to a document node or an element.", "getting submission single-node binding", XFormsSubmitErrorEvent.ErrorType.NO_DATA); } } // Get instance containing the node to submit final XFormsInstance currentInstance = xformsControls.getCurrentInstance(); // Determine if the instance to submit has one or more bound and relevant upload controls boolean hasBoundRelevantUploadControl = false; if (serialize) { final List uploadControls = xformsControls.getCurrentControlsState().getUploadControls(); if (uploadControls != null) { for (Iterator i = uploadControls.iterator(); i.hasNext();) { final XFormsUploadControl currentControl = (XFormsUploadControl) i.next(); if (currentControl.isRelevant()) { final NodeInfo boundNodeInfo = currentControl.getBoundNode(); if (currentInstance == model.getInstanceForNode(boundNodeInfo)) { // Found one relevant bound control hasBoundRelevantUploadControl = true; break; } } } } } final boolean isDeferredSubmission = (isReplaceAll && !isHandlingOptimizedGet) || (!isReplaceAll && serialize && hasBoundRelevantUploadControl); final boolean isDeferredSubmissionFirstPass = isDeferredSubmission && XFormsEvents.XFORMS_SUBMIT.equals(eventName); final boolean isDeferredSubmissionSecondPass = isDeferredSubmission && !isDeferredSubmissionFirstPass; // here we get XXFORMS_SUBMIT isDeferredSubmissionSecondPassReplaceAll = isDeferredSubmissionSecondPass && isReplaceAll; // "The data model is updated" final XFormsModel modelForInstance = currentInstance.getModel(containingDocument); { // NOTE: As of 2007-07-30, the spec says this should happen regardless of whether we serialize or not. final XFormsModel.DeferredActionContext deferredActionContext = modelForInstance.getDeferredActionContext(); if (deferredActionContext != null) { if (deferredActionContext.rebuild) { modelForInstance.doRebuild(pipelineContext); } if (deferredActionContext.recalculate) { modelForInstance.doRecalculate(pipelineContext); } } } final Document initialDocumentToSubmit; if (serialize && !isDeferredSubmissionSecondPass) { // Create document to submit try { initialDocumentToSubmit = createDocumentToSubmit(currentNodeInfo, currentInstance); // Temporarily change instance document so that we can run validation again modelForInstance.setInstanceDocument(initialDocumentToSubmit, currentInstance.getModelId(), currentInstance.getEffectiveId(), currentInstance.getSourceURI(), currentInstance.getUsername(), currentInstance.getPassword(), currentInstance.isApplicationShared(), currentInstance.getTimeToLive(), currentInstance.getValidation()); // Revalidate instance modelForInstance.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 // NOTE: If the instance is read-only, it can't have MIPs, and can't fail validation/requiredness, so we don't go through the process at all. final boolean instanceSatisfiesValidRequired = currentInstance.isReadOnly() || isDocumentSatisfiesValidRequired(initialDocumentToSubmit); if (!instanceSatisfiesValidRequired) { 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 - submission - instance document or subset thereof cannot be submitted:\n" + documentString); } throw new XFormsSubmissionException("xforms:submission: instance to submit does not satisfy valid and/or required model item properties.", "checking instance validity", XFormsSubmitErrorEvent.ErrorType.VALIDATION_ERROR); } } finally { // Restore instance document modelForInstance.setInstance(currentInstance, false); } } else { initialDocumentToSubmit = null; } // Deferred submission: end of the first pass if (isDeferredSubmissionFirstPass) { // Resolve the target AVT if needed final FunctionLibrary functionLibrary = XFormsContainingDocument.getFunctionLibrary(); resolvedXXFormsTarget = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsTarget); // When replace="all", we wait for the submission of an XXFormsSubmissionEvent from the client containingDocument.setClientActiveSubmission(this); return; } // Evaluate AVTs // TODO FIXME: the submission element is not a control, so we shouldn't use XFormsControls. { final FunctionLibrary functionLibrary = XFormsContainingDocument.getFunctionLibrary(); final String tempActionOrResource = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtActionOrResource); resolvedActionOrResource = XFormsUtils.encodeHRRI(tempActionOrResource, true); resolvedXXFormsUsername = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsUsername); resolvedXXFormsPassword = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsPassword); resolvedXXFormsReadonly = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsReadonly); resolvedXXFormsShared = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsShared); } // Check read-only and shared hints XFormsInstance.checkSharedHints(submissionElement, resolvedXXFormsReadonly, resolvedXXFormsShared); final boolean isReadonlyHint = "true".equals(resolvedXXFormsReadonly); final boolean isApplicationSharedHint = "application".equals(resolvedXXFormsShared); final long timeToLive = XFormsInstance.getTimeToLive(submissionElement); if (isApplicationSharedHint) { if (!XFormsSubmissionUtils.isGet(method)) throw new XFormsSubmissionException("xforms:submission: xxforms:shared=\"application\" can be set only with method=\"get\".", "checking read-only and shared hints"); if (!isReplaceInstance) throw new XFormsSubmissionException("xforms:submission: xxforms:shared=\"application\" can be set only with replace=\"instance\".", "checking read-only and shared hints"); } else if (isReadonlyHint) { if (!isReplaceInstance) throw new XFormsSubmissionException("xforms:submission: xxforms:readonly=\"true\" can be \"true\" only with replace=\"instance\".", "checking read-only and shared hints"); } final Document documentToSubmit; if (serialize && isDeferredSubmissionSecondPass) { // Handle uploaded files if any final Element filesElement = (event instanceof XXFormsSubmitEvent) ? ((XXFormsSubmitEvent) 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 XFormsUploadControl uploadControl = (XFormsUploadControl) containingDocument.getObjectById(pipelineContext, name); // In case of xforms:repeat, the name of the template will not match an existing control if (uploadControl == null) continue; final Element valueElement = parameterElement.element("value"); final String value = valueElement.getTextTrim(); final String filename; { final Element filenameElement = parameterElement.element("filename"); filename = (filenameElement != null) ? filenameElement.getTextTrim() : ""; } final String mediatype; { final Element mediatypeElement = parameterElement.element("content-type"); mediatype = (mediatypeElement != null) ? mediatypeElement.getTextTrim() : ""; } final String size = parameterElement.element("content-length").getTextTrim(); if (size.equals("0") && filename.equals("")) { // No file was selected in the UI } else { // A file was selected in the UI (note that the file may be empty) final String paramValueType = Dom4jUtils.qNameToexplodedQName(Dom4jUtils.extractAttributeValueQName(valueElement, XMLConstants.XSI_TYPE_QNAME)); // Set value of uploaded file into the instance (will be xs:anyURI or xs:base64Binary) uploadControl.setExternalValue(pipelineContext, value, paramValueType, !isReplaceAll); // Handle filename, mediatype and size if necessary uploadControl.setFilename(pipelineContext, filename); uploadControl.setMediatype(pipelineContext, mediatype); uploadControl.setSize(pipelineContext, size); } } } // Create document to submit try { documentToSubmit = createDocumentToSubmit(currentNodeInfo, currentInstance); // Temporarily change instance document so that we can run validation again modelForInstance.setInstanceDocument(documentToSubmit, currentInstance.getModelId(), currentInstance.getEffectiveId(), currentInstance.getSourceURI(), currentInstance.getUsername(), currentInstance.getPassword(), currentInstance.isApplicationShared(), currentInstance.getTimeToLive(), currentInstance.getValidation()); // Revalidate instance modelForInstance.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 // NOTE: If the instance is read-only, it can't have MIPs, and can't fail validation/requiredness, so we don't go through the process at all. final boolean instanceSatisfiesValidRequired = currentInstance.isReadOnly() || isDocumentSatisfiesValidRequired(documentToSubmit); if (!instanceSatisfiesValidRequired) { 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 - submission - instance document or subset thereof cannot be submitted:\n" + documentString); } throw new XFormsSubmissionException("xforms:submission: instance to submit does not satisfy valid and/or required model item properties.", "checking instance validity", XFormsSubmitErrorEvent.ErrorType.VALIDATION_ERROR); } } finally { // Restore instance document modelForInstance.setInstance(currentInstance, false); } } else { // Don't recreate document documentToSubmit = initialDocumentToSubmit; } if (serialize && !isDeferredSubmissionSecondPassReplaceAll) { // we don't want any changes to happen to the document upon xxforms-submit when producing a new document // Fire xforms-submit-serialize // "The event xforms-submit-serialize is dispatched. If the submission-body property of the event // is changed from the initial value of empty string, then the content of the submission-body // property string is used as the submission serialization. Otherwise, the submission serialization // consists of a serialization of the selected instance data according to the rules stated at 11.9 // Submission Options." // TODO: pass submission-body attribute containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitSerializeEvent(XFormsModelSubmission.this)); // TODO: look at result of submission-body attribute and see if submission body needs to be replaced } // Serialize // To support: application/xml, application/x-www-form-urlencoded, multipart/related, multipart/form-data final byte[] messageBody; final String queryString; final String defaultMediatype; { if (method.equals("multipart-post")) { // TODO throw new XFormsSubmissionException("xforms:submission: submission method not yet implemented: " + method, "serializing instance"); } else if (method.equals("form-data-post")) { // TODO // final MultipartFormDataBuilder builder = new MultipartFormDataBuilder(, , null); throw new XFormsSubmissionException("xforms:submission: submission method not yet implemented: " + method, "serializing instance"); } else if (method.equals("urlencoded-post")) { // Perform "application/x-www-form-urlencoded" serialization queryString = null; messageBody = serialize? createWwwFormUrlEncoded(documentToSubmit).getBytes("UTF-8") : null;// the resulting string is already ASCII defaultMediatype = "application/x-www-form-urlencoded"; } else if (XFormsSubmissionUtils.isPost(method) || XFormsSubmissionUtils.isPut(method)) { if (serialize) { // Serialize XML to a stream of bytes 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)); messageBody = os.toByteArray(); } catch (Exception e) { throw new XFormsSubmissionException(e, "xforms:submission: exception while serializing instance to XML.", "serializing instance"); } } else { messageBody = null; } queryString = null; defaultMediatype = "application/xml"; } else if (XFormsSubmissionUtils.isGet(method) || XFormsSubmissionUtils.isDelete(method)) { // Perform "application/x-www-form-urlencoded" serialization queryString = serialize ? createWwwFormUrlEncoded(documentToSubmit) : null; messageBody = null; defaultMediatype = null; } else { throw new XFormsSubmissionException("xforms:submission: invalid submission method requested: " + method, "serializing instance"); } } final ExternalContext externalContext = (ExternalContext) pipelineContext.getAttribute(PipelineContext.EXTERNAL_CONTEXT); // Get URL type final String urlType = submissionElement.attributeValue(XMLConstants.FORMATTING_URL_TYPE_QNAME); final ExternalContext.Request request = externalContext.getRequest(); // Find instance to update final XFormsInstance replaceInstance; if (isReplaceInstance) { if (xxfReplaceInstanceId != null) replaceInstance = containingDocument.findInstance(xxfReplaceInstanceId); else if (replaceInstanceId != null) replaceInstance = model.getInstance(replaceInstanceId); else replaceInstance = currentInstance; } else { replaceInstance = null; } // Result information ConnectionResult connectionResult = null; final long externalSubmissionStartTime = XFormsServer.logger.isDebugEnabled() ? System.currentTimeMillis() : 0; try { if (isReplaceInstance && resolvedActionOrResource.startsWith("test:")) { // Test action if (messageBody == null) throw new XFormsSubmissionException("Action 'test:': no message body.", "processing submission response"); connectionResult = new ConnectionResult(null); connectionResult.resultCode = 200; connectionResult.resultHeaders = new HashMap(); connectionResult.lastModified = 0; connectionResult.resultMediaType = "application/xml"; connectionResult.dontHandleResponse = false; connectionResult.setResultInputStream(new ByteArrayInputStream(messageBody)); } else if (isHandlingOptimizedGet) { // GET with replace="all": we can optimize and tell the client to just load the URL final String actionString = (queryString == null) ? resolvedActionOrResource : resolvedActionOrResource + ((resolvedActionOrResource.indexOf('?') == -1) ? "?" : "") + queryString; final String resultURL = XFormsLoadAction.resolveLoadValue(containingDocument, pipelineContext, submissionElement, true, actionString, null, null, fURLNorewrite, xxfShowProgress); connectionResult = new ConnectionResult(resultURL); connectionResult.dontHandleResponse = true; } else if (!NetUtils.urlHasProtocol(resolvedActionOrResource) && !fURLNorewrite && ((request.getContainerType().equals("portlet") && !"resource".equals(urlType)) || (request.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 (unless f:url-type="resource") // o Servlets cannot access resources on the same server but not in the current application // except by using absolute URLs final URI resolvedURI = XFormsUtils.resolveXMLBase(submissionElement, resolvedActionOrResource); // NOTE: We don't want any changes to happen to the document upon xxforms-submit when producing // a new document so we don't dispatch xforms-submit-done and pass a null XFormsModelSubmission // in that case connectionResult = XFormsSubmissionUtils.doOptimized(pipelineContext, externalContext, isDeferredSubmissionSecondPassReplaceAll ? null : this, method, resolvedURI.toString(), (mediatype == null) ? defaultMediatype : mediatype, isReplaceAll, messageBody, queryString); } else { // This is a regular remote submission going through a protocol handler // Absolute URLs or absolute paths are allowed to a local servlet String resolvedURL; if (NetUtils.urlHasProtocol(resolvedActionOrResource) || fURLNorewrite) { // Don't touch the URL if it is absolute or if f:url-norewrite="true" resolvedURL = resolvedActionOrResource; } else { // Rewrite URL resolvedURL= XFormsUtils.resolveResourceURL(pipelineContext, submissionElement, resolvedActionOrResource); if (request.getContainerType().equals("portlet") && "resource".equals(urlType) && !NetUtils.urlHasProtocol(resolvedURL)) { // In this case, we have to prepend the complete server path resolvedURL = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() > 0 ? ":" + request.getServerPort() : "") + resolvedURL; } } if (isApplicationSharedHint) { // Get the instance from shared instance cache // This can only happen is method="get" and replace="instance" and xxforms:readonly="true" and xxforms:shared="application" if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - using instance from application shared instance cache: " + replaceInstance.getEffectiveId()); final URL absoluteResolvedURL = XFormsSubmissionUtils.createAbsoluteURL(resolvedURL, queryString, externalContext); final String absoluteResolvedURLString = absoluteResolvedURL.toExternalForm(); final SharedXFormsInstance sharedInstance = XFormsServerSharedInstancesCache.instance().find(pipelineContext, replaceInstance.getEffectiveId(), replaceInstance.getModelId(), absoluteResolvedURLString, timeToLive, replaceInstance.getValidation()); if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - replacing instance with read-only instance: " + sharedInstance.getEffectiveId()); // Handle new instance and associated events final XFormsModel replaceModel = sharedInstance.getModel(containingDocument); replaceModel.handleNewInstanceDocuments(pipelineContext, sharedInstance); connectionResult = null; submitDone = true; } else { // Perform actual submission connectionResult = XFormsSubmissionUtils.doRegular(externalContext, method, resolvedURL, resolvedXXFormsUsername, resolvedXXFormsPassword, (mediatype == null) ? defaultMediatype : mediatype, messageBody, queryString); } } if (connectionResult != null && !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" if (!isDeferredSubmissionSecondPassReplaceAll) // we don't want any changes to happen to the document upon xxforms-submit when producing a new document containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this)); final ExternalContext.Response response = externalContext.getResponse(); // Forward headers to response connectionResult.forwardHeaders(response); // Forward content to response NetUtils.copyStream(connectionResult.getResultInputStream(), response.getOutputStream()); // TODO: [#306918] RFE: Must be able to do replace="all" during initialization. // http://forge.objectweb.org/tracker/index.php?func=detail&aid=306918&group_id=168&atid=350207 // Suggestion is to write either binary or XML to processor output ContentHandler, // and make sure the code which would output the XHTML+XForms is disabled. } else if (isReplaceInstance) { if (ProcessorUtils.isXMLContentType(connectionResult.resultMediaType)) { // Handling of XML media type try { // Set new instance document to replace the one submitted if (replaceInstance == null) { // Replacement instance was specified but not found // TODO: XForms 1.1 won't dispatch xforms-binding-exception here, but instead // will dispatch xforms-submit-error with error-type of target-error. containingDocument.dispatchEvent(pipelineContext, new XFormsBindingExceptionEvent(XFormsModelSubmission.this)); } else { // Read stream into Document final XFormsInstance newInstance; if (!isReadonlyHint) { // Resulting instance is not read-only if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - replacing instance with mutable instance: " + replaceInstance.getEffectiveId()); // TODO: Sure what default we want. In most cases, we don't use DTDs. Will XML Schema validate as well? final boolean validating = false; // We don't want XInclude automatically handled for security reasons final boolean handleXInclude = false; // TODO: Use TransformerUtils.readDom4j() instead? final Document resultingInstanceDocument = Dom4jUtils.readDom4j(connectionResult.getResultInputStream(), connectionResult.resourceURI, validating, handleXInclude); newInstance = new XFormsInstance(replaceInstance.getModelId(), replaceInstance.getEffectiveId(), resultingInstanceDocument, connectionResult.resourceURI, resolvedXXFormsUsername, resolvedXXFormsPassword, false, -1, replaceInstance.getValidation()); } else { // Resulting instance is read-only if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - replacing instance with read-only instance: " + replaceInstance.getEffectiveId()); // TODO: Handle validating and handleXInclude! // NOTE: isApplicationSharedHint is always false when get get here. isApplicationSharedHint="true" is handled above. final DocumentInfo resultingInstanceDocument = TransformerUtils.readTinyTree(connectionResult.getResultInputStream(), connectionResult.resourceURI); newInstance = new SharedXFormsInstance(replaceInstance.getModelId(), replaceInstance.getEffectiveId(), resultingInstanceDocument, connectionResult.resourceURI, resolvedXXFormsUsername, resolvedXXFormsPassword, false, -1, replaceInstance.getValidation()); } // Handle new instance and associated events final XFormsModel replaceModel = newInstance.getModel(containingDocument); replaceModel.handleNewInstanceDocuments(pipelineContext, newInstance); // Notify that submission is done submitDone = true; } } catch (Exception e) { throw new XFormsSubmissionException(e, "xforms:submission: exception while serializing XML to instance.", "processing instance replacement", XFormsSubmitErrorEvent.ErrorType.PARSE_ERROR, connectionResult); } } else { // Other media type throw new XFormsSubmissionException("Body received with non-XML media type for replace=\"instance\": " + connectionResult.resultMediaType, "processing instance replacement", XFormsSubmitErrorEvent.ErrorType.RESOURCE_ERROR, connectionResult); } } else if (replace.equals(XFormsConstants.XFORMS_SUBMIT_REPLACE_NONE)) { // Just notify that processing is terminated submitDone = true; } else { throw new XFormsSubmissionException("xforms:submission: invalid replace attribute: " + replace, "processing instance replacement", XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS, connectionResult); } } else { // There is no body, notify that processing is terminated submitDone = true; } } else if (connectionResult.resultCode == 302 || connectionResult.resultCode == 301) { // Got a redirect final ExternalContext.Response response = externalContext.getResponse(); // Forward headers to response connectionResult.forwardHeaders(response); // Forward redirect response.setStatus(connectionResult.resultCode); } else { // Error code received // submitErrorEvent = createErrorEvent(connectionResult, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS); throw new XFormsSubmissionException("xforms:submission for submission id: " + id + ", error code received when submitting instance: " + connectionResult.resultCode, "processing submission response", XFormsSubmitErrorEvent.ErrorType.RESOURCE_ERROR, connectionResult); } } } finally { // Clean-up if (connectionResult != null) { connectionResult.close(); } // Log time spent in submission if needed if (XFormsServer.logger.isDebugEnabled()) { final long submissionTime = System.currentTimeMillis() - externalSubmissionStartTime; XFormsServer.logger.debug("XForms - submission - external submission time (including handling returned body): " + submissionTime); } } } catch (Throwable e) { if (isDeferredSubmissionSecondPassReplaceAll && XFormsUtils.isOptimizePostAllSubmission()) { // It doesn't serve any purpose here to dispatch an event, so we just propagate the exception throw new XFormsSubmissionException(e, "Error while processing xforms:submission", "processing submission"); } else { // Any exception will cause an error event to be dispatched final XFormsSubmitErrorEvent submitErrorEvent; if(e instanceof XFormsSubmissionException) { final XFormsSubmissionException submissionException = (XFormsSubmissionException) e; submitErrorEvent = new XFormsSubmitErrorEvent(XFormsModelSubmission.this, resolvedActionOrResource,submissionException.getErrorType(), submissionException.getConnectionResult()); } else { submitErrorEvent = new XFormsSubmitErrorEvent(XFormsModelSubmission.this, resolvedActionOrResource, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS,null); } submitErrorEvent.setThrowable(e); containingDocument.dispatchEvent(pipelineContext, submitErrorEvent); } } finally { // If submission succeeded, dispatch success event if (submitDone && !isDeferredSubmissionSecondPassReplaceAll) { // we don't want any changes to happen to the document upon xxforms-submit when producing a new document containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this)); } // Log total time spent in submission if needed if (XFormsServer.logger.isDebugEnabled()) { final long submissionTime = System.currentTimeMillis() - submissionStartTime; XFormsServer.logger.debug("XForms - submission - total submission time: " + Long.toString(submissionTime)); } } } 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.");// TODO: what location information should we provide here? } } private Document createDocumentToSubmit(final NodeInfo currentNodeInfo, final XFormsInstance currentInstance) { final Document documentToSubmit; if (currentNodeInfo instanceof NodeWrapper) { final Node currentNode = (Node) ((NodeWrapper) currentNodeInfo).getUnderlyingNode(); // "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. " if (currentNode instanceof Element) { // Create subset of document documentToSubmit = Dom4jUtils.createDocument((Element) currentNode); } else { // Use entire instance document documentToSubmit = Dom4jUtils.createDocument(currentInstance.getDocument().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) { // Check "relevant" MIP and remove non-relevant nodes if (!InstanceData.getInheritedRelevant(node)) nodeToDetach[0] = node; } } }); if (nodeToDetach[0] != null) nodeToDetach[0].detach(); } while (nodeToDetach[0] != null); } // TODO: handle includenamespaceprefixes } else { // Submitting read-only instance backed by TinyTree (no MIPs to check) if (currentNodeInfo.getNodeKind() == org.w3c.dom.Document.ELEMENT_NODE) { documentToSubmit = TransformerUtils.tinyTreeToDom4j2(currentNodeInfo); } else { documentToSubmit = TransformerUtils.tinyTreeToDom4j2(currentNodeInfo.getRoot()); } } 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 boolean valid = checkInstanceData(element); instanceSatisfiesValidRequired[0] &= valid; if (!valid && XFormsServer.logger.isDebugEnabled()) { XFormsServer.logger.debug("XForms - submission - found invalid element: " + elementToString(element)); } } public final void visit(Attribute attribute) { final boolean valid = checkInstanceData(attribute); instanceSatisfiesValidRequired[0] &= valid; if (!valid && XFormsServer.logger.isDebugEnabled()) { XFormsServer.logger.debug("XForms - submission - found invalid attribute: " + attributeToString(attribute) + " (parent element: " + elementToString(attribute.getParent()) + ")"); } } private final boolean checkInstanceData(Node node) { // Check "valid" MIP if (!InstanceData.getValid(node)) return false; // Check "required" MIP { final boolean isRequired = InstanceData.getRequired(node); if (isRequired) { final String value = XFormsInstance.getValueForNode(node); if (value.length() == 0) { // Required and empty return false; } } } return true; } }); } return instanceSatisfiesValidRequired[0]; } private static String elementToString(Element element) { // Open start tag final FastStringBuffer sb = new FastStringBuffer("<"); sb.append(element.getQualifiedName()); // Attributes if any for (Iterator i = element.attributeIterator(); i.hasNext();) { final Attribute currentAttribute = (Attribute) i.next(); sb.append(' '); sb.append(currentAttribute.getQualifiedName()); sb.append("=\""); sb.append(currentAttribute.getValue()); sb.append('\"'); } // Close start tag sb.append('>'); if (!element.elements().isEmpty()) { // Mixed content final Object firstChild = element.content().get(0); if (firstChild instanceof Text) { sb.append(((Text) firstChild).getText()); } sb.append("[...]"); } else { // Not mixed content sb.append(element.getText()); } // Close element with end tag sb.append("</"); sb.append(element.getQualifiedName()); sb.append('>'); return sb.toString(); } private static String attributeToString(Attribute attribute) { final FastStringBuffer sb = new FastStringBuffer(attribute.getQualifiedName()); sb.append("=\""); sb.append(attribute.getValue()); sb.append('\"'); return sb.toString(); } public static class ConnectionResult { public boolean dontHandleResponse; public int resultCode; public String resultMediaType; public Map resultHeaders; public long lastModified; public String resourceURI; private InputStream resultInputStream; private boolean hasContent; public ConnectionResult(String resourceURI) { this.resourceURI = resourceURI; } public InputStream getResultInputStream() { return resultInputStream; } public boolean hasContent() { return hasContent; } 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 forwardHeaders(ExternalContext.Response response) { if (resultHeaders != null) { for (Iterator i = resultHeaders.entrySet().iterator(); i.hasNext();) { final Map.Entry currentEntry = (Map.Entry) i.next(); final String headerName = (String) currentEntry.getKey(); if (headerName != null) { // NOTE: As per the doc, this should always be a List, but for some unknown reason // it appears to be a String sometimes if (currentEntry.getValue() instanceof String) { // Case of String final String headerValue = (String) currentEntry.getValue(); forwardHeaderFilter(response, headerName, headerValue); } else { // Case of List final List headerValues = (List) currentEntry.getValue(); if (headerValues != null) { for (Iterator j = headerValues.iterator(); j.hasNext();) { final String headerValue = (String) j.next(); if (headerValue != null) { forwardHeaderFilter(response, headerName, headerValue); } } } } } } } } private void forwardHeaderFilter(ExternalContext.Response response, String headerName, String headerValue) { /** * Filtering the Transfer-Encoding header * * We don't pass the Transfer-Encoding header, as the request body is * already decoded for us. Passing along the Transfer-Encoding causes a * problem if the server sends us chunked data and we send it in the * response not chunked but saying in the header that it is chunked. * * Non-filtering of Content-Encoding header * * The Content-Encoding has the potential of causing the same problem as * the Transfer-Encoding header. It could be an issue if we get data with * Content-Encoding: gzip, but pass it along uncompressed but still * include the Content-Encoding: gzip. However this does not happen, as * the request we send does not contain a Accept-Encoding: gzip,deflate. So * filtering the Content-Encoding header is safe here. */ if (!"transfer-encoding".equals(headerName.toLowerCase())) { response.addHeader(headerName, headerValue); } } public void close() {} } private class XFormsSubmissionException extends ValidationException { private final ErrorType errorType; private final ConnectionResult connectionResult; public XFormsSubmissionException(final String message, final String description) { this(message,description, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS); } public XFormsSubmissionException(final String message, final String description, final ErrorType errorType) { this(message,description, errorType,null); } public XFormsSubmissionException(final String message, final String description, final XFormsSubmitErrorEvent.ErrorType errorType, final ConnectionResult connectionResult) { super(message, new ExtendedLocationData(XFormsModelSubmission.this.getLocationData(), description, XFormsModelSubmission.this.getSubmissionElement())); this.errorType = errorType; this.connectionResult = connectionResult; } public XFormsSubmissionException(final Throwable e, final String message, final String description) { this(e,message,description, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS,null); } public XFormsSubmissionException(final Throwable e, final String message, final String description, final XFormsSubmitErrorEvent.ErrorType errorType, final ConnectionResult connectionResult) { super(message, e, new ExtendedLocationData(XFormsModelSubmission.this.getLocationData(), description, XFormsModelSubmission.this.getSubmissionElement())); this.errorType = errorType; this.connectionResult = connectionResult; } public ErrorType getErrorType() { return errorType; } public ConnectionResult getConnectionResult() { return connectionResult; } } } /** * 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.event; import org.dom4j.Element; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.xforms.control.XFormsControl; import org.orbeon.oxf.xforms.event.events.*; import java.util.HashMap; import java.util.Map; /** * Factory for XForms events */ public class XFormsEventFactory { private static Map nameToClassMap = new HashMap(); static { nameToClassMap.put(XFormsEvents.XFORMS_DOM_ACTIVATE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsDOMActivateEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_COMPUTE_EXCEPTION, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsComputeExceptionEvent(targetObject, contextString, contextThrowable); } }); nameToClassMap.put(XFormsEvents.XFORMS_DELETE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsDeleteEvent(targetObject, contextString); } }); nameToClassMap.put(XFormsEvents.XFORMS_DESELECT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsDeselectEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_INSERT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsInsertEvent(targetObject, contextString); } }); nameToClassMap.put(XFormsEvents.XFORMS_LINK_ERROR, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsLinkErrorEvent(targetObject, contextString, contextElement, contextThrowable); } }); nameToClassMap.put(XFormsEvents.XFORMS_LINK_EXCEPTION, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsLinkExceptionEvent(targetObject, contextString, contextElement, contextThrowable); } }); nameToClassMap.put(XFormsEvents.XFORMS_BINDING_EXCEPTION, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsBindingExceptionEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_REFRESH, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsRefreshEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_SELECT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsSelectEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_SUBMIT_ERROR, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsSubmitErrorEvent(targetObject, contextString, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS,null); } }); nameToClassMap.put(XFormsEvents.XFORMS_SUBMIT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsSubmitEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_SUBMIT_SERIALIZE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsSubmitSerializeEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_SUBMIT_DONE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsSubmitDoneEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XXFORMS_READY, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XXFormsReadyEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XXFORMS_VALUE_CHANGE_WITH_FOCUS_CHANGE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XXFormsValueChangeWithFocusChangeEvent(targetObject, otherTargetObject, contextString); } }); nameToClassMap.put(XFormsEvents.XXFORMS_SUBMIT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XXFormsSubmitEvent(targetObject, filesElement); } }); nameToClassMap.put(XFormsEvents.XXFORMS_LOAD, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XXFormsLoadEvent(targetObject, contextString); } }); nameToClassMap.put(XFormsEvents.XFORMS_MODEL_CONSTRUCT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsModelConstructEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_MODEL_DESTRUCT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsModelDestructEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_RESET, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsResetEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_MODEL_CONSTRUCT_DONE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsModelConstructDoneEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_READY, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsReadyEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_REBUILD, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsRebuildEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_RECALCULATE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsRecalculateEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_REVALIDATE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsRevalidateEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_VALUE_CHANGED, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsValueChangeEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_DOM_FOCUS_OUT, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsDOMFocusOutEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_DOM_FOCUS_IN, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsDOMFocusInEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_VALID, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsValidEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_INVALID, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsInvalidEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_REQUIRED, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsRequiredEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_OPTIONAL, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsOptionalEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_READWRITE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsReadwriteEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_READONLY, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsReadonlyEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_ENABLED, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsEnabledEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_DISABLED, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsDisabledEvent((XFormsControl) targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_FOCUS, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsFocusEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_SCROLL_FIRST, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsScrollFirstEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_SCROLL_LAST, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsScrollLastEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XFORMS_LINK_EXCEPTION, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsLinkExceptionEvent(targetObject, contextString, contextElement, contextThrowable); } }); nameToClassMap.put(XFormsEvents.XFORMS_LINK_ERROR, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XFormsLinkErrorEvent(targetObject, contextString, contextElement, contextThrowable); } }); nameToClassMap.put(XFormsEvents.XXFORMS_DIALOG_CLOSE, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XXFormsDialogCloseEvent(targetObject); } }); nameToClassMap.put(XFormsEvents.XXFORMS_DIALOG_OPEN, new Factory() { public XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { return new XXFormsDialogOpenEvent(targetObject); } }); } public static XFormsEvent createEvent(String newEventName, XFormsEventTarget targetObject) { return createEvent(newEventName, targetObject, null, false, true, true, null, null, null, null); } public static XFormsEvent createEvent(String newEventName, XFormsEventTarget targetObject, boolean bubbles, boolean cancelable) { return createEvent(newEventName, targetObject, null, true, bubbles, cancelable, null, null, null, null); } public static XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement) { final Factory factory = (Factory) nameToClassMap.get(eventName); if (factory == null) { if (!allowCustomEvents) { // No custom events are allowed, just throw throw new OXFException("Invalid event name: " + eventName); } else { // Return a custom event return new XFormsCustomEvent(eventName, targetObject, bubbles, cancelable); } } else { // Return a built-in event return factory.createEvent(eventName, targetObject, otherTargetObject, allowCustomEvents, bubbles, cancelable, contextString, contextElement, contextThrowable, filesElement); } } /** * Check whether an event name maps to a built-in event. * * @param eventName event name to check * @return true if built-in, false otherwise */ public static boolean isBuiltInEvent(String eventName) { return nameToClassMap.get(eventName) != null; } private static abstract class Factory { public abstract XFormsEvent createEvent(String eventName, XFormsEventTarget targetObject, XFormsEventTarget otherTargetObject, boolean allowCustomEvents, boolean bubbles, boolean cancelable, String contextString, Element contextElement, Throwable contextThrowable, Element filesElement); } private XFormsEventFactory() {} } /** * 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.event.events; import java.io.CharArrayWriter; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.util.Collections; import org.apache.log4j.Logger; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.processor.ProcessorUtils; import org.orbeon.oxf.util.LoggerFactory; import org.orbeon.oxf.util.NetUtils; import org.orbeon.oxf.xforms.XFormsModelSubmission.ConnectionResult; import org.orbeon.oxf.xforms.event.XFormsEvent; import org.orbeon.oxf.xforms.event.XFormsEventTarget; import org.orbeon.oxf.xforms.event.XFormsEvents; import org.orbeon.oxf.xforms.processor.XFormsServer; import org.orbeon.oxf.xml.TransformerUtils; import org.orbeon.saxon.om.DocumentInfo; import org.orbeon.saxon.om.ListIterator; import org.orbeon.saxon.om.SequenceIterator; import org.orbeon.saxon.value.StringValue; /** * 4.4.19 The xforms-submit-error Event * * Target: model / Bubbles: Yes / Cancelable: No / Context Info: The submit method URI that failed (xsd:anyURI) * The default action for this event results in the following: None; notification event only. */ public class XFormsSubmitErrorEvent extends XFormsEvent { public static final String DEFAULT_TEXT_READING_ENCODING = "iso-8859-1"; private Throwable throwable; private final String urlString; private DocumentInfo bodyDocument; private String bodyString; private final ErrorType errorType; public XFormsSubmitErrorEvent(XFormsEventTarget targetObject, String urlString, final ErrorType errorType, final ConnectionResult connectionResult) { super(XFormsEvents.XFORMS_SUBMIT_ERROR, targetObject, true, false); this.urlString = urlString; this.errorType = errorType; if (connectionResult != null && 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 try { this.bodyDocument = TransformerUtils.readTinyTree(connectionResult.getResultInputStream(), connectionResult.resourceURI); } catch (Exception e) { XFormsServer.logger.error("XForms - submission - error while parsing respond body ", e); } } else if (ProcessorUtils.isTextContentType(connectionResult.resultMediaType)) { // Text content-type // Read stream into String try { 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.getResultInputStream(), charset); this.bodyString = NetUtils.readStreamAsString(reader); } catch (Exception e) { XFormsServer.logger.error("XForms - submission - error while reading respond body ", e); } } else { // This is binary // Don't store anything for now } } } public Throwable getThrowable() { return throwable; } public void setThrowable(Throwable throwable) { this.throwable = throwable; // Log exception at error level final CharArrayWriter writer = new CharArrayWriter(); OXFException.getRootThrowable(throwable).printStackTrace(new PrintWriter(writer)); XFormsServer.logger.error("XForms - submission - xforms-submit-error throwable: " + writer.toString()); } public String getUrlString() { return urlString; } public DocumentInfo getBodyDocument() { return bodyDocument; } public String getBodyString() { return bodyString; } public SequenceIterator getAttribute(String name) { if ("body".equals(name)) { // Return the body of the response if possible if (getBodyDocument() != null) return new ListIterator(Collections.singletonList(getBodyDocument())); else if (getBodyString() != null) return new ListIterator(Collections.singletonList(new StringValue(getBodyString()))); else return super.getAttribute(name); } else if ("resource-uri".equals(name)) { return new ListIterator(Collections.singletonList(new StringValue(getUrlString()))); } else if("error-type".equals(name)) { return new ListIterator(Collections.singletonList(new StringValue(String.valueOf(errorType)))); } else { return super.getAttribute(name); } } /** Enumeration of possible error-type values. */ public static class ErrorType { public static final ErrorType SUBMISSION_IN_PROGRESS = new ErrorType("submission-in-progress"); public static final ErrorType NO_DATA = new ErrorType("no-data"); public static final ErrorType VALIDATION_ERROR = new ErrorType("validation-error"); public static final ErrorType RESOURCE_ERROR = new ErrorType("resource-error"); public static final ErrorType PARSE_ERROR = new ErrorType("parse-error"); private final String errorType; private ErrorType(final String errorType) { this.errorType = errorType; } public String toString() { return errorType; } } } -- 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 OW2 mailing lists service home page: http://www.ow2.org/wws |
Oops - slight problem with passing
the connection result around: it's stream gets closed before it's read
in the XFormsSubmitErrorEvent constructor. Will fix and resend.
Adrian Baker wrote: Attached is a fairly straightforward patch to XFormsModelSubmission et al to support the XForms 1.1 error-type attribute (http://www.w3.org/TR/xforms11/#submit-evt-submit). -- 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 OW2 mailing lists service home page: http://www.ow2.org/wws |
Adrian Baker wrote:<a href="cid:part1.05060907.09090603@orionhealth.com">file:///E:/dev/Orbeon/Orion/work/src/java/org/orbeon/oxf/xforms/event/events/XFormsSubmitErrorEvent.java Oops - slight problem with passing the connection result around: it's stream gets closed before it's read in the XFormsSubmitErrorEvent constructor. Will fix and resend. /** * 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.event.events; import java.io.CharArrayWriter; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.util.Collections; import org.apache.log4j.Logger; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.processor.ProcessorUtils; import org.orbeon.oxf.util.LoggerFactory; import org.orbeon.oxf.util.NetUtils; import org.orbeon.oxf.xforms.XFormsModelSubmission.ConnectionResult; import org.orbeon.oxf.xforms.event.XFormsEvent; import org.orbeon.oxf.xforms.event.XFormsEventTarget; import org.orbeon.oxf.xforms.event.XFormsEvents; import org.orbeon.oxf.xforms.processor.XFormsServer; import org.orbeon.oxf.xml.TransformerUtils; import org.orbeon.saxon.om.DocumentInfo; import org.orbeon.saxon.om.ListIterator; import org.orbeon.saxon.om.SequenceIterator; import org.orbeon.saxon.value.StringValue; /** * 4.4.19 The xforms-submit-error Event * * Target: model / Bubbles: Yes / Cancelable: No / Context Info: The submit method URI that failed (xsd:anyURI) * The default action for this event results in the following: None; notification event only. */ public class XFormsSubmitErrorEvent extends XFormsEvent { public static final String DEFAULT_TEXT_READING_ENCODING = "iso-8859-1"; private Throwable throwable; private final String urlString; private DocumentInfo bodyDocument; private String bodyString; private final ErrorType errorType; public XFormsSubmitErrorEvent(XFormsEventTarget targetObject, String urlString, final ErrorType errorType, final ConnectionResult connectionResult) { super(XFormsEvents.XFORMS_SUBMIT_ERROR, targetObject, true, false); this.urlString = urlString; this.errorType = errorType; readConnectionResult(connectionResult); } private void readConnectionResult(final ConnectionResult connectionResult) { if (connectionResult != null && 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 try { this.bodyDocument = TransformerUtils.readTinyTree(connectionResult.getResultInputStream(), connectionResult.resourceURI); } catch (Exception e) { XFormsServer.logger.error("XForms - submission - error while parsing respond body ", e); } } else if (ProcessorUtils.isTextContentType(connectionResult.resultMediaType)) { // Text content-type // Read stream into String try { 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.getResultInputStream(), charset); this.bodyString = NetUtils.readStreamAsString(reader); } catch (Exception e) { XFormsServer.logger.error("XForms - submission - error while reading respond body ", e); } } else { // This is binary // Don't store anything for now } } } public Throwable getThrowable() { return throwable; } public void setThrowable(Throwable throwable) { this.throwable = throwable; // Log exception at error level final CharArrayWriter writer = new CharArrayWriter(); OXFException.getRootThrowable(throwable).printStackTrace(new PrintWriter(writer)); XFormsServer.logger.error("XForms - submission - xforms-submit-error throwable: " + writer.toString()); } public String getUrlString() { return urlString; } public DocumentInfo getBodyDocument() { return bodyDocument; } public String getBodyString() { return bodyString; } public SequenceIterator getAttribute(String name) { if ("body".equals(name)) { // Return the body of the response if possible if (getBodyDocument() != null) return new ListIterator(Collections.singletonList(getBodyDocument())); else if (getBodyString() != null) return new ListIterator(Collections.singletonList(new StringValue(getBodyString()))); else return super.getAttribute(name); } else if ("resource-uri".equals(name)) { return new ListIterator(Collections.singletonList(new StringValue(getUrlString()))); } else if("error-type".equals(name)) { return new ListIterator(Collections.singletonList(new StringValue(String.valueOf(errorType)))); } else { return super.getAttribute(name); } } /** Enumeration of possible error-type values. */ public static class ErrorType { public static final ErrorType SUBMISSION_IN_PROGRESS = new ErrorType("submission-in-progress"); public static final ErrorType NO_DATA = new ErrorType("no-data"); public static final ErrorType VALIDATION_ERROR = new ErrorType("validation-error"); public static final ErrorType RESOURCE_ERROR = new ErrorType("resource-error"); public static final ErrorType PARSE_ERROR = new ErrorType("parse-error"); private final String errorType; private ErrorType(final String errorType) { this.errorType = errorType; } public String toString() { return errorType; } } } /** * 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.event.events; import java.io.CharArrayWriter; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.util.Collections; import org.apache.log4j.Logger; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.processor.ProcessorUtils; import org.orbeon.oxf.util.LoggerFactory; import org.orbeon.oxf.util.NetUtils; import org.orbeon.oxf.xforms.XFormsModelSubmission.ConnectionResult; import org.orbeon.oxf.xforms.event.XFormsEvent; import org.orbeon.oxf.xforms.event.XFormsEventTarget; import org.orbeon.oxf.xforms.event.XFormsEvents; import org.orbeon.oxf.xforms.processor.XFormsServer; import org.orbeon.oxf.xml.TransformerUtils; import org.orbeon.saxon.om.DocumentInfo; import org.orbeon.saxon.om.ListIterator; import org.orbeon.saxon.om.SequenceIterator; import org.orbeon.saxon.value.StringValue; /** * 4.4.19 The xforms-submit-error Event * * Target: model / Bubbles: Yes / Cancelable: No / Context Info: The submit method URI that failed (xsd:anyURI) * The default action for this event results in the following: None; notification event only. */ public class XFormsSubmitErrorEvent extends XFormsEvent { public static final String DEFAULT_TEXT_READING_ENCODING = "iso-8859-1"; private Throwable throwable; private final String urlString; private DocumentInfo bodyDocument; private String bodyString; private final ErrorType errorType; public XFormsSubmitErrorEvent(XFormsEventTarget targetObject, String urlString, final ErrorType errorType, final ConnectionResult connectionResult) { super(XFormsEvents.XFORMS_SUBMIT_ERROR, targetObject, true, false); this.urlString = urlString; this.errorType = errorType; readConnectionResult(connectionResult); } private void readConnectionResult(final ConnectionResult connectionResult) { if (connectionResult != null && 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 try { this.bodyDocument = TransformerUtils.readTinyTree(connectionResult.getResultInputStream(), connectionResult.resourceURI); } catch (Exception e) { XFormsServer.logger.error("XForms - submission - error while parsing respond body ", e); } } else if (ProcessorUtils.isTextContentType(connectionResult.resultMediaType)) { // Text content-type // Read stream into String try { 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.getResultInputStream(), charset); this.bodyString = NetUtils.readStreamAsString(reader); } catch (Exception e) { XFormsServer.logger.error("XForms - submission - error while reading respond body ", e); } } else { // This is binary // Don't store anything for now } } } public Throwable getThrowable() { return throwable; } public void setThrowable(Throwable throwable) { this.throwable = throwable; // Log exception at error level final CharArrayWriter writer = new CharArrayWriter(); OXFException.getRootThrowable(throwable).printStackTrace(new PrintWriter(writer)); XFormsServer.logger.error("XForms - submission - xforms-submit-error throwable: " + writer.toString()); } public String getUrlString() { return urlString; } public DocumentInfo getBodyDocument() { return bodyDocument; } public String getBodyString() { return bodyString; } public SequenceIterator getAttribute(String name) { if ("body".equals(name)) { // Return the body of the response if possible if (getBodyDocument() != null) return new ListIterator(Collections.singletonList(getBodyDocument())); else if (getBodyString() != null) return new ListIterator(Collections.singletonList(new StringValue(getBodyString()))); else return super.getAttribute(name); } else if ("resource-uri".equals(name)) { return new ListIterator(Collections.singletonList(new StringValue(getUrlString()))); } else if("error-type".equals(name)) { return new ListIterator(Collections.singletonList(new StringValue(String.valueOf(errorType)))); } else { return super.getAttribute(name); } } /** Enumeration of possible error-type values. */ public static class ErrorType { public static final ErrorType SUBMISSION_IN_PROGRESS = new ErrorType("submission-in-progress"); public static final ErrorType NO_DATA = new ErrorType("no-data"); public static final ErrorType VALIDATION_ERROR = new ErrorType("validation-error"); public static final ErrorType RESOURCE_ERROR = new ErrorType("resource-error"); public static final ErrorType PARSE_ERROR = new ErrorType("parse-error"); private final String errorType; private ErrorType(final String errorType) { this.errorType = errorType; } public String toString() { return errorType; } } } /** * 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.common.ValidationException; 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.*; import org.orbeon.oxf.xforms.event.events.XFormsSubmitErrorEvent.ErrorType; import org.orbeon.oxf.xforms.processor.XFormsServer; 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.oxf.xml.dom4j.ExtendedLocationData; import org.orbeon.saxon.dom4j.NodeWrapper; import org.orbeon.saxon.functions.FunctionLibrary; import org.orbeon.saxon.om.DocumentInfo; import org.orbeon.saxon.om.NodeInfo; import org.orbeon.saxon.om.FastStringBuffer; 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.URL; 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); 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 avtActionOrResource; // required unless there is a nested xforms:resource element private String resolvedActionOrResource; private String method; // required private boolean validate = true; private boolean relevant = true; private boolean serialize = 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 xxfReplaceInstanceId; private String separator = ";"; private String includenamespaceprefixes; private String avtXXFormsUsername; private String resolvedXXFormsUsername; private String avtXXFormsPassword; private String resolvedXXFormsPassword; private String avtXXFormsReadonly; private String resolvedXXFormsReadonly; private String avtXXFormsShared; private String resolvedXXFormsShared; private String avtXXFormsTarget; private String resolvedXXFormsTarget; private boolean xxfShowProgress; private boolean fURLNorewrite; 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; } public boolean isXxfShowProgress() { return xxfShowProgress; } public String getReplace() { return replace; } public String getResolvedXXFormsTarget() { return resolvedXXFormsTarget; } private void extractSubmissionElement() { if (!submissionElementExtracted) { avtActionOrResource = submissionElement.attributeValue("resource"); if (avtActionOrResource == null) // @resource has precedence over @action avtActionOrResource = submissionElement.attributeValue("action"); if (avtActionOrResource == null) { // TODO: For XForms 1.1, support @resource and nested xforms:resource throw new XFormsSubmissionException("xforms:submission: action attribute or resource attribute is missing.", "processing xforms:submission attributes"); } method = submissionElement.attributeValue("method"); method = Dom4jUtils.qNameToexplodedQName(Dom4jUtils.extractAttributeValueQName(submissionElement, "method")); validate = !"false".equals(submissionElement.attributeValue("validate")); relevant = !"false".equals(submissionElement.attributeValue("relevant")); serialize = !"false".equals(submissionElement.attributeValue("serialize")); 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")); xxfReplaceInstanceId = XFormsUtils.namespaceId(containingDocument, submissionElement.attributeValue(XFormsConstants.XXFORMS_INSTANCE_QNAME)); } } if (submissionElement.attributeValue("separator") != null) { separator = submissionElement.attributeValue("separator"); } includenamespaceprefixes = submissionElement.attributeValue("includenamespaceprefixes"); // Extension attributes avtXXFormsUsername = submissionElement.attributeValue(XFormsConstants.XXFORMS_USERNAME_QNAME); avtXXFormsPassword = submissionElement.attributeValue(XFormsConstants.XXFORMS_PASSWORD_QNAME); avtXXFormsReadonly = submissionElement.attributeValue(XFormsConstants.XXFORMS_READONLY_ATTRIBUTE_QNAME); avtXXFormsShared = submissionElement.attributeValue(XFormsConstants.XXFORMS_SHARED_QNAME); avtXXFormsTarget = submissionElement.attributeValue(XFormsConstants.XXFORMS_TARGET_QNAME); // Whether we must show progress or not xxfShowProgress = !"false".equals(submissionElement.attributeValue(XFormsConstants.XXFORMS_SHOW_PROGRESS_QNAME)); // Whether or not to rewrite URLs fURLNorewrite = XFormsUtils.resolveUrlNorewrite(submissionElement); // 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(XFormsContainingDocument containingDocument) { return model; } public List getEventHandlers(XFormsContainingDocument containingDocument) { 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 isDeferredSubmissionSecondPassReplaceAll = false; boolean submitDone = false; final long submissionStartTime = XFormsServer.logger.isDebugEnabled() ? System.currentTimeMillis() : 0; // Result information ConnectionResult connectionResult = null; // Make sure submission element info is extracted extractSubmissionElement(); try { 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 && avtXXFormsUsername == null; // can't optimize if there are authentication credentials final XFormsControls xformsControls = containingDocument.getXFormsControls(); // Get current node for xforms:submission final NodeInfo currentNodeInfo; { // 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)); currentNodeInfo = xformsControls.getCurrentSingleNode(); // Check that we have a current node and that it is pointing to a document or an element if (currentNodeInfo == null) throw new XFormsSubmissionException("Empty single-node binding on xforms:submission for submission id: " + id, "getting submission single-node binding", XFormsSubmitErrorEvent.ErrorType.NO_DATA); if (!(currentNodeInfo instanceof DocumentInfo || currentNodeInfo.getNodeKind() == org.w3c.dom.Document.ELEMENT_NODE)) { throw new XFormsSubmissionException("xforms:submission: single-node binding must refer to a document node or an element.", "getting submission single-node binding", XFormsSubmitErrorEvent.ErrorType.NO_DATA); } } // Get instance containing the node to submit final XFormsInstance currentInstance = xformsControls.getCurrentInstance(); // Determine if the instance to submit has one or more bound and relevant upload controls boolean hasBoundRelevantUploadControl = false; if (serialize) { final List uploadControls = xformsControls.getCurrentControlsState().getUploadControls(); if (uploadControls != null) { for (Iterator i = uploadControls.iterator(); i.hasNext();) { final XFormsUploadControl currentControl = (XFormsUploadControl) i.next(); if (currentControl.isRelevant()) { final NodeInfo boundNodeInfo = currentControl.getBoundNode(); if (currentInstance == model.getInstanceForNode(boundNodeInfo)) { // Found one relevant bound control hasBoundRelevantUploadControl = true; break; } } } } } final boolean isDeferredSubmission = (isReplaceAll && !isHandlingOptimizedGet) || (!isReplaceAll && serialize && hasBoundRelevantUploadControl); final boolean isDeferredSubmissionFirstPass = isDeferredSubmission && XFormsEvents.XFORMS_SUBMIT.equals(eventName); final boolean isDeferredSubmissionSecondPass = isDeferredSubmission && !isDeferredSubmissionFirstPass; // here we get XXFORMS_SUBMIT isDeferredSubmissionSecondPassReplaceAll = isDeferredSubmissionSecondPass && isReplaceAll; // "The data model is updated" final XFormsModel modelForInstance = currentInstance.getModel(containingDocument); { // NOTE: As of 2007-07-30, the spec says this should happen regardless of whether we serialize or not. final XFormsModel.DeferredActionContext deferredActionContext = modelForInstance.getDeferredActionContext(); if (deferredActionContext != null) { if (deferredActionContext.rebuild) { modelForInstance.doRebuild(pipelineContext); } if (deferredActionContext.recalculate) { modelForInstance.doRecalculate(pipelineContext); } } } final Document initialDocumentToSubmit; if (serialize && !isDeferredSubmissionSecondPass) { // Create document to submit try { initialDocumentToSubmit = createDocumentToSubmit(currentNodeInfo, currentInstance); // Temporarily change instance document so that we can run validation again modelForInstance.setInstanceDocument(initialDocumentToSubmit, currentInstance.getModelId(), currentInstance.getEffectiveId(), currentInstance.getSourceURI(), currentInstance.getUsername(), currentInstance.getPassword(), currentInstance.isApplicationShared(), currentInstance.getTimeToLive(), currentInstance.getValidation()); // Revalidate instance modelForInstance.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 // NOTE: If the instance is read-only, it can't have MIPs, and can't fail validation/requiredness, so we don't go through the process at all. final boolean instanceSatisfiesValidRequired = currentInstance.isReadOnly() || isDocumentSatisfiesValidRequired(initialDocumentToSubmit); if (!instanceSatisfiesValidRequired) { 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 - submission - instance document or subset thereof cannot be submitted:\n" + documentString); } throw new XFormsSubmissionException("xforms:submission: instance to submit does not satisfy valid and/or required model item properties.", "checking instance validity", XFormsSubmitErrorEvent.ErrorType.VALIDATION_ERROR); } } finally { // Restore instance document modelForInstance.setInstance(currentInstance, false); } } else { initialDocumentToSubmit = null; } // Deferred submission: end of the first pass if (isDeferredSubmissionFirstPass) { // Resolve the target AVT if needed final FunctionLibrary functionLibrary = XFormsContainingDocument.getFunctionLibrary(); resolvedXXFormsTarget = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsTarget); // When replace="all", we wait for the submission of an XXFormsSubmissionEvent from the client containingDocument.setClientActiveSubmission(this); return; } // Evaluate AVTs // TODO FIXME: the submission element is not a control, so we shouldn't use XFormsControls. { final FunctionLibrary functionLibrary = XFormsContainingDocument.getFunctionLibrary(); final String tempActionOrResource = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtActionOrResource); resolvedActionOrResource = XFormsUtils.encodeHRRI(tempActionOrResource, true); resolvedXXFormsUsername = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsUsername); resolvedXXFormsPassword = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsPassword); resolvedXXFormsReadonly = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsReadonly); resolvedXXFormsShared = XFormsUtils.resolveAttributeValueTemplates(pipelineContext, currentNodeInfo, null, functionLibrary, xformsControls, submissionElement, avtXXFormsShared); } // Check read-only and shared hints XFormsInstance.checkSharedHints(submissionElement, resolvedXXFormsReadonly, resolvedXXFormsShared); final boolean isReadonlyHint = "true".equals(resolvedXXFormsReadonly); final boolean isApplicationSharedHint = "application".equals(resolvedXXFormsShared); final long timeToLive = XFormsInstance.getTimeToLive(submissionElement); if (isApplicationSharedHint) { if (!XFormsSubmissionUtils.isGet(method)) throw new XFormsSubmissionException("xforms:submission: xxforms:shared=\"application\" can be set only with method=\"get\".", "checking read-only and shared hints"); if (!isReplaceInstance) throw new XFormsSubmissionException("xforms:submission: xxforms:shared=\"application\" can be set only with replace=\"instance\".", "checking read-only and shared hints"); } else if (isReadonlyHint) { if (!isReplaceInstance) throw new XFormsSubmissionException("xforms:submission: xxforms:readonly=\"true\" can be \"true\" only with replace=\"instance\".", "checking read-only and shared hints"); } final Document documentToSubmit; if (serialize && isDeferredSubmissionSecondPass) { // Handle uploaded files if any final Element filesElement = (event instanceof XXFormsSubmitEvent) ? ((XXFormsSubmitEvent) 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 XFormsUploadControl uploadControl = (XFormsUploadControl) containingDocument.getObjectById(pipelineContext, name); // In case of xforms:repeat, the name of the template will not match an existing control if (uploadControl == null) continue; final Element valueElement = parameterElement.element("value"); final String value = valueElement.getTextTrim(); final String filename; { final Element filenameElement = parameterElement.element("filename"); filename = (filenameElement != null) ? filenameElement.getTextTrim() : ""; } final String mediatype; { final Element mediatypeElement = parameterElement.element("content-type"); mediatype = (mediatypeElement != null) ? mediatypeElement.getTextTrim() : ""; } final String size = parameterElement.element("content-length").getTextTrim(); if (size.equals("0") && filename.equals("")) { // No file was selected in the UI } else { // A file was selected in the UI (note that the file may be empty) final String paramValueType = Dom4jUtils.qNameToexplodedQName(Dom4jUtils.extractAttributeValueQName(valueElement, XMLConstants.XSI_TYPE_QNAME)); // Set value of uploaded file into the instance (will be xs:anyURI or xs:base64Binary) uploadControl.setExternalValue(pipelineContext, value, paramValueType, !isReplaceAll); // Handle filename, mediatype and size if necessary uploadControl.setFilename(pipelineContext, filename); uploadControl.setMediatype(pipelineContext, mediatype); uploadControl.setSize(pipelineContext, size); } } } // Create document to submit try { documentToSubmit = createDocumentToSubmit(currentNodeInfo, currentInstance); // Temporarily change instance document so that we can run validation again modelForInstance.setInstanceDocument(documentToSubmit, currentInstance.getModelId(), currentInstance.getEffectiveId(), currentInstance.getSourceURI(), currentInstance.getUsername(), currentInstance.getPassword(), currentInstance.isApplicationShared(), currentInstance.getTimeToLive(), currentInstance.getValidation()); // Revalidate instance modelForInstance.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 // NOTE: If the instance is read-only, it can't have MIPs, and can't fail validation/requiredness, so we don't go through the process at all. final boolean instanceSatisfiesValidRequired = currentInstance.isReadOnly() || isDocumentSatisfiesValidRequired(documentToSubmit); if (!instanceSatisfiesValidRequired) { 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 - submission - instance document or subset thereof cannot be submitted:\n" + documentString); } throw new XFormsSubmissionException("xforms:submission: instance to submit does not satisfy valid and/or required model item properties.", "checking instance validity", XFormsSubmitErrorEvent.ErrorType.VALIDATION_ERROR); } } finally { // Restore instance document modelForInstance.setInstance(currentInstance, false); } } else { // Don't recreate document documentToSubmit = initialDocumentToSubmit; } if (serialize && !isDeferredSubmissionSecondPassReplaceAll) { // we don't want any changes to happen to the document upon xxforms-submit when producing a new document // Fire xforms-submit-serialize // "The event xforms-submit-serialize is dispatched. If the submission-body property of the event // is changed from the initial value of empty string, then the content of the submission-body // property string is used as the submission serialization. Otherwise, the submission serialization // consists of a serialization of the selected instance data according to the rules stated at 11.9 // Submission Options." // TODO: pass submission-body attribute containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitSerializeEvent(XFormsModelSubmission.this)); // TODO: look at result of submission-body attribute and see if submission body needs to be replaced } // Serialize // To support: application/xml, application/x-www-form-urlencoded, multipart/related, multipart/form-data final byte[] messageBody; final String queryString; final String defaultMediatype; { if (method.equals("multipart-post")) { // TODO throw new XFormsSubmissionException("xforms:submission: submission method not yet implemented: " + method, "serializing instance"); } else if (method.equals("form-data-post")) { // TODO // final MultipartFormDataBuilder builder = new MultipartFormDataBuilder(, , null); throw new XFormsSubmissionException("xforms:submission: submission method not yet implemented: " + method, "serializing instance"); } else if (method.equals("urlencoded-post")) { // Perform "application/x-www-form-urlencoded" serialization queryString = null; messageBody = serialize? createWwwFormUrlEncoded(documentToSubmit).getBytes("UTF-8") : null;// the resulting string is already ASCII defaultMediatype = "application/x-www-form-urlencoded"; } else if (XFormsSubmissionUtils.isPost(method) || XFormsSubmissionUtils.isPut(method)) { if (serialize) { // Serialize XML to a stream of bytes 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)); messageBody = os.toByteArray(); } catch (Exception e) { throw new XFormsSubmissionException(e, "xforms:submission: exception while serializing instance to XML.", "serializing instance"); } } else { messageBody = null; } queryString = null; defaultMediatype = "application/xml"; } else if (XFormsSubmissionUtils.isGet(method) || XFormsSubmissionUtils.isDelete(method)) { // Perform "application/x-www-form-urlencoded" serialization queryString = serialize ? createWwwFormUrlEncoded(documentToSubmit) : null; messageBody = null; defaultMediatype = null; } else { throw new XFormsSubmissionException("xforms:submission: invalid submission method requested: " + method, "serializing instance"); } } final ExternalContext externalContext = (ExternalContext) pipelineContext.getAttribute(PipelineContext.EXTERNAL_CONTEXT); // Get URL type final String urlType = submissionElement.attributeValue(XMLConstants.FORMATTING_URL_TYPE_QNAME); final ExternalContext.Request request = externalContext.getRequest(); // Find instance to update final XFormsInstance replaceInstance; if (isReplaceInstance) { if (xxfReplaceInstanceId != null) replaceInstance = containingDocument.findInstance(xxfReplaceInstanceId); else if (replaceInstanceId != null) replaceInstance = model.getInstance(replaceInstanceId); else replaceInstance = currentInstance; } else { replaceInstance = null; } final long externalSubmissionStartTime = XFormsServer.logger.isDebugEnabled() ? System.currentTimeMillis() : 0; try { if (isReplaceInstance && resolvedActionOrResource.startsWith("test:")) { // Test action if (messageBody == null) throw new XFormsSubmissionException("Action 'test:': no message body.", "processing submission response"); connectionResult = new ConnectionResult(null); connectionResult.resultCode = 200; connectionResult.resultHeaders = new HashMap(); connectionResult.lastModified = 0; connectionResult.resultMediaType = "application/xml"; connectionResult.dontHandleResponse = false; connectionResult.setResultInputStream(new ByteArrayInputStream(messageBody)); } else if (isHandlingOptimizedGet) { // GET with replace="all": we can optimize and tell the client to just load the URL final String actionString = (queryString == null) ? resolvedActionOrResource : resolvedActionOrResource + ((resolvedActionOrResource.indexOf('?') == -1) ? "?" : "") + queryString; final String resultURL = XFormsLoadAction.resolveLoadValue(containingDocument, pipelineContext, submissionElement, true, actionString, null, null, fURLNorewrite, xxfShowProgress); connectionResult = new ConnectionResult(resultURL); connectionResult.dontHandleResponse = true; } else if (!NetUtils.urlHasProtocol(resolvedActionOrResource) && !fURLNorewrite && ((request.getContainerType().equals("portlet") && !"resource".equals(urlType)) || (request.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 (unless f:url-type="resource") // o Servlets cannot access resources on the same server but not in the current application // except by using absolute URLs final URI resolvedURI = XFormsUtils.resolveXMLBase(submissionElement, resolvedActionOrResource); // NOTE: We don't want any changes to happen to the document upon xxforms-submit when producing // a new document so we don't dispatch xforms-submit-done and pass a null XFormsModelSubmission // in that case connectionResult = XFormsSubmissionUtils.doOptimized(pipelineContext, externalContext, isDeferredSubmissionSecondPassReplaceAll ? null : this, method, resolvedURI.toString(), (mediatype == null) ? defaultMediatype : mediatype, isReplaceAll, messageBody, queryString); } else { // This is a regular remote submission going through a protocol handler // Absolute URLs or absolute paths are allowed to a local servlet String resolvedURL; if (NetUtils.urlHasProtocol(resolvedActionOrResource) || fURLNorewrite) { // Don't touch the URL if it is absolute or if f:url-norewrite="true" resolvedURL = resolvedActionOrResource; } else { // Rewrite URL resolvedURL= XFormsUtils.resolveResourceURL(pipelineContext, submissionElement, resolvedActionOrResource); if (request.getContainerType().equals("portlet") && "resource".equals(urlType) && !NetUtils.urlHasProtocol(resolvedURL)) { // In this case, we have to prepend the complete server path resolvedURL = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() > 0 ? ":" + request.getServerPort() : "") + resolvedURL; } } if (isApplicationSharedHint) { // Get the instance from shared instance cache // This can only happen is method="get" and replace="instance" and xxforms:readonly="true" and xxforms:shared="application" if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - using instance from application shared instance cache: " + replaceInstance.getEffectiveId()); final URL absoluteResolvedURL = XFormsSubmissionUtils.createAbsoluteURL(resolvedURL, queryString, externalContext); final String absoluteResolvedURLString = absoluteResolvedURL.toExternalForm(); final SharedXFormsInstance sharedInstance = XFormsServerSharedInstancesCache.instance().find(pipelineContext, replaceInstance.getEffectiveId(), replaceInstance.getModelId(), absoluteResolvedURLString, timeToLive, replaceInstance.getValidation()); if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - replacing instance with read-only instance: " + sharedInstance.getEffectiveId()); // Handle new instance and associated events final XFormsModel replaceModel = sharedInstance.getModel(containingDocument); replaceModel.handleNewInstanceDocuments(pipelineContext, sharedInstance); connectionResult = null; submitDone = true; } else { // Perform actual submission connectionResult = XFormsSubmissionUtils.doRegular(externalContext, method, resolvedURL, resolvedXXFormsUsername, resolvedXXFormsPassword, (mediatype == null) ? defaultMediatype : mediatype, messageBody, queryString); } } if (connectionResult != null && !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" if (!isDeferredSubmissionSecondPassReplaceAll) // we don't want any changes to happen to the document upon xxforms-submit when producing a new document containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this)); final ExternalContext.Response response = externalContext.getResponse(); // Forward headers to response connectionResult.forwardHeaders(response); // Forward content to response NetUtils.copyStream(connectionResult.getResultInputStream(), response.getOutputStream()); // TODO: [#306918] RFE: Must be able to do replace="all" during initialization. // http://forge.objectweb.org/tracker/index.php?func=detail&aid=306918&group_id=168&atid=350207 // Suggestion is to write either binary or XML to processor output ContentHandler, // and make sure the code which would output the XHTML+XForms is disabled. } else if (isReplaceInstance) { if (ProcessorUtils.isXMLContentType(connectionResult.resultMediaType)) { // Handling of XML media type try { // Set new instance document to replace the one submitted if (replaceInstance == null) { // Replacement instance was specified but not found // TODO: XForms 1.1 won't dispatch xforms-binding-exception here, but instead // will dispatch xforms-submit-error with error-type of target-error. containingDocument.dispatchEvent(pipelineContext, new XFormsBindingExceptionEvent(XFormsModelSubmission.this)); } else { // Read stream into Document final XFormsInstance newInstance; if (!isReadonlyHint) { // Resulting instance is not read-only if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - replacing instance with mutable instance: " + replaceInstance.getEffectiveId()); // TODO: Sure what default we want. In most cases, we don't use DTDs. Will XML Schema validate as well? final boolean validating = false; // We don't want XInclude automatically handled for security reasons final boolean handleXInclude = false; // TODO: Use TransformerUtils.readDom4j() instead? final Document resultingInstanceDocument = Dom4jUtils.readDom4j(connectionResult.getResultInputStream(), connectionResult.resourceURI, validating, handleXInclude); newInstance = new XFormsInstance(replaceInstance.getModelId(), replaceInstance.getEffectiveId(), resultingInstanceDocument, connectionResult.resourceURI, resolvedXXFormsUsername, resolvedXXFormsPassword, false, -1, replaceInstance.getValidation()); } else { // Resulting instance is read-only if (XFormsServer.logger.isDebugEnabled()) XFormsServer.logger.debug("XForms - submission - replacing instance with read-only instance: " + replaceInstance.getEffectiveId()); // TODO: Handle validating and handleXInclude! // NOTE: isApplicationSharedHint is always false when get get here. isApplicationSharedHint="true" is handled above. final DocumentInfo resultingInstanceDocument = TransformerUtils.readTinyTree(connectionResult.getResultInputStream(), connectionResult.resourceURI); newInstance = new SharedXFormsInstance(replaceInstance.getModelId(), replaceInstance.getEffectiveId(), resultingInstanceDocument, connectionResult.resourceURI, resolvedXXFormsUsername, resolvedXXFormsPassword, false, -1, replaceInstance.getValidation()); } // Handle new instance and associated events final XFormsModel replaceModel = newInstance.getModel(containingDocument); replaceModel.handleNewInstanceDocuments(pipelineContext, newInstance); // Notify that submission is done submitDone = true; } } catch (Exception e) { throw new XFormsSubmissionException(e, "xforms:submission: exception while serializing XML to instance.", "processing instance replacement", XFormsSubmitErrorEvent.ErrorType.PARSE_ERROR); } } else { // Other media type throw new XFormsSubmissionException("Body received with non-XML media type for replace=\"instance\": " + connectionResult.resultMediaType, "processing instance replacement", XFormsSubmitErrorEvent.ErrorType.RESOURCE_ERROR); } } else if (replace.equals(XFormsConstants.XFORMS_SUBMIT_REPLACE_NONE)) { // Just notify that processing is terminated submitDone = true; } else { throw new XFormsSubmissionException("xforms:submission: invalid replace attribute: " + replace, "processing instance replacement", XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS); } } else { // There is no body, notify that processing is terminated submitDone = true; } } else if (connectionResult.resultCode == 302 || connectionResult.resultCode == 301) { // Got a redirect final ExternalContext.Response response = externalContext.getResponse(); // Forward headers to response connectionResult.forwardHeaders(response); // Forward redirect response.setStatus(connectionResult.resultCode); } else { // Error code received // submitErrorEvent = createErrorEvent(connectionResult, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS); throw new XFormsSubmissionException("xforms:submission for submission id: " + id + ", error code received when submitting instance: " + connectionResult.resultCode, "processing submission response", XFormsSubmitErrorEvent.ErrorType.RESOURCE_ERROR); } } } finally { // Clean-up // Log time spent in submission if needed if (XFormsServer.logger.isDebugEnabled()) { final long submissionTime = System.currentTimeMillis() - externalSubmissionStartTime; XFormsServer.logger.debug("XForms - submission - external submission time (including handling returned body): " + submissionTime); } } } catch (Throwable e) { if (isDeferredSubmissionSecondPassReplaceAll && XFormsUtils.isOptimizePostAllSubmission()) { // It doesn't serve any purpose here to dispatch an event, so we just propagate the exception throw new XFormsSubmissionException(e, "Error while processing xforms:submission", "processing submission"); } else { // Any exception will cause an error event to be dispatched final XFormsSubmitErrorEvent submitErrorEvent = new XFormsSubmitErrorEvent(XFormsModelSubmission.this, resolvedActionOrResource, e instanceof XFormsSubmissionException ? ((XFormsSubmissionException) e).getErrorType() : XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS, connectionResult); submitErrorEvent.setThrowable(e); containingDocument.dispatchEvent(pipelineContext, submitErrorEvent); } } finally { if (connectionResult != null) { connectionResult.close(); } // If submission succeeded, dispatch success event if (submitDone && !isDeferredSubmissionSecondPassReplaceAll) { // we don't want any changes to happen to the document upon xxforms-submit when producing a new document containingDocument.dispatchEvent(pipelineContext, new XFormsSubmitDoneEvent(XFormsModelSubmission.this)); } // Log total time spent in submission if needed if (XFormsServer.logger.isDebugEnabled()) { final long submissionTime = System.currentTimeMillis() - submissionStartTime; XFormsServer.logger.debug("XForms - submission - total submission time: " + Long.toString(submissionTime)); } } } 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.");// TODO: what location information should we provide here? } } private Document createDocumentToSubmit(final NodeInfo currentNodeInfo, final XFormsInstance currentInstance) { final Document documentToSubmit; if (currentNodeInfo instanceof NodeWrapper) { final Node currentNode = (Node) ((NodeWrapper) currentNodeInfo).getUnderlyingNode(); // "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. " if (currentNode instanceof Element) { // Create subset of document documentToSubmit = Dom4jUtils.createDocument((Element) currentNode); } else { // Use entire instance document documentToSubmit = Dom4jUtils.createDocument(currentInstance.getDocument().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) { // Check "relevant" MIP and remove non-relevant nodes if (!InstanceData.getInheritedRelevant(node)) nodeToDetach[0] = node; } } }); if (nodeToDetach[0] != null) nodeToDetach[0].detach(); } while (nodeToDetach[0] != null); } // TODO: handle includenamespaceprefixes } else { // Submitting read-only instance backed by TinyTree (no MIPs to check) if (currentNodeInfo.getNodeKind() == org.w3c.dom.Document.ELEMENT_NODE) { documentToSubmit = TransformerUtils.tinyTreeToDom4j2(currentNodeInfo); } else { documentToSubmit = TransformerUtils.tinyTreeToDom4j2(currentNodeInfo.getRoot()); } } 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 boolean valid = checkInstanceData(element); instanceSatisfiesValidRequired[0] &= valid; if (!valid && XFormsServer.logger.isDebugEnabled()) { XFormsServer.logger.debug("XForms - submission - found invalid element: " + elementToString(element)); } } public final void visit(Attribute attribute) { final boolean valid = checkInstanceData(attribute); instanceSatisfiesValidRequired[0] &= valid; if (!valid && XFormsServer.logger.isDebugEnabled()) { XFormsServer.logger.debug("XForms - submission - found invalid attribute: " + attributeToString(attribute) + " (parent element: " + elementToString(attribute.getParent()) + ")"); } } private final boolean checkInstanceData(Node node) { // Check "valid" MIP if (!InstanceData.getValid(node)) return false; // Check "required" MIP { final boolean isRequired = InstanceData.getRequired(node); if (isRequired) { final String value = XFormsInstance.getValueForNode(node); if (value.length() == 0) { // Required and empty return false; } } } return true; } }); } return instanceSatisfiesValidRequired[0]; } private static String elementToString(Element element) { // Open start tag final FastStringBuffer sb = new FastStringBuffer("<"); sb.append(element.getQualifiedName()); // Attributes if any for (Iterator i = element.attributeIterator(); i.hasNext();) { final Attribute currentAttribute = (Attribute) i.next(); sb.append(' '); sb.append(currentAttribute.getQualifiedName()); sb.append("=\""); sb.append(currentAttribute.getValue()); sb.append('\"'); } // Close start tag sb.append('>'); if (!element.elements().isEmpty()) { // Mixed content final Object firstChild = element.content().get(0); if (firstChild instanceof Text) { sb.append(((Text) firstChild).getText()); } sb.append("[...]"); } else { // Not mixed content sb.append(element.getText()); } // Close element with end tag sb.append("</"); sb.append(element.getQualifiedName()); sb.append('>'); return sb.toString(); } private static String attributeToString(Attribute attribute) { final FastStringBuffer sb = new FastStringBuffer(attribute.getQualifiedName()); sb.append("=\""); sb.append(attribute.getValue()); sb.append('\"'); return sb.toString(); } public static class ConnectionResult { public boolean dontHandleResponse; public int resultCode; public String resultMediaType; public Map resultHeaders; public long lastModified; public String resourceURI; private InputStream resultInputStream; private boolean hasContent; public ConnectionResult(String resourceURI) { this.resourceURI = resourceURI; } public InputStream getResultInputStream() { return resultInputStream; } public boolean hasContent() { return hasContent; } 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 forwardHeaders(ExternalContext.Response response) { if (resultHeaders != null) { for (Iterator i = resultHeaders.entrySet().iterator(); i.hasNext();) { final Map.Entry currentEntry = (Map.Entry) i.next(); final String headerName = (String) currentEntry.getKey(); if (headerName != null) { // NOTE: As per the doc, this should always be a List, but for some unknown reason // it appears to be a String sometimes if (currentEntry.getValue() instanceof String) { // Case of String final String headerValue = (String) currentEntry.getValue(); forwardHeaderFilter(response, headerName, headerValue); } else { // Case of List final List headerValues = (List) currentEntry.getValue(); if (headerValues != null) { for (Iterator j = headerValues.iterator(); j.hasNext();) { final String headerValue = (String) j.next(); if (headerValue != null) { forwardHeaderFilter(response, headerName, headerValue); } } } } } } } } private void forwardHeaderFilter(ExternalContext.Response response, String headerName, String headerValue) { /** * Filtering the Transfer-Encoding header * * We don't pass the Transfer-Encoding header, as the request body is * already decoded for us. Passing along the Transfer-Encoding causes a * problem if the server sends us chunked data and we send it in the * response not chunked but saying in the header that it is chunked. * * Non-filtering of Content-Encoding header * * The Content-Encoding has the potential of causing the same problem as * the Transfer-Encoding header. It could be an issue if we get data with * Content-Encoding: gzip, but pass it along uncompressed but still * include the Content-Encoding: gzip. However this does not happen, as * the request we send does not contain a Accept-Encoding: gzip,deflate. So * filtering the Content-Encoding header is safe here. */ if (!"transfer-encoding".equals(headerName.toLowerCase())) { response.addHeader(headerName, headerValue); } } public void close() {} } private class XFormsSubmissionException extends ValidationException { private final ErrorType errorType; public XFormsSubmissionException(final String message, final String description) { this(message,description, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS); } public XFormsSubmissionException(final String message, final String description, final XFormsSubmitErrorEvent.ErrorType errorType) { super(message, new ExtendedLocationData(XFormsModelSubmission.this.getLocationData(), description, XFormsModelSubmission.this.getSubmissionElement())); this.errorType = errorType; } public XFormsSubmissionException(final Throwable e, final String message, final String description) { this(e,message,description, XFormsSubmitErrorEvent.ErrorType.SUBMISSION_IN_PROGRESS); } public XFormsSubmissionException(final Throwable e, final String message, final String description, final XFormsSubmitErrorEvent.ErrorType errorType) { super(message, e, new ExtendedLocationData(XFormsModelSubmission.this.getLocationData(), description, XFormsModelSubmission.this.getSubmissionElement())); this.errorType = errorType; } public ErrorType getErrorType() { return errorType; } } } -- 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 OW2 mailing lists service home page: http://www.ow2.org/wws |
Administrator
|
Adrian,
Fantastic, we will try to integrate this ASAP. -Erik Adrian Baker wrote: > Fixed version attached. > > Adrian Baker > wrote:file:///E:/dev/Orbeon/Orion/work/src/java/org/orbeon/oxf/xforms/event/events/XFormsSubmitErrorEvent.java > <imap://[hidden email]:143/fetch%3EUID%3E.INBOX.Orbeon.ops-users%3E15230?header=quotebody&part=1.1.2&filename=XFormsSubmitErrorEvent.java> > >> Oops - slight problem with passing the connection result around: it's >> stream gets closed before it's read in the XFormsSubmitErrorEvent >> constructor. Will fix and resend. >> >> Adrian Baker wrote: >>> Attached is a fairly straightforward patch to XFormsModelSubmission >>> et al to support the XForms 1.1 error-type attribute >>> (http://www.w3.org/TR/xforms11/#submit-evt-submit). >>> >>> This event is particularly useful for distinguishing between >>> submission failures caused by invalid data (user error) or a failure >>> of the submission target (system error). Previously I had been using >>> the xforms-submit-serialize to set a flag to identify the two cases, >>> but came across a case where this was broken by @serialize="false" on >>> the <xf:submission>, and rather than update my rather fiddly >>> workaround it was quicker to implement the proper solution. >>> >>> Previously the XFormsSubmitError event was being created throughout >>> the body of the performDefaultAction() method (always just before an >>> exception is thrown), as well as in a catch block in the bottom. I >>> felt a bit uneasy about spreading the creation around like this, and >>> changed this so the event is created solely in the catch block at the >>> bottom, since there is always an exception thrown if the submission >>> fails. >>> >>> Adrian >>> >>> ______________________________________________________________________ >>> This email has been scanned by the MessageLabs Email Security System. >>> For more information please visit http://www.messagelabs.com/email >>> ______________________________________________________________________ >> >> ______________________________________________________________________ >> This email has been scanned by the MessageLabs Email Security System. >> For more information please visit http://www.messagelabs.com/email >> ______________________________________________________________________ > -- Orbeon Forms - Web Forms for the Enterprise Done the Right Way http://www.orbeon.com/ -- 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 OW2 mailing lists service home page: http://www.ow2.org/wws |
What's the status of this? Is there a tracker item?
|
Free forum by Nabble | Edit this page |