/*
* (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;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import com.google.gson.Gson;
import com.kurento.kmf.common.exception.KurentoMediaFrameworkException;
import com.kurento.kmf.content.jsonrpc.JsonRpcRequest;
import com.kurento.kmf.content.jsonrpc.JsonRpcResponse;
/**
*
* This class handles the JSON-based representations for information exchange
* (media negotiation).
*
* @author Luis López (llopez@gsyc.es)
* @author Boni García (bgarcia@gsyc.es)
* @version 1.0.0
*/
public class ControlProtocolManager {
private static final Logger log = LoggerFactory
.getLogger(ControlProtocolManager.class);
private static final int BUFF = 4096;
/**
* Encodings accepted in JSON (UTF-8, UTF-16BE/LE, UTF-32BE/LE).
*/
private static final String UTF8 = "UTF-8";
private static final String UTF16BE = "UTF-16BE";
private static final String UTF16LE = "UTF-16LE";
private static final String UTF32BE = "UTF-32BE";
private static final String UTF32LE = "UTF-32LE";
private Gson gson;
/**
* Default constructor; it creates the Gson (Google JSON API) instance.
*/
public ControlProtocolManager() {
gson = new Gson();
}
/**
* Receiver method for JSON throw a request.
*
* @param asyncCtx
* Asynchronous context
* @return Received JSON encapsulated as a Java class
*/
public JsonRpcRequest receiveJsonRequest(AsyncContext asyncCtx) {
HttpServletRequest request = (HttpServletRequest) asyncCtx.getRequest();
// Received character encoding should be UTF-8. In order to check this,
// the method detectJsonEncoding will be used. Before that, the
// InputStream read from request.getInputStream() should be cloned
// (using a ByteArrayOutputStream) to be used on detectJsonEncoding and
// then for reading the JSON message
try {
InputStream inputStream = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFF];
int len;
while ((len = inputStream.read(buffer)) > -1) {
baos.write(buffer, 0, len);
}
baos.flush();
String encoding = detectJsonEncoding(new ByteArrayInputStream(
baos.toByteArray()));
log.debug("Detected JSON encoding: " + encoding);
if (encoding == null) {
throw new KurentoMediaFrameworkException(
"Invalid or unsupported charset encondig in received JSON request",
10018);
}
InputStreamReader isr = new InputStreamReader(
new ByteArrayInputStream(baos.toByteArray()), encoding);
JsonRpcRequest jsonRequest = gson.fromJson(isr,
JsonRpcRequest.class);
Assert.notNull(jsonRequest.getMethod());
log.info("Received JsonRpc request ...\n " + jsonRequest.toString());
return jsonRequest;
} catch (IOException e) {
// TODO: trace this exception and double check appropriate JsonRpc
// answer is sent
throw new KurentoMediaFrameworkException(
"IOException reading JsonRPC request. Cause: "
+ e.getMessage(), e, 20016);
}
}
/**
* Sender method for JSON throw a request.
*
* @param asyncCtx
* Asynchronous context
* @param message
* JSON message (as a Java class)
* @throws IOException
* Exception while parsing operating with asynchronous context
*/
public void sendJsonAnswer(AsyncContext asyncCtx, JsonRpcResponse message) {
internalSendJsonAnswer(asyncCtx, message);
}
/**
* Sender method for error messages in JSON throw a request.
*
* @param asyncCtx
* Asynchronous context
* @param message
* JSON error message (as a Java class)
* @throws IOException
* Exception while parsing operating with asynchronous context
*/
public void sendJsonError(AsyncContext asyncCtx, JsonRpcResponse message) {
try {
internalSendJsonAnswer(asyncCtx, message);
} catch (Throwable e) {
log.info("Cannot send answer message to destination", e);
} finally {
if (asyncCtx != null) {
try {
asyncCtx.complete();
} catch (IllegalStateException e) {
log.warn("Exception try to complete asyncCtx: {}", e
.getClass().getName());
// FIXME: We ignore this exception because is thrown when
// asyncContext in yet in MUST_COMPLETE STATE.
}
}
}
}
/**
* Internal implementation for sending JSON (called from sendJsonAnswer and
* sendJsonError methods).
*
* @param asyncCtx
* Asynchronous context
* @param message
* JSON message (as a Java class)
* @throws IOException
* Exception while parsing operating with asynchronous context
*/
private void internalSendJsonAnswer(AsyncContext asyncCtx,
JsonRpcResponse message) {
try {
if (asyncCtx == null) {
throw new KurentoMediaFrameworkException("Null asyncCtx found",
20017);
}
synchronized (asyncCtx) {
if (!asyncCtx.getRequest().isAsyncStarted()) {
throw new KurentoMediaFrameworkException(
"Cannot send message in completed asyncCtx", 1); // TODO
}
HttpServletResponse response = (HttpServletResponse) asyncCtx
.getResponse();
response.setContentType("application/json");
OutputStreamWriter osw = new OutputStreamWriter(
response.getOutputStream(), UTF8);
osw.write(gson.toJson(message));
osw.flush();
log.info("Sent JsonRpc answer ...\n" + message);
asyncCtx.complete();
}
} catch (IOException ioe) {
throw new KurentoMediaFrameworkException(ioe.getMessage(), ioe,
20018);
}
}
/**
* Parses Java class to JSON.
*
* @param object
* Generic objetc to be parsed
* @return JSON serialization
*/
public String toString(Object object) {
return gson.toJson(object);
}
/**
* Reads inputStream (from request) and detects incoming JSON encoding.
*
* @param inputStream
* Input Stream from request
* @return String identifier for detected JSON (UTF8, UTF16LE, ...)
* @throws IOException
* Exception while parsing JSON
*/
private String detectJsonEncoding(InputStream inputStream)
throws IOException {
inputStream.mark(4);
int mask = 0;
for (int count = 0; count < 4; count++) {
int r = inputStream.read();
if (r == -1) {
break;
}
mask = mask << 1;
mask |= (r == 0) ? 0 : 1;
}
inputStream.reset();
return match(mask);
}
/**
* Match recovered mask to String identifier (UTF8, UTF16LE, ...).
*
* @param mask
* Mask from detectJsonEncoding method
* @return String identifier for the detected JSON encoding
*/
private String match(int mask) {
switch (mask) {
case 1:
return UTF32BE;
case 5:
return UTF16BE;
case 8:
return UTF32LE;
case 10:
return UTF16LE;
case 15:
return UTF8;
default:
return null;
}
}
}