/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the License at the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apereo.portal.portlet.rendering.worker;
import com.google.common.util.concurrent.Futures;
import java.lang.Thread.State;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apereo.portal.portlet.om.IPortletWindow;
import org.apereo.portal.portlet.om.IPortletWindowId;
import org.apereo.portal.portlet.rendering.IPortletRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Base for portlet execution dispatching. Tracks the target, request, response objects as well as
* submitted, started and completed timestamps.
*/
abstract class PortletExecutionWorker<V> implements IPortletExecutionWorker<V> {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Map<String, Object> executionAttributes = new ConcurrentHashMap<String, Object>();
private final CountDownLatch startLatch = new CountDownLatch(1);
private final ExecutorService executorService;
private final List<IPortletExecutionInterceptor> interceptors;
final IPortletRenderer portletRenderer;
final IPortletWindowId portletWindowId;
final String portletFname;
final long timeout;
final HttpServletRequest request;
final HttpServletResponse response;
final SecurityContext springSecurityContext;
private volatile Future<V> future;
private volatile Thread workerThread;
private volatile long submitted = 0;
private volatile long started = 0;
private volatile long complete = 0;
private final AtomicInteger cancelCount = new AtomicInteger();
private final AtomicBoolean canceled = new AtomicBoolean();
private volatile boolean retrieved = false;
public PortletExecutionWorker(
ExecutorService executorService,
List<IPortletExecutionInterceptor> interceptors,
IPortletRenderer portletRenderer,
HttpServletRequest request,
HttpServletResponse response,
IPortletWindow portletWindow,
long timeout) {
this.executorService = executorService;
this.interceptors = interceptors;
this.portletRenderer = portletRenderer;
this.request = new GuardingHttpServletRequest(request, canceled);
this.response = new GuardingHttpServletResponse(response, canceled);
this.portletWindowId = portletWindow.getPortletWindowId();
this.portletFname = portletWindow.getPortletEntity().getPortletDefinition().getFName();
this.timeout = timeout;
// For now, use the SpringSecurityContext for the currently-executing HTTP Thread as the
// SpringSecurityContext for the portlet worker thread. In the future we may want to do
// more sophisticated behavior such as setting up the worker thread's GrantedAuthorities
// based on the information from the portlet.xml, such as the <security-role-ref> and
// <user-attribute> elements.
//
// However one IMPORTANT thing to note is that when SpringSecurity is in the mix the response
// objects are org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper
// objects which save the current SpringSecurityContext to the
// HttpSessionSecurityContextRepository when the response is flushed or closed,
// when an error occurs, or when an HTTP redirect is issued.
//
// At least when Spring Webflow is in the mix (which the admin pages currently use, and who
// knows when else it might occur), response objects (including the
// response objects GuardingHttpServletResponse delegates method calls to) are also
// org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper objects.
//
// If using the standard HTTP Session storage strategy, this means each portlet worker thread could replace
// the SpringSecurityContext in HTTP Session if the SpringSecurityContext object in the worker
// thread is not set or it was different than what was in HTTP Session, though once all
// worker threads complete (ASSUMING there is no issue and they all complete or abort BEFORE
// the HTTP Thread), the completion of the HTTP Thread would restore its thread-local
// SpringSecurityContext back into the HTTP Session. Still, there is an uncomfortable and
// VERY error-prone period of time where the SpringSecurityContext in HTTP Session
// is wrong. If another HTTP thread simultaneously executes for the same HTTP Session, it would
// execute using the wrong SpringSecurityContext object which would be very bad.
//
// So for now and possibly forever, we want the same SpringSecurityContext object in the worker
// thread as was in the HTTP thread. However in the future we could create a smarter
// SecurityContextRepository implementation (similar to HttpSessionSecurityContextRepository)
// that would save the SpringSecurityContext into HTTP Session for an HTTP thread
// but for a portlet worker thread save it into a portlet's session or not save it at all.
this.springSecurityContext = SecurityContextHolder.getContext();
}
@Override
public Object setExecutionAttribute(String name, Object value) {
if (value == null) {
return executionAttributes.remove(name);
}
return executionAttributes.put(name, value);
}
@Override
public Object getExecutionAttribute(String name) {
return executionAttributes.get(name);
}
@Override
public IPortletWindowId getPortletWindowId() {
return this.portletWindowId;
}
@Override
public String getPortletFname() {
return this.portletFname;
}
/** @return The timeout setting for the operation in process */
@Override
public long getApplicableTimeout() {
return this.timeout;
}
@Override
public final void submit() {
if (this.submitted > 0) {
throw new IllegalStateException(
this.getClass().getSimpleName()
+ " for "
+ this.getPortletWindowId()
+ " has already been submitted.");
}
this.submitted = System.currentTimeMillis();
try {
//Run pre-submit interceptors
for (final IPortletExecutionInterceptor interceptor : this.interceptors) {
interceptor.preSubmit(request, response, this);
}
final Callable<V> callable =
new PortletExecutionCallable<V>(
this,
new ExecutionLifecycleCallable<V>(
new Callable<V>() {
@Override
public V call() throws Exception {
return callInternal();
}
}));
this.future = this.executorService.submit(callable);
} catch (final Exception e) {
//All is not well do the basic portlet execution lifecycle and then, return a Future that simply rethrows the exception
final Callable<Future<V>> callable =
new ExecutionLifecycleCallable<Future<V>>(
new Callable<Future<V>>() {
@Override
public Future<V> call() throws Exception {
return Futures.immediateFailedFuture(e);
}
});
try {
this.future = callable.call();
} catch (Exception e1) {
//We know this will never throw
}
}
}
private final class ExecutionLifecycleCallable<V1> implements Callable<V1> {
private final Callable<V1> callable;
public ExecutionLifecycleCallable(Callable<V1> callable) {
this.callable = callable;
}
@Override
public V1 call() throws Exception {
startExecution();
try {
runPreExecutionInterceptors();
final V1 result = this.callable.call();
doPostExecution(null);
return result;
} catch (Exception e) {
doPostExecution(e);
throw e;
} finally {
executionComplete();
}
}
}
private void startExecution() {
//grab the current thread
workerThread = Thread.currentThread();
// Initialize the Spring Security Context for this thread.
SecurityContextHolder.setContext(springSecurityContext);
started = System.currentTimeMillis();
//signal any threads waiting for the worker to start
startLatch.countDown();
}
private void runPreExecutionInterceptors() {
for (final IPortletExecutionInterceptor interceptor : this.interceptors) {
interceptor.preExecution(request, response, this);
}
}
private void executionComplete() {
complete = System.currentTimeMillis();
if (logger.isDebugEnabled()) {
logger.debug(
"Execution complete on portlet "
+ portletWindowId
+ " in "
+ getDuration()
+ "ms");
}
workerThread = null;
}
private void doPostExecution(Exception e) {
//Iterate over handlers in reverse for post execution
final ListIterator<IPortletExecutionInterceptor> listIterator =
this.interceptors.listIterator(this.interceptors.size());
while (listIterator.hasPrevious()) {
final IPortletExecutionInterceptor interceptor = listIterator.previous();
try {
interceptor.postExecution(request, response, this, e);
} catch (Throwable ex2) {
logger.error("HandlerInterceptor.postExecution threw exception for {}", this, ex2);
}
}
}
/** @see Callable#call() */
protected abstract V callInternal() throws Exception;
@Override
public final boolean isStarted() {
return this.started > 0;
}
@Override
public boolean isSubmitted() {
return this.submitted > 0;
}
@Override
public boolean isComplete() {
return this.complete > 0
|| this.workerThread == null
|| this.workerThread.getState() == State.TERMINATED;
}
@Override
public boolean isRetrieved() {
return this.retrieved;
}
@Override
public final long waitForStart(long timeout) throws InterruptedException {
//Wait for start Callable to start
this.startLatch.await(timeout, TimeUnit.MILLISECONDS);
return this.started;
}
@Override
public V get(long timeout) throws Exception {
if (this.future == null) {
throw new IllegalStateException(
"submit() must be called before get(long) can be called");
}
this.retrieved = true;
try {
final long startTime = this.waitForStart(timeout);
final long waitTime = Math.max(0, timeout - (System.currentTimeMillis() - startTime));
return this.future.get(waitTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
this.logger.warn("Execution interrupted on portlet {}", this, e);
throw e;
} catch (ExecutionException e) {
this.logger.warn("Execution failed on portlet {}", this, e);
throw e;
} catch (TimeoutException e) {
final StringBuilder errorBuilder = new StringBuilder("Execution timed out on portlet ");
errorBuilder.append(this.toString());
final Thread localWorkerThread = workerThread;
if (localWorkerThread != null) {
final State state = localWorkerThread.getState();
final StackTraceElement[] stackTrace = localWorkerThread.getStackTrace();
errorBuilder.append("\n\tPortlet Thread State: ").append(state).append("\n");
errorBuilder.append("\tPortlet Thread Stack Trace: \n");
for (final StackTraceElement stackTraceElement : stackTrace) {
errorBuilder.append("\t\tat ").append(stackTraceElement).append("\n");
}
}
this.logger.warn(errorBuilder.toString());
throw e;
}
}
@Override
public final void cancel() {
if (this.future == null) {
throw new IllegalStateException(
"submit() must be called before cancel() can be called");
}
if (this.isComplete()) {
return;
}
//Mark worker as retrieved
this.retrieved = true;
//Notify the guarding req/res wrappers that cancel has been called
this.canceled.set(true);
//Cancel the future, interrupting the thread
this.future.cancel(true);
//Track the number of times cancel has been called
final int count = this.cancelCount.getAndIncrement();
if (count > 0) {
//Since Future.cancel only interrupts the thread on the first call interrupt the thread directly
final Thread thread = this.workerThread;
if (thread != null) {
thread.interrupt();
}
}
}
@Override
public final int getCancelCount() {
return this.cancelCount.get();
}
@Override
public final long getSubmittedTime() {
return this.submitted;
}
@Override
public final long getStartedTime() {
return this.started;
}
@Override
public final long getCompleteTime() {
return this.complete;
}
@Override
public final long getWait() {
return this.started - this.submitted;
}
@Override
public final long getDuration() {
if (this.complete > 0) {
return this.complete - this.submitted;
} else {
return System.currentTimeMillis() - this.submitted;
}
}
@Override
public String toString() {
return "PortletExecutionWorker ["
+ "portletFname="
+ this.portletFname
+ ", "
+ "timeout="
+ this.timeout
+ ", "
+ "portletWindowId="
+ this.portletWindowId
+ ", "
+ "started="
+ this.started
+ ", "
+ "submitted="
+ this.submitted
+ ", "
+ "complete="
+ this.complete
+ ", "
+ "retrieved="
+ this.retrieved
+ ", "
+ "canceled="
+ this.canceled
+ ", "
+ "cancelCount="
+ this.cancelCount
+ ", "
+ "wait="
+ this.getWait()
+ ", "
+ "duration="
+ this.getDuration()
+ "ms]";
}
}