/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xforms.submission;
import org.apache.log4j.Logger;
import org.orbeon.dom.Document;
import org.orbeon.dom.Element;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.externalcontext.ExternalContext;
import org.orbeon.oxf.util.*;
import org.orbeon.oxf.xforms.XFormsContainingDocument;
import org.orbeon.oxf.xforms.XFormsError;
import org.orbeon.oxf.xforms.XFormsProperties;
import org.orbeon.oxf.xforms.XFormsUtils;
import org.orbeon.oxf.xforms.event.Dispatch;
import org.orbeon.oxf.xforms.event.XFormsEvent;
import org.orbeon.oxf.xforms.event.XFormsEventObserver;
import org.orbeon.oxf.xforms.event.XFormsEvents;
import org.orbeon.oxf.xforms.event.events.*;
import org.orbeon.oxf.xforms.model.XFormsInstance;
import org.orbeon.oxf.xforms.model.XFormsModel;
import org.orbeon.oxf.xforms.xbl.Scope;
import org.orbeon.oxf.xforms.xbl.XBLContainer;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.saxon.om.Item;
import org.orbeon.saxon.om.NodeInfo;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* Represents an XForms model submission instance.
*
* TODO: Refactor handling of serialization to separate classes.
*/
public class XFormsModelSubmission extends XFormsModelSubmissionBase {
public static final String LOGGING_CATEGORY = "submission";
public final static Logger logger = LoggerFactory.createLogger(XFormsModelSubmission.class);
public final org.orbeon.oxf.xforms.analysis.model.Submission staticSubmission;
private final XBLContainer container;
private final XFormsContainingDocument containingDocument;
private final XFormsModel model;
private String resolvedXXFormsTarget;
private boolean resolvedXXFormsShowProgress = true;
// All the submission types in the order they must be checked
private final Submission[] submissions;
public XFormsModelSubmission(XBLContainer container, org.orbeon.oxf.xforms.analysis.model.Submission staticSubmission, XFormsModel model) {
this.staticSubmission = staticSubmission;
this.container = container;
this.containingDocument = container.getContainingDocument();
this.model = model;
this.submissions = new Submission[] {
new EchoSubmission(this),
new ClientGetAllSubmission(this),
new CacheableSubmission(this),
new RequestDispatcherSubmission(this),
new RegularSubmission(this)
};
}
public XFormsContainingDocument containingDocument() {
return containingDocument;
}
public Element getSubmissionElement() {
return staticSubmission.element();
}
public boolean isShowProgress() {
return resolvedXXFormsShowProgress;
}
public boolean isURLNorewrite() {
return staticSubmission.urlNorewrite();
}
public String getUrlType() {
return staticSubmission.urlTypeOrNull();
}
// Only set for replace="all" at the end of he first pass of the submission
public String getResolvedXXFormsTarget() {
return resolvedXXFormsTarget;
}
public String getId() {
return staticSubmission.staticId();
}
public String getPrefixedId() {
return XFormsUtils.getPrefixedId(getEffectiveId());
}
public Scope scope() {
return staticSubmission.scope();
}
public String getEffectiveId() {
return XFormsUtils.getRelatedEffectiveId(model.getEffectiveId(), getId());
}
public XBLContainer container() {
return getModel().container();
}
public LocationData getLocationData() {
return staticSubmission.locationData();
}
public XFormsEventObserver parentEventObserver() {
return model;
}
public XFormsModel getModel() {
return model;
}
public void performDefaultAction(XFormsEvent event) {
final String eventName = event.name();
if (XFormsEvents.XFORMS_SUBMIT.equals(eventName) || XFormsEvents.XXFORMS_SUBMIT.equals(eventName)) {
// 11.1 The xforms-submit Event
// Bubbles: Yes / Cancelable: Yes / Context Info: None
doSubmit(event);
} else if (XFormsEvents.XXFORMS_ACTION_ERROR.equals(eventName)) {
final XXFormsActionErrorEvent ev = (XXFormsActionErrorEvent) event;
XFormsError.handleNonFatalActionError(this, ev.throwable());
}
}
private void doSubmit(XFormsEvent event) {
containingDocument.setGotSubmission();
final IndentedLogger indentedLogger = getIndentedLogger();
// Variables declared here as they are used in a catch/finally block
SubmissionParameters p = null;
String resolvedActionOrResource = null;
Runnable submitDoneOrErrorRunnable = null;
try {
try {
// Big bag of initial runtime parameters
p = SubmissionParameters.apply(event.name(), this);
if (indentedLogger.isDebugEnabled()) {
final String message = p.isDeferredSubmissionFirstPass() ? "submission first pass" : p.isDeferredSubmissionSecondPass() ? "submission second pass" : "submission";
indentedLogger.startHandleOperation("", message, "id", getEffectiveId());
}
// If a submission requiring a second pass was already set, then we ignore a subsequent submission but
// issue a warning
{
final XFormsModelSubmission existingSubmission = containingDocument.getClientActiveSubmissionFirstPass();
if (p.isDeferredSubmission() && existingSubmission != null) {
indentedLogger.logWarning("", "another submission requiring a second pass already exists",
"existing submission", existingSubmission.getEffectiveId(),
"new submission", this.getEffectiveId());
return;
}
}
/* ***** Check for pending uploads ********************************************************************** */
// We can do this first, because the check just depends on the controls, instance to submit, and pending
// submissions if any. This does not depend on the actual state of the instance.
if (p.serialize() && p.xxfUploads() && SubmissionUtils.hasBoundRelevantPendingUploadControls(containingDocument, p.refContext().refInstanceOpt())) {
throw new XFormsSubmissionException(this, "xf:submission: instance to submit has at least one pending upload.",
"checking pending uploads",
new XFormsSubmitErrorEvent(XFormsModelSubmission.this, ErrorType$.MODULE$.XXFORMS_PENDING_UPLOADS(), null));
}
/* ***** Update data model ****************************************************************************** */
final RelevanceHandling relevanceHandling = p.relevanceHandling();
// "The data model is updated"
if (p.refContext().refInstanceOpt().isDefined()) {
final XFormsModel modelForInstance = p.refContext().refInstanceOpt().get().model();
// NOTE: XForms 1.1 says that we should rebuild/recalculate the "model containing this submission".
// Here, we rebuild/recalculate instead the model containing the submission's single-node binding.
// This can be different than the model containing the submission if using e.g. xxf:instance().
// NOTE: XForms 1.1 seems to say this should happen regardless of whether we serialize or not. If
// the instance is not serialized and if no instance data is otherwise used for the submission,
// this seems however unneeded so we optimize out.
if (p.validate() || ! relevanceHandling.equals(RelevanceHandling.Keep$.MODULE$) || p.xxfCalculate()) {
// Rebuild impacts validation, relevance and calculated values (set by recalculate)
modelForInstance.doRebuild();
}
if (! relevanceHandling.equals(RelevanceHandling.Keep$.MODULE$) || p.xxfCalculate()) {
// Recalculate impacts relevance and calculated values
modelForInstance.doRecalculateRevalidate();
}
}
/* ***** Handle deferred submission ********************************************************************* */
// Deferred submission: end of the first pass
if (p.isDeferredSubmissionFirstPass()) {
// Create (but abandon) document to submit here because in case of error, an Ajax response will still be produced
if (p.serialize()) {
createDocumentToSubmit(
p.refContext().refNodeInfo(),
p.refContext().refInstanceOpt(),
p.validate(),
relevanceHandling,
p.xxfAnnotate(),
indentedLogger
);
}
// Resolve the target and show-progress AVTs because XFormsServer requires them for deferred submission
resolvedXXFormsTarget =
XFormsUtils.resolveAttributeValueTemplates(
containingDocument,
p.refContext().xpathContext(),
p.refContext().refNodeInfo(),
staticSubmission.avtXxfTargetOpt().isDefined() ?
staticSubmission.avtXxfTargetOpt().get() :
null
);
resolvedXXFormsShowProgress =
!"false".equals(
XFormsUtils.resolveAttributeValueTemplates(
containingDocument,
p.refContext().xpathContext(),
p.refContext().refNodeInfo(),
staticSubmission.avtXxfShowProgressOpt().isDefined() ?
staticSubmission.avtXxfShowProgressOpt().get() :
null
)
);
// When replace="all", we wait for the submission of an XXFormsSubmissionEvent from the client
containingDocument.setActiveSubmissionFirstPass(this);
return;
}
/* ***** Submission second pass ************************************************************************* */
// Compute parameters only needed during second pass
final SecondPassParameters p2 = SecondPassParameters.apply(this, p);
resolvedActionOrResource = p2.actionOrResource(); // in case of exception
/* ***** Serialization ********************************************************************************** */
// Get serialization requested from @method and @serialization attributes
final String requestedSerialization = getRequestedSerializationOrNull(p.serializationOpt(), p.xformsMethod(), p.httpMethod());
if (requestedSerialization == null)
throw new XFormsSubmissionException(this, "xf:submission: invalid submission method requested: " + p.xformsMethod(), "serializing instance");
final Document documentToSubmit;
if (p.serialize()) {
// Check if a submission requires file upload information
if (requestedSerialization.startsWith("multipart/") && p.refContext().refInstanceOpt().isDefined()) {
// Annotate before re-rooting/pruning
XFormsSubmissionUtils.annotateBoundRelevantUploadControls(containingDocument, p.refContext().refInstanceOpt().get());
}
// Create document to submit
documentToSubmit = createDocumentToSubmit(
p.refContext().refNodeInfo(),
p.refContext().refInstanceOpt(),
p.validate(),
relevanceHandling,
p.xxfAnnotate(),
indentedLogger
);
} else {
// Don't recreate document
documentToSubmit = null;
}
final String overriddenSerializedData;
if (!p.isDeferredSubmissionSecondPass()) {
if (p.serialize()) {
// 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."
final XFormsSubmitSerializeEvent serializeEvent = new XFormsSubmitSerializeEvent(XFormsModelSubmission.this, p.refContext().refNodeInfo(), requestedSerialization);
Dispatch.dispatchEvent(serializeEvent);
// TODO: rest of submission should happen upon default action of event
overriddenSerializedData = serializeEvent.submissionBodyAsString();
} else {
overriddenSerializedData = null;
}
} else {
// Two reasons: 1. We don't want to modify the document state 2. This can be called outside of the document
// lock, see XFormsServer.
overriddenSerializedData = null;
}
// Serialize
final SerializationParameters sp =
SerializationParameters.apply(this, p, p2, requestedSerialization, documentToSubmit, overriddenSerializedData);
/* ***** Submission connection ************************************************************************** */
// Result information
SubmissionResult submissionResult = null;
// Iterate through submissions and run the first match
for (final Submission submission : submissions) {
if (submission.isMatch(p, p2, sp)) {
if (indentedLogger.isDebugEnabled())
indentedLogger.startHandleOperation("", "connecting", "type", submission.getType());
try {
submissionResult = submission.connect(p, p2, sp);
break;
} finally {
if (indentedLogger.isDebugEnabled())
indentedLogger.endHandleOperation();
}
}
}
/* ***** Submission result processing ******************************************************************* */
// NOTE: handleSubmissionResult() catches Throwable and returns a Runnable
if (submissionResult != null)// submissionResult is null in case the submission is running asynchronously, AND when ???
submitDoneOrErrorRunnable = handleSubmissionResult(p, p2, submissionResult, true); // true because function context might have changed
} catch (final Throwable throwable) {
/* ***** Handle errors ********************************************************************************** */
final SubmissionParameters pVal = p;
final String resolvedActionOrResourceVal = resolvedActionOrResource;
submitDoneOrErrorRunnable = new Runnable() {
public void run() {
if (pVal != null && pVal.isDeferredSubmissionSecondPass() && containingDocument.isLocalSubmissionForward()) {
// It doesn't serve any purpose here to dispatch an event, so we just propagate the exception
throw new XFormsSubmissionException(XFormsModelSubmission.this, throwable, "Error while processing xf:submission", "processing submission");
} else {
// Any exception will cause an error event to be dispatched
sendSubmitError(throwable, resolvedActionOrResourceVal);
}
}
};
}
} finally {
// Log total time spent in submission
if (p != null && indentedLogger.isDebugEnabled()) {
indentedLogger.endHandleOperation();
}
}
// Execute post-submission code if any
// This typically dispatches xforms-submit-done/xforms-submit-error, or may throw another exception
if (submitDoneOrErrorRunnable != null) {
// We do this outside the above catch block so that if a problem occurs during dispatching xforms-submit-done
// or xforms-submit-error we don't dispatch xforms-submit-error (which would be illegal).
// This will also close the connection result if needed.
submitDoneOrErrorRunnable.run();
}
}
/*
* Process the response of an asynchronous submission.
*/
void doSubmitReplace(SubmissionResult submissionResult) {
assert submissionResult != null;
// Big bag of initial runtime parameters
final SubmissionParameters p = SubmissionParameters.apply(null, this);
final SecondPassParameters p2 = SecondPassParameters.apply(this, p);
final Runnable submitDoneRunnable = handleSubmissionResult(p, p2, submissionResult, false);
// Execute submit done runnable if any
if (submitDoneRunnable != null) {
// Do this outside the handleSubmissionResult catch block so that if a problem occurs during dispatching
// xforms-submit-done we don't dispatch xforms-submit-error (which would be illegal)
submitDoneRunnable.run();
}
}
private Runnable handleSubmissionResult(SubmissionParameters p, SecondPassParameters p2, final SubmissionResult submissionResult, boolean initializeXPathContext) {
assert p != null;
assert p2 != null;
assert submissionResult != null;
Runnable submitDoneOrErrorRunnable = null;
try {
final IndentedLogger indentedLogger = getIndentedLogger();
if (indentedLogger.isDebugEnabled())
indentedLogger.startHandleOperation("", "handling result");
try {
// Get fresh XPath context if requested
final SubmissionParameters updatedP =
initializeXPathContext ? SubmissionParameters.withUpdatedRefContext(p, XFormsModelSubmission.this) : p;
// Process the different types of response
if (submissionResult.getThrowable() != null) {
// Propagate throwable, which might have come from a separate thread
submitDoneOrErrorRunnable = new Runnable() {
public void run() { sendSubmitError(submissionResult.getThrowable(), submissionResult); }
};
} else {
// Replacer provided, perform replacement
assert submissionResult.getReplacer() != null;
submitDoneOrErrorRunnable = submissionResult.getReplacer().replace(submissionResult.getConnectionResult(), updatedP, p2);
}
} finally {
if (indentedLogger.isDebugEnabled())
indentedLogger.endHandleOperation();
}
} catch (final Throwable throwable) {
// Any exception will cause an error event to be dispatched
submitDoneOrErrorRunnable = new Runnable() {
public void run() { sendSubmitError(throwable, submissionResult); }
};
}
// Create wrapping runnable to make sure the submission result is closed
final Runnable finalSubmitDoneOrErrorRunnable = submitDoneOrErrorRunnable;
return new Runnable() {
public void run() {
try {
if (finalSubmitDoneOrErrorRunnable != null)
finalSubmitDoneOrErrorRunnable.run();
} finally {
// Close only after the submission result has run
submissionResult.close();
}
}
};
}
/**
* Run the given submission callable. This must be a callable for a replace="all" submission.
*
* @param callable callable run
* @param response response to write to if needed
*/
public static void runDeferredSubmission(Callable<SubmissionResult> callable, ExternalContext.Response response) {
// Run submission
try {
final SubmissionResult result = callable.call();
if (result != null) {
// Callable did not do all the work, completed it here
try {
if (result.getReplacer() != null) {
// Replacer provided, perform replacement
if (result.getReplacer() instanceof AllReplacer)
AllReplacer.forwardResultToResponse(result.getConnectionResult(), response);
else if (result.getReplacer() instanceof RedirectReplacer)
RedirectReplacer.replace(result.getConnectionResult(), response);
else
assert result.getReplacer() instanceof NoneReplacer;
} else if (result.getThrowable() != null) {
// Propagate throwable, which might have come from a separate thread
throw new OXFException(result.getThrowable());
} else {
// Should not happen
}
} finally {
result.close();
}
}
} catch (Exception e) {
// Something bad happened
throw new OXFException(e);
}
}
public Runnable sendSubmitDone(final ConnectionResult connectionResult) {
return new Runnable() {
public void run() {
// After a submission, the context might have changed
model.resetAndEvaluateVariables();
Dispatch.dispatchEvent(new XFormsSubmitDoneEvent(XFormsModelSubmission.this, connectionResult));
}
};
}
public Replacer getReplacer(ConnectionResult connectionResult, SubmissionParameters p) throws IOException {
// NOTE: This can be called from other threads so it must NOT modify the XFCD or submission
if (connectionResult != null) {
// Handle response
final Replacer replacer;
if (connectionResult.dontHandleResponse()) {
// Always return a replacer even if it does nothing, this way we don't have to deal with null
replacer = new NoneReplacer(this, containingDocument);
} else if (NetUtils.isSuccessCode(connectionResult.statusCode())) {
// Successful response
if (connectionResult.hasContent()) {
// There is a body
// Get replacer
if (ReplaceType.isReplaceAll(p.replaceType())) {
replacer = new AllReplacer(this, containingDocument);
} else if (ReplaceType.isReplaceInstance(p.replaceType())) {
replacer = new InstanceReplacer(this, containingDocument);
} else if (ReplaceType.isReplaceText(p.replaceType())) {
replacer = new TextReplacer(this, containingDocument);
} else if (ReplaceType.isReplaceNone(p.replaceType())) {
replacer = new NoneReplacer(this, containingDocument);
} else {
throw new XFormsSubmissionException(this, "xf:submission: invalid replace attribute: " + p.replaceType(), "processing instance replacement",
new XFormsSubmitErrorEvent(this, ErrorType$.MODULE$.XXFORMS_INTERNAL_ERROR(), connectionResult));
}
} else {
// There is no body, notify that processing is terminated
if (ReplaceType.isReplaceInstance(p.replaceType()) || ReplaceType.isReplaceText(p.replaceType())) {
// XForms 1.1 says it is fine not to have a body, but in most cases you will want to know that
// no instance replacement took place
final IndentedLogger indentedLogger = getIndentedLogger();
indentedLogger.logWarning("", "instance or text replacement did not take place upon successful response because no body was provided.",
"submission id", getEffectiveId());
}
// "For a success response not including a body, submission processing concludes after dispatching
// xforms-submit-done"
replacer = new NoneReplacer(this, containingDocument);
}
} else if (NetUtils.isRedirectCode(connectionResult.statusCode())) {
// Got a redirect
// Currently we don't know how to handle a redirect for replace != "all"
if (! ReplaceType.isReplaceAll(p.replaceType()))
throw new XFormsSubmissionException(this, "xf:submission for submission id: " + getId() + ", redirect code received with replace=\"" + p.replaceType() + "\"", "processing submission response",
new XFormsSubmitErrorEvent(this, ErrorType$.MODULE$.RESOURCE_ERROR(), connectionResult));
replacer = new RedirectReplacer(this, containingDocument);
} else {
// Error code received
throw new XFormsSubmissionException(this, "xf:submission for submission id: " + getId() + ", error code received when submitting instance: " + connectionResult.statusCode(), "processing submission response",
new XFormsSubmitErrorEvent(this, ErrorType$.MODULE$.RESOURCE_ERROR(), connectionResult));
}
return replacer;
} else {
return null;
}
}
public XFormsInstance findReplaceInstanceNoTargetref(scala.Option<XFormsInstance> refInstance) {
final XFormsInstance replaceInstance;
if (staticSubmission.xxfReplaceInstanceIdOrNull() != null)
replaceInstance = container.findInstanceOrNull(staticSubmission.xxfReplaceInstanceIdOrNull());
else if (staticSubmission.replaceInstanceIdOrNull() != null)
replaceInstance = model.getInstance(staticSubmission.replaceInstanceIdOrNull());
else if (refInstance.isEmpty())
replaceInstance = model.getDefaultInstance();
else
replaceInstance = refInstance.get();
return replaceInstance;
}
public NodeInfo evaluateTargetRef(XPathCache.XPathContext xpathContext,
XFormsInstance defaultReplaceInstance, Item submissionElementContextItem) {
final Object destinationObject;
if (staticSubmission.targetrefOpt().isEmpty()) {
// There is no explicit @targetref, so the target is implicitly the root element of either the instance
// pointed to by @ref, or the instance specified by @instance or @xxf:instance.
destinationObject = defaultReplaceInstance.rootElement();
} else {
// There is an explicit @targetref, which must be evaluated.
// "The in-scope evaluation context of the submission element is used to evaluate the expression." BUT ALSO "The
// evaluation context for this attribute is the in-scope evaluation context for the submission element, except
// the context node is modified to be the document element of the instance identified by the instance attribute
// if it is specified."
final boolean hasInstanceAttribute = staticSubmission.xxfReplaceInstanceIdOrNull() != null || staticSubmission.replaceInstanceIdOrNull() != null;
final Item targetRefContextItem = hasInstanceAttribute
? defaultReplaceInstance.rootElement() : submissionElementContextItem;
// Evaluate destination node
// "This attribute is evaluated only once a successful submission response has been received and if the replace
// attribute value is "instance" or "text". The first node rule is applied to the result."
destinationObject = XPathCache.evaluateSingleWithContext(xpathContext, targetRefContextItem, staticSubmission.targetrefOpt().get(), containingDocument().getRequestStats().getReporter());
}
// TODO: Also detect readonly node/ancestor situation
if (destinationObject instanceof NodeInfo && ((NodeInfo) destinationObject).getNodeKind() == org.w3c.dom.Node.ELEMENT_NODE)
return (NodeInfo) destinationObject;
else
return null;
}
public void performTargetAction(XFormsEvent event) {
// NOP
}
public IndentedLogger getIndentedLogger() {
return containingDocument.getIndentedLogger(XFormsModelSubmission.LOGGING_CATEGORY);
}
public IndentedLogger getDetailsLogger(final SubmissionParameters p, final SecondPassParameters p2) {
return getNewLogger(p, p2, getIndentedLogger(), isLogDetails());
}
public IndentedLogger getTimingLogger(final SubmissionParameters p, final SecondPassParameters p2) {
final IndentedLogger indentedLogger = getIndentedLogger();
return getNewLogger(p, p2, indentedLogger, indentedLogger.isDebugEnabled());
}
private static IndentedLogger getNewLogger(final SubmissionParameters p, final SecondPassParameters p2,
IndentedLogger indentedLogger, boolean newDebugEnabled) {
if (p2.isAsynchronous() && ! ReplaceType.isReplaceNone(p.replaceType())) {
// Background asynchronous submission creates a new logger with its own independent indentation
final IndentedLogger.Indentation newIndentation = new IndentedLogger.Indentation(indentedLogger.getIndentation().indentation);
return new IndentedLogger(indentedLogger, newIndentation, newDebugEnabled);
} else if (indentedLogger.isDebugEnabled() != newDebugEnabled) {
// Keep shared indentation but use new debug setting
return new IndentedLogger(indentedLogger, indentedLogger.getIndentation(), newDebugEnabled);
} else {
// Synchronous submission or foreground asynchronous submission uses current logger
return indentedLogger;
}
}
private static boolean isLogDetails() {
return XFormsProperties.getDebugLogging().contains("submission-details");
}
// Only allow xxforms-submit from client
private static final Set<String> ALLOWED_EXTERNAL_EVENTS = new HashSet<String>();
static {
ALLOWED_EXTERNAL_EVENTS.add(XFormsEvents.XXFORMS_SUBMIT);
}
public boolean allowExternalEvent(String eventName) {
return ALLOWED_EXTERNAL_EVENTS.contains(eventName);
}
}