/*
* (C) Copyright 2013 Kurento (http://kurento.org/)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl-2.1.html
*
* This library 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.
*
*/
package com.kurento.kmf.content.internal.base;
import static com.kurento.kmf.content.jsonrpc.JsonRpcConstants.METHOD_EXECUTE;
import static com.kurento.kmf.content.jsonrpc.JsonRpcConstants.METHOD_START;
import java.io.IOException;
import java.util.concurrent.Future;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.kurento.kmf.common.exception.KurentoMediaFrameworkException;
import com.kurento.kmf.common.exception.internal.ExceptionUtils;
import com.kurento.kmf.common.exception.internal.ServletUtils;
import com.kurento.kmf.content.ContentHandler;
import com.kurento.kmf.content.ContentSession;
import com.kurento.kmf.content.internal.ContentApiExecutorService;
import com.kurento.kmf.content.internal.ContentApiWebApplicationInitializer;
import com.kurento.kmf.content.internal.ContentAsyncListener;
import com.kurento.kmf.content.internal.ContentSessionManager;
import com.kurento.kmf.content.internal.ControlProtocolManager;
import com.kurento.kmf.content.internal.RejectableRunnable;
import com.kurento.kmf.content.jsonrpc.JsonRpcRequest;
import com.kurento.kmf.content.jsonrpc.JsonRpcResponse;
import com.kurento.kmf.spring.KurentoApplicationContextUtils;
/**
*
* Abstract class with the definition for Handler Servlets.
*
* @author Luis López (llopez@gsyc.es)
* @version 1.0.0
*/
public abstract class AbstractContentHandlerServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Autowired
private ContentApiExecutorService executor;
@Autowired
private ControlProtocolManager protocolManager;
@Autowired
protected ContentHandler<? extends ContentSession> handler;
protected ContentSessionManager contentSessionManager;
// protected boolean useRedirectStrategy = true;
protected Class<?> handlerClass;
protected boolean useControlProtocol = false;
protected abstract boolean getUseJsonControlProtocol(Class<?> handlerClass)
throws ServletException;
protected abstract AbstractContentSession createContentSession(
AsyncContext asyncCtx, String contentId);
protected abstract Logger getLogger();
@Override
public void init() throws ServletException {
super.init();
// Recover application context associated to this servlet in this
// context
AnnotationConfigApplicationContext thisServletContext = KurentoApplicationContextUtils
.getKurentoServletApplicationContext(this.getClass(),
this.getServletName());
// If there is not application context associated to this servlet,
// create one
if (thisServletContext == null) {
// Locate the handler class associated to this servlet
String handlerClassName = this
.getInitParameter(ContentApiWebApplicationInitializer.HANDLER_CLASS_PARAM_NAME);
if (handlerClassName == null || handlerClassName.equals("")) {
String message = "Cannot find handler class associated to handler servlet with name "
+ this.getServletConfig().getServletName()
+ " and class " + this.getClass().getName();
getLogger().error(message);
throw new ServletException(message);
}
// Create application context for this servlet containing the
// handler
thisServletContext = KurentoApplicationContextUtils
.createKurentoHandlerServletApplicationContext(
this.getClass(), this.getServletName(),
this.getServletContext(), handlerClassName);
// useRedirectStrategy = getUseRedirectStrategy(handlerClass);
try {
handlerClass = Class.forName(handlerClassName);
} catch (ClassNotFoundException e) {
String message = "Cannot recover class " + handlerClass
+ " on classpath";
getLogger().error(message);
throw new ServletException(message);
}
useControlProtocol = getUseJsonControlProtocol(handlerClass);
}
// Make this servlet to receive beans to resolve the @Autowired present
// on it
KurentoApplicationContextUtils
.processInjectionBasedOnApplicationContext(this,
thisServletContext);
if (useControlProtocol) {
contentSessionManager = (ContentSessionManager) KurentoApplicationContextUtils
.getBean("contentSessionManager");
}
}
@Override
protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (useControlProtocol) {
ServletUtils
.sendHttpError(
req,
resp,
HttpServletResponse.SC_NOT_IMPLEMENTED,
"Only POST request are supported for this service. You can enable GET requests "
+ " by setting useControlProtocol to false on the appropriate handler annotation");
return;
}
getLogger().info("GET request received: " + req.getRequestURI());
if (!req.isAsyncSupported()) {
// Async context could not be created. It is not necessary to
// complete it. Just send error message to
ServletUtils
.sendHttpError(
req,
resp,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"AsyncContext could not be started. The application should add \"asyncSupported = true\" in all "
+ this.getClass().getName()
+ " instances and in all filters in the associated chain");
return;
}
if (handler == null) {
ServletUtils
.sendHttpError(
req,
resp,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
handler.getClass().getSimpleName()
+ " is null. This error means that you "
+ "need to provide a valid implementation of interface "
+ handler.getClass().getSimpleName());
return;
}
String contentId = req.getPathInfo();
if (contentId != null) {
contentId = contentId.substring(1);
}
AsyncContext asyncCtx = req.startAsync();
// Add listener for managing error conditions
asyncCtx.addListener(new ContentAsyncListener());
doRequest4SimpleHttpProtocol(asyncCtx, contentId, resp);
}
@Override
protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
getLogger().info("POST request received: " + req.getRequestURI());
if (!req.isAsyncSupported()) {
// Async context could not be created. It is not necessary to
// complete it. Just send error message to
ServletUtils
.sendHttpError(
req,
resp,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"AsyncContext could not be started. The application should add \"asyncSupported = true\" in all "
+ this.getClass().getName()
+ " instances and in all filters in the associated chain");
return;
}
if (handler == null) {
ServletUtils
.sendHttpError(
req,
resp,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
handler.getClass().getSimpleName()
+ " is null. This error means that you "
+ "need to provide a valid implementation of interface "
+ handler.getClass().getSimpleName());
return;
}
String contentId = req.getPathInfo();
if (contentId != null) {
contentId = contentId.substring(1);
}
AsyncContext asyncCtx = req.startAsync();
// Add listener for managing error conditions
asyncCtx.addListener(new ContentAsyncListener());
if (useControlProtocol) {
doRequest4JsonControlProtocol(asyncCtx, contentId, resp);
} else {
// TODO: we should check that the content type correspond to
// the ones we support. We should avoid receiving application/json
// here and send a coherent error message in that case because this
// case corresponds to using incorrectly annotations on handlers
doRequest4SimpleHttpProtocol(asyncCtx, contentId, resp);
}
}
/**
* Generic processor of HTTP request when not using JSON control procotol.
*
* @param asyncCtx
* Asynchronous context
* @param contentId
* Content unique identifier
* @param resp
* HTTP response
* @throws ServletException
* Exception in Servlet
* @throws IOException
* Input/Ouput Exception
*/
private void doRequest4SimpleHttpProtocol(AsyncContext asyncCtx,
String contentId, HttpServletResponse resp)
throws ServletException, IOException {
try {
AbstractContentSession contentRequest = createContentSession(
asyncCtx, contentId);
Future<?> future = executor.getExecutor()
.submit(createAsyncRequestProcessor(contentRequest, null,
asyncCtx));
// Store future and request for using it in case of error
asyncCtx.getRequest().setAttribute(
ContentAsyncListener.FUTURE_REQUEST_PROCESSOR_ATT_NAME,
future);
asyncCtx.getRequest().setAttribute(
ContentAsyncListener.CONTENT_REQUEST_ATT_NAME,
contentRequest);
} catch (KurentoMediaFrameworkException ke) {
getLogger().error(ke.getMessage(), ke);
ServletUtils.sendHttpError(
(HttpServletRequest) asyncCtx.getRequest(), resp,
ExceptionUtils.getHttpErrorCode(ke.getCode()),
ke.getMessage());
}
}
/**
* Generic processor of HTTP request when using JSON control protocol.
*
* @param asyncCtx
* Asynchronous context
* @param contentId
* Content unique identifier
* @param resp
* HTTP response
* @throws ServletException
* Exception in servlet
* @throws IOException
* Input/Ouput Exception
*/
private void doRequest4JsonControlProtocol(AsyncContext asyncCtx,
String contentId, HttpServletResponse resp)
throws ServletException, IOException {
JsonRpcRequest message = null;
try {
message = protocolManager.receiveJsonRequest(asyncCtx);
if (message == null) {
throw new KurentoMediaFrameworkException(
"Null json message received", 10020);
}
AbstractContentSession contentSession = null;
String sessionId = message.getParams() != null ? message
.getParams().getSessionId() : null;
if (sessionId == null && message.getMethod().equals(METHOD_START)) {
// Session is created by a start request, we need to fill
// asyncCtx associated to start requests.
contentSession = createContentSession(asyncCtx, contentId);
contentSessionManager.put(contentSession);
} else if (sessionId == null
&& message.getMethod().equals(METHOD_EXECUTE)) {
// Session is created by an execute request, the asyncCtx for
// start requests must be set to null
contentSession = createContentSession(null, contentId);
contentSessionManager.put(contentSession);
} else if (sessionId != null) {
contentSession = contentSessionManager.get(sessionId);
if (contentSession == null) {
throw new KurentoMediaFrameworkException(
"Could not find contentSession object associated to sessionId "
+ sessionId, 10021);
}
} else {
throw new KurentoMediaFrameworkException(
"Could not find required sessionId field in request",
10022);
}
Future<?> future = executor.getExecutor().submit(
createAsyncRequestProcessor(contentSession, message,
asyncCtx));
// Store future for using it in ContentAsyncListener in case of
// error
asyncCtx.getRequest().setAttribute(
ContentAsyncListener.FUTURE_REQUEST_PROCESSOR_ATT_NAME,
future);
asyncCtx.getRequest().setAttribute(
ContentAsyncListener.CONTENT_REQUEST_ATT_NAME,
contentSession);
asyncCtx.getRequest()
.setAttribute(
ContentAsyncListener.CONTROL_PROTOCOL_REQUEST_MESSAGE_ATT_NAME,
message);
} catch (KurentoMediaFrameworkException ke) {
int reqId = message != null ? message.getId() : 0;
protocolManager.sendJsonError(
asyncCtx,
JsonRpcResponse.newError(
ExceptionUtils.getJsonErrorCode(ke.getCode()),
ke.getMessage(), reqId));
} catch (Throwable t) {
int reqId = message != null ? message.getId() : 0;
protocolManager.sendJsonError(asyncCtx, JsonRpcResponse.newError(
ExceptionUtils.getJsonErrorCode(1), t.getMessage(), reqId));
}
}
private RejectableRunnable createAsyncRequestProcessor(
AbstractContentSession contentRequest, JsonRpcRequest message,
AsyncContext asyncCtx) {
return (AsyncContentRequestProcessor) KurentoApplicationContextUtils
.getBean("asyncContentRequestProcessor", contentRequest,
message, asyncCtx);
}
}