/** * 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.common.OXFException; import org.orbeon.oxf.externalcontext.AsyncRequest; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.externalcontext.LocalExternalContext; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.util.IndentedLogger; import org.orbeon.oxf.util.NetUtils; import org.orbeon.oxf.xforms.XFormsContainingDocument; import org.orbeon.oxf.xforms.event.XFormsEvents; import java.util.concurrent.*; /** * Handle asynchronous submissions. * * The CompletionService is stored in the session, indexed by document UUID. * * See https://doc.orbeon.com/xforms/submission-asynchronous.html * See http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ExecutorCompletionService.html */ public class AsynchronousSubmissionManager { private static final String ASYNC_SUBMISSIONS_SESSION_KEY_PREFIX = "oxf.xforms.state.async-submissions."; // Global thread pool private static final ExecutorService threadPool = Executors.newCachedThreadPool(); private final XFormsContainingDocument containingDocument; public AsynchronousSubmissionManager(XFormsContainingDocument containingDocument) { this.containingDocument = containingDocument; } /** * Add a special delay event to the containing document if there are pending submissions. * * This should be called just before sending an Ajax response. */ public void addClientDelayEventIfNeeded() { if (hasPendingAsynchronousSubmissions()) { // NOTE: Could get isShowProgress() from submission, but default must be false containingDocument.addDelayedEvent( XFormsEvents.XXFORMS_POLL, containingDocument.getEffectiveId(), false, false, System.currentTimeMillis() + containingDocument.getSubmissionPollDelay(), true, false ); } } private static String getSessionKey(XFormsContainingDocument containingDocument) { return getSessionKey(containingDocument.getUUID()); } private static String getSessionKey(String documentUUID) { return ASYNC_SUBMISSIONS_SESSION_KEY_PREFIX + documentUUID; } private static AsynchronousSubmissions getAsynchronousSubmissions(boolean create, String sessionKey) { final ExternalContext.Session session = NetUtils.getExternalContext().getRequest().getSession(true); final AsynchronousSubmissions existingAsynchronousSubmissions = (AsynchronousSubmissions) session.javaGetAttribute(sessionKey); if (existingAsynchronousSubmissions != null) { return existingAsynchronousSubmissions; } else if (create) { final AsynchronousSubmissions asynchronousSubmissions = new AsynchronousSubmissions(); session.javaSetAttribute(sessionKey, asynchronousSubmissions); return asynchronousSubmissions; } else { return null; } } public void addAsynchronousSubmission(final Callable<SubmissionResult> callable) { final AsynchronousSubmissions asynchronousSubmissions = getAsynchronousSubmissions(true, getSessionKey(containingDocument)); // NOTE: If we want to re-enable foreground async submissions, we must: // - do a better detection: !(xf-submit-done/xf-submit-error listener) && replace="none" // - OR provide an explicit hint on xf:submission asynchronousSubmissions.submit(new Callable<SubmissionResult>() { // Submission should not need an ExternalContext, but if it does we must provide access to a safe one final ExternalContext currentExternalContext = NetUtils.getExternalContext(); final ExternalContext newExternalContext = new LocalExternalContext( currentExternalContext.getWebAppContext(), new AsyncRequest(currentExternalContext.getRequest()), currentExternalContext.getResponse()); public SubmissionResult call() throws Exception { // Make sure an ExternalContext is scoped for the callable. We use the same external context as the caller, // even though that can be a dangerous. Should we use AsyncExternalContext here? // Candidate for Scala withPipelineContext final PipelineContext pipelineContext = new PipelineContext(); pipelineContext.setAttribute(PipelineContext.EXTERNAL_CONTEXT, newExternalContext); boolean success = false; try { // Perform call final SubmissionResult result = callable.call(); success = true; return result; } finally { pipelineContext.destroy(success); } } }); } public boolean hasPendingAsynchronousSubmissions() { final AsynchronousSubmissions asynchronousSubmissions = getAsynchronousSubmissions(false, getSessionKey(containingDocument)); return asynchronousSubmissions != null && asynchronousSubmissions.getPendingCount() > 0; } /** * Process all pending asynchronous submissions if any. If processing of a particular submission causes new * asynchronous submissions to be started, also wait for the completion of those. * * Submissions are processed in the order in which they are made available upon termination by the completion * service. */ public void processAllAsynchronousSubmissions() { final AsynchronousSubmissions asynchronousSubmissions = getAsynchronousSubmissions(false, getSessionKey(containingDocument)); if (asynchronousSubmissions != null && asynchronousSubmissions.getPendingCount() > 0) { final IndentedLogger indentedLogger = containingDocument.getIndentedLogger(XFormsModelSubmission.LOGGING_CATEGORY); indentedLogger.startHandleOperation("", "processing all background asynchronous submissions"); int processedCount = 0; try { while (asynchronousSubmissions.getPendingCount() > 0) { try { // Handle next completed task final Future<SubmissionResult> future = asynchronousSubmissions.take(); final SubmissionResult result = future.get(); // Process response by dispatching an event to the submission final XFormsModelSubmission submission = (XFormsModelSubmission) containingDocument.getObjectByEffectiveId(result.getSubmissionEffectiveId()); submission.doSubmitReplace(result); } catch (Throwable throwable) { // Something bad happened throw new OXFException(throwable); } processedCount++; } } finally { indentedLogger.endHandleOperation("processed", Integer.toString(processedCount)); } } } /** * Process all completed asynchronous submissions if any. This method returns as soon as no completed submission is * available. * * Submissions are processed in the order in which they are made available upon termination by the completion * service. */ public void processCompletedAsynchronousSubmissions() { final AsynchronousSubmissions asynchronousSubmissions = getAsynchronousSubmissions(false, getSessionKey(containingDocument)); if (asynchronousSubmissions != null && asynchronousSubmissions.getPendingCount() > 0) { final IndentedLogger indentedLogger = containingDocument.getIndentedLogger(XFormsModelSubmission.LOGGING_CATEGORY); indentedLogger.startHandleOperation("", "processing completed background asynchronous submissions"); int processedCount = 0; try { Future<SubmissionResult> future = asynchronousSubmissions.poll(); while (future != null) { try { // Handle next completed task final SubmissionResult result = future.get(); // Process response by dispatching an event to the submission final XFormsModelSubmission submission = (XFormsModelSubmission) containingDocument.getObjectByEffectiveId(result.getSubmissionEffectiveId()); submission.doSubmitReplace(result); } catch (Throwable throwable) { // Something bad happened throw new OXFException(throwable); } processedCount++; future = asynchronousSubmissions.poll(); } } finally { indentedLogger.endHandleOperation("processed", Integer.toString(processedCount), "pending", Integer.toString(asynchronousSubmissions.getPendingCount())); } } } private static class AsynchronousSubmissions { private final CompletionService<SubmissionResult> completionService = new ExecutorCompletionService<SubmissionResult>(threadPool); private int pendingCount = 0; public Future<SubmissionResult> submit(Callable<SubmissionResult> task) { final Future<SubmissionResult> future = completionService.submit(task); pendingCount++; return future; } public Future<SubmissionResult> poll() { final Future<SubmissionResult> future = completionService.poll(); if (future != null) pendingCount--; return future; } public Future<SubmissionResult> take() throws InterruptedException { final Future<SubmissionResult> future = completionService.take(); pendingCount--; return future; } public int getPendingCount() { return pendingCount; } } }