
org.atmosphere.cpr.AsynchronousProcessor Maven / Gradle / Ivy
/*
* Copyright 2013 Jeanfrancois Arcand
*
* Licensed 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
/*
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2007-2008 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can obtain
* a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
* or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
* Sun designates this particular file as subject to the "Classpath" exception
* as provided by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the License
* Header, with the fields enclosed by brackets [] replaced by your own
* identifying information: "Portions Copyrighted [year]
* [name of copyright owner]"
*
* Contributor(s):
*
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*
*/
package org.atmosphere.cpr;
import org.atmosphere.util.Utils;
import org.atmosphere.util.uri.UriTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.atmosphere.cpr.Action.TYPE.SKIP_ATMOSPHEREHANDLER;
import static org.atmosphere.cpr.ApplicationConfig.MAX_INACTIVE;
import static org.atmosphere.cpr.AtmosphereFramework.AtmosphereHandlerWrapper;
import static org.atmosphere.cpr.HeaderConfig.X_ATMOSPHERE_ERROR;
import static org.atmosphere.cpr.HeaderConfig.X_ATMOSPHERE_TRANSPORT;
/**
* Base class which implement the semantics of suspending and resuming of a
* Comet/WebSocket Request.
*
* @author Jeanfrancois Arcand
*/
public abstract class AsynchronousProcessor implements AsyncSupport {
private static final Logger logger = LoggerFactory.getLogger(AsynchronousProcessor.class);
protected static final Action timedoutAction = new Action(Action.TYPE.TIMEOUT);
protected static final Action cancelledAction = new Action(Action.TYPE.CANCELLED);
protected final AtmosphereConfig config;
protected final ConcurrentHashMap
aliveRequests = new ConcurrentHashMap();
private boolean trackActiveRequest = false;
private final ScheduledExecutorService closedDetector = Executors.newScheduledThreadPool(1);
public AsynchronousProcessor(AtmosphereConfig config) {
this.config = config;
}
@Override
public void init(ServletConfig sc) throws ServletException {
String maxInactive = sc.getInitParameter(MAX_INACTIVE) != null ? sc.getInitParameter(MAX_INACTIVE) :
config.getInitParameter(MAX_INACTIVE);
if (maxInactive != null) {
trackActiveRequest = true;
final long maxInactiveTime = Long.parseLong(maxInactive);
if (maxInactiveTime <= 0) return;
closedDetector.scheduleAtFixedRate(new Runnable() {
public void run() {
for (AtmosphereRequest req : aliveRequests.keySet()) {
long l = (Long) req.getAttribute(MAX_INACTIVE);
if (l > 0 && System.currentTimeMillis() - l > maxInactiveTime) {
try {
logger.debug("Close detector disconnecting {}. Current size {}", req.resource(), aliveRequests.size());
AtmosphereResourceImpl r = (AtmosphereResourceImpl) aliveRequests.remove(req);
cancelled(req, r.getResponse(false));
} catch (Throwable e) {
logger.warn("closedDetector", e);
} finally {
try {
req.setAttribute(MAX_INACTIVE, (long) -1);
} catch (Throwable t) {
logger.trace("closedDetector", t);
}
}
}
}
}
}, 0, 1, TimeUnit.SECONDS);
}
}
/**
* Is {@link HttpSession} supported
*
* @return true if supported
*/
protected boolean supportSession() {
return config.isSupportSession();
}
/**
* Return the container's name.
*/
public String getContainerName() {
return config.getServletConfig().getServletContext().getServerInfo();
}
/**
* All proprietary Comet based {@link Servlet} must invoke the suspended
* method when the first request comes in. The returned value, of type
* {@link Action}, tells the proprietary Comet {@link Servlet}
* to suspended or not the current {@link AtmosphereResponse}.
*
* @param request the {@link AtmosphereRequest}
* @param response the {@link AtmosphereResponse}
* @return action the Action operation.
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
public Action suspended(AtmosphereRequest request, AtmosphereResponse response) throws IOException, ServletException {
return action(request, response);
}
/**
* Invoke the {@link AtmosphereHandler#onRequest} method.
*
* @param req the {@link AtmosphereRequest}
* @param res the {@link AtmosphereResponse}
* @return action the Action operation.
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
Action action(AtmosphereRequest req, AtmosphereResponse res) throws IOException, ServletException {
if (!Utils.properProtocol(req) || (Utils.webSocketEnabled(req) && !supportWebSocket())) {
res.setStatus(501);
res.addHeader(X_ATMOSPHERE_ERROR, "Websocket protocol not supported");
res.flushBuffer();
return new Action();
}
if (config.handlers().isEmpty()) {
logger.error("No AtmosphereHandler defined or scanned. Make sure you define it inside META-INF/atmosphere.xml or annotate using @AtmosphereHandlerService and added org.atmosphere.cpr.packages as init-param");
throw new AtmosphereMappingException("No AtmosphereHandler found.");
}
if (res.request() == null) {
res.request(req);
}
if (supportSession()) {
// Create the session needed to support the Resume
// operation from disparate requests.
HttpSession session = req.getSession(true);
}
req.setAttribute(FrameworkConfig.SUPPORT_SESSION, supportSession());
AtmosphereHandlerWrapper handlerWrapper = map(req);
// Check Broadcaster state. If destroyed, replace it.
Broadcaster b = handlerWrapper.broadcaster;
if (b.isDestroyed()) {
BroadcasterFactory f = config.getBroadcasterFactory();
synchronized (f) {
f.remove(b, b.getID());
try {
handlerWrapper.broadcaster = f.get(b.getID());
} catch (IllegalStateException ex) {
// Something wrong occurred, let's not fail and loookup the value
logger.trace("", ex);
// fallback to lookup
handlerWrapper.broadcaster = f.lookup(b.getID(), true);
}
}
}
AtmosphereResourceImpl resource = (AtmosphereResourceImpl) req.getAttribute(FrameworkConfig.INJECTED_ATMOSPHERE_RESOURCE);
if (resource == null) {
// TODO: cast is dangerous
resource = (AtmosphereResourceImpl)
AtmosphereResourceFactory.getDefault().create(config, handlerWrapper.broadcaster, res, this, handlerWrapper.atmosphereHandler);
} else {
// TODO: This piece of code can be removed, but for backward compat with existing extension we needs it for now.
try {
// Make sure it wasn't set before
resource.getBroadcaster();
} catch (IllegalStateException ex) {
resource.setBroadcaster(handlerWrapper.broadcaster);
}
resource.atmosphereHandler(handlerWrapper.atmosphereHandler);
}
req.setAttribute(FrameworkConfig.ATMOSPHERE_RESOURCE, resource);
req.setAttribute(FrameworkConfig.ATMOSPHERE_HANDLER, handlerWrapper.atmosphereHandler);
req.setAttribute(SKIP_ATMOSPHEREHANDLER.name(), Boolean.FALSE);
// Globally defined
Action a = invokeInterceptors(config.framework().interceptors(), resource);
if (a.type() != Action.TYPE.CONTINUE) {
return a;
}
// Per AtmosphereHandler
a = invokeInterceptors(handlerWrapper.interceptors, resource);
if (a.type() != Action.TYPE.CONTINUE) {
return a;
}
//Unit test mock the request and will throw NPE.
boolean skipAtmosphereHandler = req.getAttribute(SKIP_ATMOSPHEREHANDLER.name()) != null
? (Boolean) req.getAttribute(SKIP_ATMOSPHEREHANDLER.name()) : Boolean.FALSE;
if (!skipAtmosphereHandler) {
try {
handlerWrapper.atmosphereHandler.onRequest(resource);
} catch (IOException t) {
resource.onThrowable(t);
throw t;
}
}
postInterceptors(handlerWrapper.interceptors, resource);
postInterceptors(config.framework().interceptors(), resource);
if (trackActiveRequest && resource.getAtmosphereResourceEvent().isSuspended() && req.getAttribute(FrameworkConfig.CANCEL_SUSPEND_OPERATION) == null) {
req.setAttribute(MAX_INACTIVE, System.currentTimeMillis());
aliveRequests.put(req, resource);
}
Action action = skipAtmosphereHandler ? Action.CANCELLED : resource.action();
if (supportSession() && action.type().equals(Action.TYPE.SUSPEND)) {
// Do not allow times out.
SessionTimeoutSupport.setupTimeout(req.getSession());
}
logger.trace("Action for {} was {} with transport " + req.getHeader(X_ATMOSPHERE_TRANSPORT), req.resource() != null ? req.resource().uuid() : "null", action);
return action;
}
private Action invokeInterceptors(List c, AtmosphereResource r) {
Action a = Action.CONTINUE;
for (AtmosphereInterceptor arc : c) {
a = arc.inspect(r);
if (a == null) {
a = Action.CANCELLED;
}
boolean skip = a.type() == SKIP_ATMOSPHEREHANDLER;
if (skip) {
logger.trace("AtmosphereInterceptor {} asked to skip the AtmosphereHandler", arc);
r.getRequest().setAttribute(SKIP_ATMOSPHEREHANDLER.name(), Boolean.TRUE);
}
if (a.type() != Action.TYPE.CONTINUE) {
logger.trace("Interceptor {} interrupted the dispatch with {}", arc, a);
return a;
}
}
return a;
}
private void postInterceptors(List c, AtmosphereResource r) {
for (int i = c.size() - 1; i > -1; i--) {
c.get(i).postInspect(r);
}
}
/**
* {@inheritDoc}
*/
public void action(AtmosphereResourceImpl r) {
if (trackActiveRequest) {
aliveRequests.remove(r.getRequest(false));
}
}
protected AtmosphereHandlerWrapper map(String path) {
AtmosphereHandlerWrapper atmosphereHandlerWrapper = config.handlers().get(path);
if (atmosphereHandlerWrapper == null) {
final Map m = new HashMap();
for (Map.Entry e : config.handlers().entrySet()) {
UriTemplate t = new UriTemplate(e.getKey());
logger.trace("Trying to map {} to {}", t, path);
if (t.match(path, m)) {
atmosphereHandlerWrapper = e.getValue();
logger.trace("Mapped {} to {}", t, e.getValue().atmosphereHandler);
break;
}
}
}
return atmosphereHandlerWrapper;
}
/**
* Return the {@link AtmosphereHandler} mapped to the passed servlet-path.
*
* @param req the {@link AtmosphereResponse}
* @return the {@link AtmosphereHandler} mapped to the passed servlet-path.
* @throws javax.servlet.ServletException
*/
protected AtmosphereHandlerWrapper map(AtmosphereRequest req) throws ServletException {
String path;
String pathInfo = null;
try {
pathInfo = req.getPathInfo();
} catch (IllegalStateException ex) {
// http://java.net/jira/browse/GRIZZLY-1301
}
if (pathInfo != null) {
path = req.getServletPath() + pathInfo;
} else {
path = req.getServletPath();
}
if (path == null || path.isEmpty()) {
path = "/";
}
AtmosphereHandlerWrapper atmosphereHandlerWrapper = map(path + (path.endsWith("/") ? "all" : "/all"));
if (atmosphereHandlerWrapper == null) {
// (2) First, try exact match
atmosphereHandlerWrapper = map(path);
if (atmosphereHandlerWrapper == null) {
// (3) Wildcard
atmosphereHandlerWrapper = map(path + "*");
// (4) try without a path
if (atmosphereHandlerWrapper == null) {
String p = path.lastIndexOf("/") == 0 ? "/" : path.substring(0, path.lastIndexOf("/"));
while (p.length() > 0) {
atmosphereHandlerWrapper = map(p);
// (3.1) Try path wildcard
if (atmosphereHandlerWrapper != null) {
break;
}
p = p.substring(0, p.lastIndexOf("/"));
}
}
// Glassfish 3.1.2 issue .. BEUUUURRRRKKKKKK!!
if (atmosphereHandlerWrapper == null) {
if (path.indexOf(req.getContextPath()) != -1) {
path = path.substring(req.getContextPath().length());
}
atmosphereHandlerWrapper = map(path);
}
}
}
if (atmosphereHandlerWrapper == null) {
logger.debug("No AtmosphereHandler maps request for {} with mapping {}", path, config.handlers());
throw new AtmosphereMappingException("No AtmosphereHandler maps request for " + path);
}
config.getBroadcasterFactory().add(atmosphereHandlerWrapper.broadcaster,
atmosphereHandlerWrapper.broadcaster.getID());
return atmosphereHandlerWrapper;
}
/**
* All proprietary Comet based {@link Servlet} must invoke the resume
* method when the Atmosphere's application decide to resume the {@link AtmosphereResponse}.
* The returned value, of type
* {@link Action}, tells the proprietary Comet {@link Servlet}
* to resume (again), suspended or do nothing with the current {@link AtmosphereResponse}.
*
* @param request the {@link AtmosphereRequest}
* @param response the {@link AtmosphereResponse}
* @return action the Action operation.
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
public Action resumed(AtmosphereRequest request, AtmosphereResponse response)
throws IOException, ServletException {
return action(request, response);
}
/**
* All proprietary Comet based {@link Servlet} must invoke the timedout
* method when the underlying WebServer time out the {@link AtmosphereResponse}.
* The returned value, of type
* {@link Action}, tells the proprietary Comet {@link Servlet}
* to resume (again), suspended or do nothing with the current {@link AtmosphereResponse}.
*
* @param req the {@link AtmosphereRequest}
* @param res the {@link AtmosphereResponse}
* @return action the Action operation.
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
public Action timedout(AtmosphereRequest req, AtmosphereResponse res)
throws IOException, ServletException {
logger.trace("Timing out {}", req);
if (trackActiveRequest(req) && completeLifecycle(req.resource(), false)) {
config.framework().notify(Action.TYPE.TIMEOUT, req, res);
}
return timedoutAction;
}
protected boolean trackActiveRequest(AtmosphereRequest req) {
if (trackActiveRequest) {
try {
long l = (Long) req.getAttribute(MAX_INACTIVE);
if (l == -1) {
// The closedDetector closed the connection.
return false;
}
req.setAttribute(MAX_INACTIVE, (long) -1);
// GlassFish
} catch (Throwable ex) {
logger.trace("Request already recycled", req);
// Request is no longer active, return
return false;
}
}
return true;
}
/**
* Cancel or times out an {@link AtmosphereResource} by invoking it's associated {@link AtmosphereHandler#onStateChange(AtmosphereResourceEvent)}
*
* @param r an {@link AtmosphereResource}
* @param cancelled true if cancelled, false if timedout
* @return true if the operation was executed.
*/
protected boolean completeLifecycle(final AtmosphereResource r, boolean cancelled) {
if (r != null) {
logger.debug("Finishing lifecycle for AtmosphereResource {}", r.uuid());
final AtmosphereResourceImpl impl = AtmosphereResourceImpl.class.cast(r);
synchronized (impl) {
try {
if (impl.isCancelled()) {
logger.trace("{} is already cancelled", impl.uuid());
return false;
}
if (cancelled) {
impl.getAtmosphereResourceEvent().setCancelled(true);
} else {
impl.getAtmosphereResourceEvent().setIsResumedOnTimeout(true);
Broadcaster b = r.getBroadcaster();
if (b instanceof DefaultBroadcaster) {
((DefaultBroadcaster) b).broadcastOnResume(r);
}
// impl.getAtmosphereResourceEvent().setIsResumedOnTimeout(impl.resumeOnBroadcast());
}
invokeAtmosphereHandler(impl);
try {
impl.getResponse().getOutputStream().close();
} catch (Throwable t) {
try {
impl.getResponse().getWriter().close();
} catch (Throwable t2) {
}
}
} catch (Throwable ex) {
// Something wrong happenned, ignore the exception
logger.trace("Failed to cancel resource: {}", impl.uuid(), ex);
} finally {
try {
impl.notifyListeners();
impl.setIsInScope(false);
impl.cancel();
} catch (Throwable t) {
logger.trace("completeLifecycle", t);
} finally {
impl._destroy();
}
}
}
return true;
} else {
logger.debug("AtmosphereResource was null, failed to cancel AtmosphereRequest {}");
return false;
}
}
/**
* Invoke the associated {@link AtmosphereHandler}. This method must be synchronized on an AtmosphereResource
*
* @param r a {@link AtmosphereResourceImpl}
* @throws IOException
*/
protected void invokeAtmosphereHandler(AtmosphereResourceImpl r) throws IOException {
if (!r.isInScope()) {
logger.trace("AtmosphereResource out of scope {}", r.uuid());
return;
}
AtmosphereRequest req = r.getRequest(false);
String disableOnEvent = r.getAtmosphereConfig().getInitParameter(ApplicationConfig.DISABLE_ONSTATE_EVENT);
r.getAtmosphereResourceEvent().setMessage(r.writeOnTimeout());
try {
if (disableOnEvent == null || !disableOnEvent.equals(String.valueOf(true))) {
AtmosphereHandler atmosphereHandler =
(AtmosphereHandler)
req.getAttribute(FrameworkConfig.ATMOSPHERE_HANDLER);
if (atmosphereHandler != null) {
try {
atmosphereHandler.onStateChange(r.getAtmosphereResourceEvent());
} finally {
Meteor m = (Meteor) req.getAttribute(AtmosphereResourceImpl.METEOR);
if (m != null) {
m.destroy();
}
req.removeAttribute(FrameworkConfig.ATMOSPHERE_RESOURCE);
}
}
}
} catch (IOException ex) {
try {
r.onThrowable(ex);
} catch (Throwable t) {
logger.warn("failed calling onThrowable()", ex);
}
}
}
/**
* All proprietary Comet based {@link Servlet} must invoke the cancelled
* method when the underlying WebServer detect that the client closed
* the connection.
*
* @param req the {@link AtmosphereRequest}
* @param res the {@link AtmosphereResponse}
* @return action the Action operation.
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
public Action cancelled(AtmosphereRequest req, AtmosphereResponse res)
throws IOException, ServletException {
logger.trace("Cancelling {}", req);
if (trackActiveRequest(req) && completeLifecycle(req.resource(), true)) {
config.framework().notify(Action.TYPE.CANCELLED, req, res);
}
return cancelledAction;
}
protected void shutdown() {
closedDetector.shutdownNow();
for (AtmosphereResource resource : aliveRequests.values()) {
try {
resource.resume();
} catch (Throwable t) {
// Something wrong happenned, ignore the exception
logger.debug("failed on resume: " + resource, t);
}
}
}
@Override
public boolean supportWebSocket() {
return false;
}
/**
* An Callback class that can be used by Framework integrator to handle the close/timedout/resume life cycle
* of an {@link AtmosphereResource}. This class support only support {@link AsyncSupport} implementation that
* extends {@link AsynchronousProcessor}
*/
public final static class AsynchronousProcessorHook {
private final AtmosphereResourceImpl r;
public AsynchronousProcessorHook(AtmosphereResourceImpl r) {
this.r = r;
if (!AsynchronousProcessor.class.isAssignableFrom(r.asyncSupport.getClass())) {
throw new IllegalStateException("AsyncSupport must extends AsynchronousProcessor");
}
}
public void closed() {
try {
((AsynchronousProcessor) r.asyncSupport).cancelled(r.getRequest(false), r.getResponse(false));
} catch (IOException e) {
logger.debug("", e);
} catch (ServletException e) {
logger.debug("", e);
}
}
public void timedOut() {
try {
((AsynchronousProcessor) r.asyncSupport).timedout(r.getRequest(false), r.getResponse(false));
} catch (IOException e) {
logger.debug("", e);
} catch (ServletException e) {
logger.debug("", e);
}
}
public void resume() {
((AsynchronousProcessor) r.asyncSupport).action(r);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy