/** * 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; import org.apache.commons.lang3.StringUtils; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.common.OrbeonLocationException; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.common.Version; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.logging.LifecycleLogger; import org.orbeon.oxf.util.SecureUtils; import org.orbeon.oxf.xforms.action.XFormsAPI; import org.orbeon.oxf.xforms.analysis.XPathDependencies; import org.orbeon.oxf.xforms.control.Controls; import org.orbeon.oxf.xforms.control.XFormsControl; import org.orbeon.oxf.xforms.control.XFormsSingleNodeControl; import org.orbeon.oxf.xforms.control.controls.XFormsUploadControl; import org.orbeon.oxf.xforms.processor.XFormsURIResolver; import org.orbeon.oxf.xforms.state.*; import org.orbeon.oxf.xforms.submission.AsynchronousSubmissionManager; import org.orbeon.oxf.xforms.submission.SubmissionResult; import org.orbeon.oxf.xforms.submission.XFormsModelSubmission; import org.orbeon.oxf.xforms.xbl.Scope; import org.orbeon.oxf.xml.SAXStore; import org.orbeon.oxf.xml.dom4j.ExtendedLocationData; import org.orbeon.saxon.functions.FunctionLibrary; import scala.collection.Seq; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.locks.Lock; /** * Represents an XForms containing document. * * The containing document: * * - Is the container for root XForms models (including multiple instances) * - Contains XForms controls * - Handles event handlers hierarchy */ public class XFormsContainingDocument extends XFormsContainingDocumentSupport { private String uuid; // UUID of this document private long sequence = 1; // sequence number of changes to this document private SAXStore lastAjaxResponse; // last Ajax response for retry feature // Global XForms function library private FunctionLibrary functionLibrary = null; // Whether this document is currently being initialized private boolean initializing; // Transient URI resolver for initialization private XFormsURIResolver uriResolver; // Transient OutputStream for xf:submission[@replace = 'all'], or null if not available private ExternalContext.Response response; // Asynchronous submission manager private AsynchronousSubmissionManager asynchronousSubmissionManager; // A document refers to the static state and controls private final XFormsStaticState staticState; private final StaticStateGlobalOps staticOps; private XFormsControls xformsControls; // Other state private Set<String> pendingUploads; // Client state private XFormsModelSubmission activeSubmissionFirstPass; private Callable<SubmissionResult> replaceAllCallable; private boolean gotSubmissionReplaceAll; private boolean gotSubmissionRedirect; private List<Message> messagesToRun; private List<Load> loadsToRun; private List<ScriptInvocation> scriptsToRun; private String helpEffectiveControlId; private List<ServerError> serverErrors; private Set<String> controlsStructuralChanges; private final XPathDependencies xpathDependencies; /** * Return the global function library. */ public FunctionLibrary getFunctionLibrary() { return functionLibrary; } /** * Create an XFormsContainingDocument from an XFormsStaticState object. * * Used by XFormsToXHTML. * * @param staticState static state object * @param uriResolver URIResolver for loading instances during initialization (and possibly more, such as schemas and "GET" submissions upon initialization) * @param response optional response for handling replace="all" during initialization * @param initialize initialize document (false for testing only) */ public XFormsContainingDocument(XFormsStaticState staticState, XFormsURIResolver uriResolver, ExternalContext.Response response, boolean initialize) { super(false); // Create UUID for this document instance this.uuid = SecureUtils.randomHexId(); // Initialize request information initializeRequestInformation(); initializePathMatchers(); // Initialize function library this.functionLibrary = staticState.functionLibrary(); indentedLogger().startHandleOperation("initialization", "creating new ContainingDocument (static state object provided).", "uuid", this.uuid); { // Remember static state this.staticState = staticState; this.staticOps = new StaticStateGlobalOps(staticState.topLevelPart()); if (! isNoUpdatesStatic()) // attempt to ignore oxf:xforms-submission LifecycleLogger.eventAssumingRequestJava("xforms", "new form session", new String[] { "uuid", uuid }); // NOTE: template is not stored right away, as it depends on the evaluation of the noscript property. this.xpathDependencies = Version.instance().createUIDependencies(this); // Remember parameters used during initialization this.uriResolver = uriResolver; this.response = response; this.initializing = true; // Initialize the containing document if (initialize) { try { initialize(); } catch (Exception e) { throw OrbeonLocationException.wrapException(e, new ExtendedLocationData(null, "initializing XForms containing document")); } } } indentedLogger().endHandleOperation(); } // This is called upon the first creation of the XForms engine private void initialize() { // Scope the containing document for the XForms API XFormsAPI.withContainingDocumentJava(this, new Runnable() { public void run() { // Create XForms controls and models createControlsAndModels(); // Group all xforms-model-construct-done and xforms-ready events within a single outermost action handler in // order to optimize events // Perform deferred updates only for xforms-ready startOutermostActionHandler(); { // Initialize models initializeModels(); // After initialization, some async submissions might be running processCompletedAsynchronousSubmissions(true, true); } // End deferred behavior endOutermostActionHandler(); processDueDelayedEvents(); } }); } /** * Restore an XFormsContainingDocument from XFormsState only. * * Used by XFormsStateManager. * * @param xformsState XFormsState containing static and dynamic state * @param disableUpdates whether to disable updates (for recreating initial document upon browser back) */ public XFormsContainingDocument(XFormsState xformsState, boolean disableUpdates, boolean forceEncryption) { super(disableUpdates); // 1. Restore the static state { final scala.Option<String> staticStateDigest = xformsState.staticStateDigest(); if (staticStateDigest.isDefined()) { final XFormsStaticState cachedState = XFormsStaticStateCache.getDocumentJava(staticStateDigest.get()); if (cachedState != null) { // Found static state in cache indentedLogger().logDebug("", "found static state by digest in cache"); this.staticState = cachedState; } else { // Not found static state in cache, create static state from input indentedLogger().logDebug("", "did not find static state by digest in cache"); indentedLogger().startHandleOperation("initialization", "restoring static state"); this.staticState = XFormsStaticStateImpl.restore(staticStateDigest, xformsState.staticState().get(), forceEncryption); indentedLogger().endHandleOperation(); // Store in cache XFormsStaticStateCache.storeDocument(this.staticState); } assert this.staticState.isServerStateHandling(); } else { // Not digest provided, create static state from input indentedLogger().logDebug("", "did not find static state by digest in cache"); this.staticState = XFormsStaticStateImpl.restore(staticStateDigest, xformsState.staticState().get(), forceEncryption); assert this.staticState.isClientStateHandling(); } this.staticOps = new StaticStateGlobalOps(staticState.topLevelPart()); this.xpathDependencies = Version.instance().createUIDependencies(this); this.functionLibrary = staticState.functionLibrary(); } // 2. Restore the dynamic state indentedLogger().startHandleOperation("initialization", "restoring containing document"); try { restoreDynamicState(xformsState.dynamicState().get()); } catch (Exception e) { throw OrbeonLocationException.wrapException(e, new ExtendedLocationData(null, "re-initializing XForms containing document")); } indentedLogger().endHandleOperation(); } private void restoreDynamicState(final DynamicState dynamicState) { this.uuid = dynamicState.uuid(); this.sequence = dynamicState.sequence(); indentedLogger().logDebug("initialization", "restoring dynamic state for UUID", "UUID", this.uuid, "sequence", Long.toString(this.sequence)); // Restore request information restoreRequestInformation(dynamicState); restorePathMatchers(dynamicState); restoreTemplate(dynamicState); // Restore other encoded objects this.pendingUploads = new HashSet<String>(dynamicState.decodePendingUploadsJava()); // make copy as must be mutable this.lastAjaxResponse = dynamicState.decodeLastAjaxResponseJava(); // Scope the containing document for the XForms API XFormsAPI.withContainingDocumentJava(this, new Runnable() { public void run() { Controls.withDynamicStateToRestoreJava(dynamicState.decodeInstancesControls(), new Runnable() { public void run() { // Restore models state // Create XForms controls and models createControlsAndModels(); // Restore top-level models state, including instances restoreModelsState(false); // Restore controls state // Store serialized control state for retrieval later xformsControls.createControlTree(Controls.restoringControls()); // Once the control tree is rebuilt, restore focus if needed if (dynamicState.decodeFocusedControlJava() != null) xformsControls.setFocusedControl(xformsControls.getCurrentControlTree().findControlOrNullJava(dynamicState.decodeFocusedControlJava())); } }); } }); } @Override public PartAnalysis partAnalysis() { return staticState.topLevelPart(); } public XFormsURIResolver getURIResolver() { return uriResolver; } public String getUUID() { return uuid; } public void updateChangeSequence() { sequence++; } public SAXStore getLastAjaxResponse() { return lastAjaxResponse; } public boolean isInitializing() { return initializing; } /** * Whether the document is currently in a mode where it must remember differences. This is the case when: * * - the document is currently handling an update (as opposed to initialization) * - the property "no-updates" is false (the default) * - the document is * * @return true iif the document must handle differences */ public boolean isHandleDifferences() { return ! initializing && supportUpdates(); } /** * Return the controls. */ public XFormsControls getControls() { return xformsControls; } public XFormsControl getControlByEffectiveId(String effectiveId) { return xformsControls.getObjectByEffectiveId(effectiveId); } /** * Return dependencies implementation. */ public final XPathDependencies getXPathDependencies() { return xpathDependencies; } /** * Whether the document is dirty since the last request. * * @return whether the document is dirty since the last request */ public boolean isDirtySinceLastRequest() { return xformsControls.isDirtySinceLastRequest(); } /** * Return the static state of this document. */ public XFormsStaticState getStaticState() { return staticState; } public StaticStateGlobalOps getStaticOps() { return staticOps; } /** * Get object with the effective id specified. * * @param effectiveId effective id of the target * @return object, or null if not found */ public XFormsObject getObjectByEffectiveId(String effectiveId) { // Search in controls first because that's the fast way { final XFormsObject resultObject = getControlByEffectiveId(effectiveId); if (resultObject != null) return resultObject; } // Search in parent (models and this) { final XFormsObject resultObject = super.getObjectByEffectiveId(effectiveId); if (resultObject != null) return resultObject; } // Check container id // TODO: This should no longer be needed since we have a root control, right? In which case, the document would // no longer need to be an XFormsObject. if (effectiveId.equals(getEffectiveId())) return this; return null; } /** * Return the active submission if any or null. */ public XFormsModelSubmission getClientActiveSubmissionFirstPass() { return activeSubmissionFirstPass; } public Callable<SubmissionResult> getReplaceAllCallable() { return replaceAllCallable; } /** * Clear current client state. */ private void clearClientState() { assert !initializing; assert response == null; assert uriResolver == null; this.activeSubmissionFirstPass = null; this.replaceAllCallable = null; this.gotSubmissionReplaceAll = false; this.gotSubmissionRedirect = false; this.messagesToRun = null; this.loadsToRun = null; this.scriptsToRun = null; this.helpEffectiveControlId = null; this.clearAllDelayedEvents(); this.serverErrors = null; clearRequestStats(); if (this.controlsStructuralChanges != null) this.controlsStructuralChanges.clear(); } /** * Add a two-pass submission. * * This can be called with a non-null value at most once. */ public void setActiveSubmissionFirstPass(XFormsModelSubmission submission) { if (this.activeSubmissionFirstPass != null) throw new ValidationException("There is already an active submission.", submission.getLocationData()); if (loadsToRun != null) throw new ValidationException("Unable to run a two-pass submission and xf:load within a same action sequence.", submission.getLocationData()); // NOTE: It seems reasonable to run scripts, messages, focus, and help up to the point where the submission takes place. // Remember submission this.activeSubmissionFirstPass = submission; } public void setReplaceAllCallable(Callable<SubmissionResult> callable) { this.replaceAllCallable = callable; } public void setGotSubmission() {} public void setGotSubmissionReplaceAll() { if (this.gotSubmissionReplaceAll) throw new OXFException("Unable to run a second submission with replace=\"all\" within a same action sequence."); this.gotSubmissionReplaceAll = true; } public boolean isGotSubmissionReplaceAll() { return gotSubmissionReplaceAll; } public void setGotSubmissionRedirect() { if (this.gotSubmissionRedirect) throw new OXFException("Unable to run a second submission with replace=\"all\" redirection within a same action sequence."); this.gotSubmissionRedirect = true; } public boolean isGotSubmissionRedirect() { return gotSubmissionRedirect; } /** * Add an XForms message to send to the client. */ public void addMessageToRun(String message, String level) { if (messagesToRun == null) messagesToRun = new ArrayList<Message>(); messagesToRun.add(new Message(message, level)); } /** * Return the list of messages to send to the client, null if none. */ public List<Message> getMessagesToRun() { if (messagesToRun != null) return messagesToRun; else return Collections.emptyList(); } public static class Message { private String message; private String level; public Message(String message, String level) { this.message = message; this.level = level; } public String getMessage() { return message; } public String getLevel() { return level; } } public void addScriptToRun(ScriptInvocation scriptInvocation) { if (activeSubmissionFirstPass != null && StringUtils.isBlank(activeSubmissionFirstPass.getResolvedXXFormsTarget())) { // Scripts occurring after a submission without a target takes place should not run // TODO: Should we allow scripts anyway? Don't we allow value changes updates on the client anyway? indentedLogger().logWarning( "", "script will be ignored because two-pass submission started", "script id", scriptInvocation.script().prefixedId() ); } else { // Warn that scripts won't run in noscript mode (duh) if (noscript()) indentedLogger().logInfo( "noscript", "script won't run in noscript mode", "script id", scriptInvocation.script().prefixedId() ); if (scriptsToRun == null) scriptsToRun = new ArrayList<ScriptInvocation>(); scriptsToRun.add(scriptInvocation); } } public List<ScriptInvocation> getScriptsToRun() { if (scriptsToRun != null) return scriptsToRun; else return Collections.emptyList(); } /** * Add an XForms load to send to the client. */ public void addLoadToRun(String resource, String targetOrNull, String urlType, boolean isReplace, boolean isShowProgress) { if (activeSubmissionFirstPass != null) throw new ValidationException("Unable to run a two-pass submission and xf:load within a same action sequence.", activeSubmissionFirstPass.getLocationData()); if (loadsToRun == null) loadsToRun = new ArrayList<Load>(); loadsToRun.add(new Load(resource, scala.Option.apply(targetOrNull), urlType, isReplace, isShowProgress)); } /** * Return the list of loads to send to the client, null if none. */ public List<Load> getLoadsToRun() { if (loadsToRun != null) return loadsToRun; else return Collections.emptyList(); } /** * Tell the client that help must be shown for the given effective control id. * * This can be called several times, but only the last control id is remembered. * * @param effectiveControlId */ public void setClientHelpEffectiveControlId(String effectiveControlId) { this.helpEffectiveControlId = effectiveControlId; } /** * Return the effective control id of the control to help for, or null. */ public String getClientHelpControlEffectiveId() { if (helpEffectiveControlId == null) return null; final XFormsControl xformsControl = getControlByEffectiveId(helpEffectiveControlId); // It doesn't make sense to tell the client to show help for an element that is non-relevant, but we allow readonly if (xformsControl != null && xformsControl instanceof XFormsSingleNodeControl) { final XFormsSingleNodeControl xformsSingleNodeControl = (XFormsSingleNodeControl) xformsControl; if (xformsSingleNodeControl.isRelevant()) return helpEffectiveControlId; else return null; } else { return null; } } public void addServerError(ServerError serverError) { final int maxErrors = getShowMaxRecoverableErrors(); if (maxErrors > 0) { if (serverErrors == null) serverErrors = new ArrayList<ServerError>(); if (serverErrors.size() < maxErrors) serverErrors.add(serverError); } } public List<ServerError> getServerErrors() { return serverErrors != null ? serverErrors : Collections.<ServerError>emptyList(); } public Set<String> getControlsStructuralChanges() { return controlsStructuralChanges != null ? controlsStructuralChanges : Collections.<String>emptySet(); } public void addControlStructuralChange(String prefixedId) { if (this.controlsStructuralChanges == null) this.controlsStructuralChanges = new HashSet<String>(); this.controlsStructuralChanges.add(prefixedId); } @Override public Scope innerScope() { // Do it here because at construction time, we don't yet have access to the static state! return staticState.topLevelPart().startScope(); } public void afterInitialResponse() { getRequestStats().afterInitialResponse(); this.uriResolver = null; // URI resolver is of no use after initialization and it may keep dangerous references (PipelineContext) this.response = null; // same as above this.initializing = false; clearClientState(); // client state can contain e.g. focus information, etc. set during initialization // Tell dependencies xpathDependencies.afterInitialResponse(); } /** * Prepare the document for a sequence of external events. * * @param response ExternalContext.Response for xf:submission[@replace = 'all'], or null */ public void beforeExternalEvents(ExternalContext.Response response) { // Tell dependencies xpathDependencies.beforeUpdateResponse(); // Remember OutputStream this.response = response; // Process completed asynchronous submissions if any processCompletedAsynchronousSubmissions(false, false); } /** * End a sequence of external events. * */ public void afterExternalEvents() { processCompletedAsynchronousSubmissions(false, true); processDueDelayedEvents(); this.response = null; } /** * Called after sending a successful update response. */ public void afterUpdateResponse() { getRequestStats().afterUpdateResponse(); clearClientState(); xformsControls.afterUpdateResponse(); // Tell dependencies xpathDependencies.afterUpdateResponse(); } public void rememberLastAjaxResponse(SAXStore response) { lastAjaxResponse = response; } public long getSequence() { return sequence; } /** * Return an OutputStream for xf:submission[@replace = 'all']. Used by submission. * * @return OutputStream */ public ExternalContext.Response getResponse() { return response; } public AsynchronousSubmissionManager getAsynchronousSubmissionManager(boolean create) { if (asynchronousSubmissionManager == null && create) asynchronousSubmissionManager = new AsynchronousSubmissionManager(this); return asynchronousSubmissionManager; } private void processCompletedAsynchronousSubmissions(boolean skipDeferredEventHandling, boolean addPollEvent) { final AsynchronousSubmissionManager manager = getAsynchronousSubmissionManager(false); if (manager != null && manager.hasPendingAsynchronousSubmissions()) { if (!skipDeferredEventHandling) startOutermostActionHandler(); manager.processCompletedAsynchronousSubmissions(); if (!skipDeferredEventHandling) endOutermostActionHandler(); // Remember to send a poll event if needed if (addPollEvent) manager.addClientDelayEventIfNeeded(); } } private void createControlsAndModels() { addAllModels(); xformsControls = new XFormsControls(this); } public void initializeNestedControls() { // Call-back from super class models initialization // This is important because if controls use binds, those must be up to date. In addition, MIP values will be up // to date. Finally, upon receiving xforms-ready after initialization, it is better if calculations and // validations are up to date. rebuildRecalculateRevalidateIfNeeded(); // Initialize controls xformsControls.createControlTree( scala.Option.<scala.collection.immutable.Map<String, ControlState >>apply(null)); } @Override public Seq<XFormsControl> getChildrenControls(XFormsControls controls) { return controls.getCurrentControlTree().children(); } /** * Register that an upload has started. */ public void startUpload(String uploadId) { if (pendingUploads == null) pendingUploads = new HashSet<String>(); pendingUploads.add(uploadId); } /** * Register that an upload has ended. */ public void endUpload(String uploadId) { // NOTE: Don't enforce existence of upload, as this is also called if upload control becomes non-relevant, and // also because asynchronously if the client notifies us to end an upload after a control has become non-relevant, // we don't want to fail. if (pendingUploads != null) pendingUploads.remove(uploadId); } public Set<String> getPendingUploads() { if (pendingUploads == null) return Collections.emptySet(); else return pendingUploads; } /** * Return the number of pending uploads. */ public int countPendingUploads() { return (pendingUploads == null) ? 0 : pendingUploads.size(); } /** * Whether an upload is pending for the given upload control. */ public boolean isUploadPendingFor(XFormsUploadControl uploadControl) { return (pendingUploads != null) && pendingUploads.contains(uploadControl.getUploadUniqueId()); } /** * Called when this document is added to the document cache. */ public void added() { XFormsStateManager.instance().onAddedToCache(getUUID()); } /** * Called when somebody explicitly removes this document from the document cache. */ public void removed() { // WARNING: This can be called while another threads owns this document lock XFormsStateManager.instance().onRemovedFromCache(getUUID()); } /** * Called by the cache to check that we are ready to be evicted from cache. * * @return lock or null in case session just expired */ public Lock getEvictionLock() { return XFormsStateManager.getDocumentLockOrNull(getUUID()); } /** * Called when cache expires this document from the document cache. */ public void evicted() { // WARNING: This could have been called while another threads owns this document lock, but the cache now obtains // the lock on the document first and will not evict us if we have the lock. This means that this will be called // only if no thread is dealing with this document. XFormsStateManager.instance().onEvictedFromCache(this); } }