/**
* 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.orbeon.oxf.http.Headers;
import org.orbeon.oxf.http.StreamedContent;
import org.orbeon.oxf.util.ConnectionResult;
import org.orbeon.oxf.util.IndentedLogger;
import org.orbeon.oxf.util.SecureUtils;
import org.orbeon.oxf.xforms.XFormsServerSharedInstancesCache;
import org.orbeon.oxf.xforms.analysis.model.Instance;
import org.orbeon.oxf.xforms.event.events.ErrorType$;
import org.orbeon.oxf.xforms.event.events.XFormsSubmitErrorEvent;
import org.orbeon.oxf.xforms.model.InstanceCaching;
import org.orbeon.oxf.xforms.model.XFormsInstance;
import org.orbeon.saxon.om.DocumentInfo;
import org.orbeon.saxon.om.NodeInfo;
import org.orbeon.saxon.om.VirtualNode;
import java.util.concurrent.Callable;
/**
* Cacheable remote submission going through a protocol handler.
*
* NOTE: This could possibly be made to work as well for optimized submissions, but currently this is not the case.
*/
public class CacheableSubmission extends BaseSubmission {
public CacheableSubmission(XFormsModelSubmission submission) {
super(submission);
}
public String getType() {
return "cacheable";
}
public boolean isMatch(SubmissionParameters p,
SecondPassParameters p2, SerializationParameters sp) {
// Match if the submission has replace="instance" and xxf:cache="true"
return ReplaceType.isReplaceInstance(p.replaceType()) && p2.isCache();
}
public SubmissionResult connect(final SubmissionParameters p,
final SecondPassParameters p2, final SerializationParameters sp) throws Exception {
// Get the instance from shared instance cache
// This can only happen is method="get" and replace="instance" and xxf:cache="true"
// Convert URL to string
final String absoluteResolvedURLString = getAbsoluteSubmissionURL(p2.actionOrResource(), sp.queryString(), submission().isURLNorewrite());
// Compute a hash of the body if needed
final String requestBodyHash;
if (sp.messageBody() != null) {
requestBodyHash = SecureUtils.digestBytes(sp.messageBody(), "hex");
} else {
requestBodyHash = null;
}
final IndentedLogger detailsLogger = getDetailsLogger(p, p2);
// Parameters to callable
final String submissionEffectiveId = submission().getEffectiveId();
// Find and check replacement location
final XFormsInstance instanceToUpdate = checkInstanceToUpdate(detailsLogger, p);
final Instance staticInstance = instanceToUpdate.instance();
final InstanceCaching instanceCaching = InstanceCaching.fromValues(p2.timeToLive(), p2.isHandleXInclude(), absoluteResolvedURLString, requestBodyHash);
final String instanceStaticId = staticInstance.staticId();
// Obtain replacer
// Pass a pseudo connection result which contains information used by getReplacer()
// We know that we will get an InstanceReplacer
final ConnectionResult connectionResult = createPseudoConnectionResult(absoluteResolvedURLString);
final InstanceReplacer replacer = (InstanceReplacer) submission().getReplacer(connectionResult, p);
// As an optimization, try from cache first
// The purpose of this is to avoid starting a new thread in asynchronous mode if the instance is already in cache
final DocumentInfo cachedDocumentInfo = XFormsServerSharedInstancesCache.findContentOrNull(
staticInstance,
instanceCaching,
p2.isReadonly(),
detailsLogger);
if (cachedDocumentInfo != null) {
// Here we cheat a bit: instead of calling generically deserialize(), we directly set the instance document
replacer.setCachedResult(cachedDocumentInfo, instanceCaching);
return new SubmissionResult(submissionEffectiveId, replacer, connectionResult);
} else {
// NOTE: technically, somebody else could put an instance in cache between now and the Callable execution
if (detailsLogger.isDebugEnabled())
detailsLogger.logDebug("", "did not find instance in cache",
"id", instanceStaticId, "URI", absoluteResolvedURLString, "request hash", requestBodyHash);
final IndentedLogger timingLogger = getTimingLogger(p, p2);
// Create callable for synchronous or asynchronous loading
final Callable<SubmissionResult> callable = new Callable<SubmissionResult>() {
public SubmissionResult call() {
if (p2.isAsynchronous() && timingLogger.isDebugEnabled())
timingLogger.startHandleOperation("", "running asynchronous submission", "id", submission().getEffectiveId(), "cacheable", "true");
final boolean[] status = { false , false};
try {
final DocumentInfo newDocumentInfo = XFormsServerSharedInstancesCache.findContentOrLoad(
staticInstance, instanceCaching, p2.isReadonly(),
new XFormsServerSharedInstancesCache.Loader() {
public DocumentInfo load(String instanceSourceURI, boolean handleXInclude) {
// Update status
status[0] = true;
// Call regular submission
SubmissionResult submissionResult = null;
try {
// Run regular submission but force:
// - synchronous execution
// - readonly result
final SecondPassParameters updatedP2 = SecondPassParameters.amendForJava(p2, false, true);
// For now support caching local portlet, request dispatcher, and regular submissions
final Submission[] submissions = new Submission[] {
new RequestDispatcherSubmission(submission()),
new RegularSubmission(submission())
};
// Iterate through submissions and run the first match
for (final Submission submission : submissions) {
if (submission.isMatch(p, p2, sp)) {
if (detailsLogger.isDebugEnabled())
detailsLogger.startHandleOperation("", "connecting", "type", submission.getType());
try {
submissionResult = submission.connect(p, updatedP2, sp);
break;
} finally {
if (detailsLogger.isDebugEnabled())
detailsLogger.endHandleOperation();
}
}
}
// Check if the connection returned a throwable
final Throwable throwable = submissionResult.getThrowable();
if (throwable != null) {
// Propagate
throw new ThrowableWrapper(throwable, submissionResult.getConnectionResult());
} else {
// There was no throwable
// We know that RegularSubmission returns a Replacer with an instance document
final Object documentOrDocumentInfo =
((InstanceReplacer) submissionResult.getReplacer()).resultingDocumentOrDocumentInfo();
// Update status
status[1] = true;
// load() requires an immutable TinyTree
// Since we forced readonly above, the result must also be a readonly instance7
assert documentOrDocumentInfo instanceof DocumentInfo;
assert ! (documentOrDocumentInfo instanceof VirtualNode);
return (DocumentInfo) documentOrDocumentInfo;
}
} catch (ThrowableWrapper throwableWrapper) {
// In case we just threw it above, just propagate
throw throwableWrapper;
} catch (Throwable throwable) {
// Exceptions are handled further down
throw new ThrowableWrapper(throwable, (submissionResult != null) ? submissionResult.getConnectionResult() : null);
}
}
},
detailsLogger);
// Here we cheat a bit: instead of calling generically deserialize(), we directly set the DocumentInfo
replacer.setCachedResult(newDocumentInfo, instanceCaching);
// Return result
return new SubmissionResult(submissionEffectiveId, replacer, connectionResult);
} catch (ThrowableWrapper throwableWrapper) {
// The ThrowableWrapper was thrown within the inner load() method above
return new SubmissionResult(submissionEffectiveId, throwableWrapper.getThrowable(), throwableWrapper.getConnectionResult());
} catch (Throwable throwable) {
// Any other throwable
return new SubmissionResult(submissionEffectiveId, throwable, null);
} finally {
if (p2.isAsynchronous() && timingLogger.isDebugEnabled())
timingLogger.endHandleOperation("id", submission().getEffectiveId(), "asynchronous", Boolean.toString(p2.isAsynchronous()),
"loading attempted", Boolean.toString(status[0]), "deserialized", Boolean.toString(status[1]));
}
}
};
// Submit the callable
// This returns null if the execution is deferred
return submitCallable(p, p2, callable);
}
}
private static class ThrowableWrapper extends RuntimeException {
final Throwable throwable;
final ConnectionResult connectionResult;
private ThrowableWrapper(Throwable throwable, ConnectionResult connectionResult) {
this.throwable = throwable;
this.connectionResult = connectionResult;
}
public Throwable getThrowable() {
return throwable;
}
public ConnectionResult getConnectionResult() {
return connectionResult;
}
}
private XFormsInstance checkInstanceToUpdate(IndentedLogger indentedLogger, SubmissionParameters p) {
XFormsInstance updatedInstance;
final NodeInfo destinationNodeInfo = submission().evaluateTargetRef(p.refContext().xpathContext(),
submission().findReplaceInstanceNoTargetref(p.refContext().refInstanceOpt()), p.refContext().submissionElementContextItem());
if (destinationNodeInfo == null) {
// Throw target-error
// XForms 1.1: "If the processing of the targetref attribute fails,
// then submission processing ends after dispatching the event
// xforms-submit-error with an error-type of target-error."
throw new XFormsSubmissionException(submission(), "targetref attribute doesn't point to an element for replace=\"instance\".", "processing targetref attribute",
new XFormsSubmitErrorEvent(submission(), ErrorType$.MODULE$.TARGET_ERROR(), null));
}
updatedInstance = submission().containingDocument().getInstanceForNode(destinationNodeInfo);
if (updatedInstance == null || !updatedInstance.rootElement().isSameNodeInfo(destinationNodeInfo)) {
// Only support replacing the root element of an instance
// TODO: in the future, check on resolvedXXFormsReadonly to implement this restriction only when using a readonly instance
throw new XFormsSubmissionException(submission(), "targetref attribute must point to an instance root element when using cached/shared instance replacement.", "processing targetref attribute",
new XFormsSubmitErrorEvent(submission(), ErrorType$.MODULE$.TARGET_ERROR(), null));
}
if (indentedLogger.isDebugEnabled())
indentedLogger.logDebug("", "using instance from application shared instance cache",
"instance", updatedInstance.getEffectiveId());
return updatedInstance;
}
// NOTE: This is really weird: the ConnectionResult returned must essentially say that it has some content.
private ConnectionResult createPseudoConnectionResult(String resourceURI) {
return ConnectionResult.apply(
resourceURI,
200,
Headers.EmptyHeaders(),
StreamedContent.fromBytes(new byte[]{0}, scala.Option.<String>apply(null), scala.Option.<String>apply(null)),
false
);
}
}