/*******************************************************************************
* Copyright 2013 butor.com
*
* 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.
******************************************************************************/
package org.butor.web.servlet;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.butor.json.JsonHelper;
import org.butor.json.JsonRequest;
import org.butor.json.service.Context;
import org.butor.json.service.ResponseHandler;
import org.butor.utils.ApplicationException;
import org.butor.utils.CommonMessageID;
import org.butor.utils.Message;
import org.butor.utils.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultAjaxComponent implements AjaxComponent {
protected Logger _logger = LoggerFactory.getLogger(getClass());
private JsonHelper _jsh = new JsonHelper();
private static final int MAX_ROWS_PER_NOTIF = 40;
private ConcurrentMap<String, Method> _servicesMap =
new ConcurrentHashMap<String, Method>();
private Object targetCmp = null;
public DefaultAjaxComponent(Object targetCmp) {
this.targetCmp = targetCmp;
Method[] methods = this.targetCmp.getClass().getDeclaredMethods();
if (methods.length == 0) {
return;
}
for (Method m : methods) {
Class<?> rt = m.getReturnType();
Class<?>[] pts = m.getParameterTypes();
if (pts.length > 0 && pts[0].isAssignableFrom(Context.class) && rt.equals(void.class)) {
String key = buildKey(m.getName(), pts.length);
if (_servicesMap.containsKey(key)) {
_logger.warn(String.format("method %s with %d args has been mapped already. Ignoring similar one!"));
continue;
}
_servicesMap.put(key, m);
}
}
}
private String buildKey(String methodName_, int nbArgs_) {
return String.format("%s.%d", methodName_, nbArgs_);
}
//TODO to be used
/*
private void startResp(OutputStream os, boolean streaming,
String reqId, byte[] funcDef) throws IOException {
if (streaming) {
os.write(funcDef);
os.flush();
} else {
os.write(String.format("{\"reqId\":\"%s\",\"data\":[", reqId).getBytes());
os.flush();
}
}
*/
private void handleException(final ResponseHandler<?> handler, String serviceName, Throwable e) {
if (e instanceof ApplicationException) {
ApplicationException appEx = (ApplicationException)e;
_logger.warn(String.format("Failed to invoke service e=%s, cmp=%s",
serviceName, targetCmp.getClass().getName()), appEx);
for (Message message : appEx.getMessages()) {
handler.addMessage(message);
}
} else {
_logger.error(String.format("Failed to invoke service=%s, cmp=%s",
serviceName, targetCmp.getClass().getName()), e);
handler.addMessage(CommonMessageID.SERVICE_FAILURE.getMessage());
}
}
@Override
public void process(final HttpServletRequest req_, final HttpServletResponse resp_)
throws ServletException, IOException {
InputStream is = null;
OutputStream os = null;
ResponseHandler<Object> streamer = null;
String serviceName = null;
final List<Message> fmsgs = new ArrayList<Message>();
final List<Object> frows = new ArrayList<Object>();
ScriptStreamer notif = null;
JsonRequest jr = null;
try {
String qs;
if (req_.getMethod().equalsIgnoreCase("post")) {
is = req_.getInputStream();
StringWriter sw = new StringWriter();
while (true) {
int bb = is.read();
if (bb == -1)
break;
sw.write(bb);
}
qs = sw.toString();
} else {
qs = req_.getQueryString();
}
Map<String,String> argsMap = new HashMap<String, String>();
String[] toks = qs.split("&");
for (String tok : toks) {
tok = URLDecoder.decode(tok, "utf-8");
String[] pair = tok.split("=");
argsMap.put(pair[0], pair.length > 1 ? pair[1] : null);
}
jr = new JsonRequest();
String userId = null;
if (req_.getUserPrincipal() != null)
userId = req_.getUserPrincipal().getName();
jr.setUserId(userId);
jr.setSessionId(req_.getSession().getId());
jr.setReqId(argsMap.get("reqId"));
jr.setService(argsMap.get("service"));
jr.setLang(argsMap.get("lang"));
String val = argsMap.get("streaming");
jr.setStreaming(StringUtil.isEmpty(val) ? false : val.equalsIgnoreCase("yes") || val.equalsIgnoreCase("true"));
jr.setServiceArgsJson(URLDecoder.decode(argsMap.get("args"), "UTF-8"));
List<?> params = _jsh.deserialize(jr.getServiceArgsJson(), List.class);
int nbArgs = params.size();
final JsonRequest fjr = jr;
serviceName = jr.getService();
if (StringUtil.isEmpty(serviceName))
throw new IllegalArgumentException("Missing serviceName");
String key = buildKey(serviceName, nbArgs +1);//+1 for context as arg[0]
Method m = _servicesMap.get(key);
if (m == null)
throw new UnsupportedOperationException(
String.format("Service=%s with %d args doesn't exists",
serviceName, nbArgs +1));
if (!fjr.isStreaming()) {
resp_.setContentType("application/json");
} else {
resp_.setContentType("text/html");
}
os = resp_.getOutputStream();
final OutputStream fos = os;
notif = fjr.isStreaming() ? new ScriptStreamer() : null;
final ScriptStreamer fnotif = notif;
final String reqId = jr.getReqId();
streamer = new ResponseHandler<Object>() {
int rowsPerNotif = 2;
long rowCount = 0;
boolean startedResp = false;
@Override
public void end() {
if (!startedResp)
try {
//startResp(os, fjr.isStreaming(), reqId, notif.funcDef());
startedResp = true;
if (fjr.isStreaming()) {
fos.write(fnotif.funcDef());
fos.flush();
} else {
fos.write(String.format("{\"reqId\":\"%s\",\"data\":[", reqId).getBytes());
fos.flush();
}
} catch (IOException e1) {
_logger.warn("Failed to write beginnig of response", e1);
}
}
@Override
public boolean addRow(Object row_) {
if (row_ == null)
return false;
if (!startedResp)
try {
//startResp(os, fjr.isStreaming(), reqId, notif.funcDef());
startedResp = true;
if (fjr.isStreaming()) {
fos.write(fnotif.funcDef());
fos.flush();
} else {
fos.write(String.format("{\"reqId\":\"%s\",\"data\":[", reqId).getBytes());
fos.flush();
}
} catch (IOException e1) {
_logger.warn("Failed to addRow", e1);
return false;
}
rowCount++;
try {
if (fjr.isStreaming()) {
// send small size notif first, then grows slowly
// to reach a max. This way, results will hit the browser
// very soon.
if (rowsPerNotif < MAX_ROWS_PER_NOTIF)
rowsPerNotif+=2;
frows.add(row_);
if (frows.size() >= rowsPerNotif) {
String json = _jsh.serialize(frows);
frows.clear();
fos.write(fnotif.buildNotif(fjr.getReqId(), "row", json, false));
}
} else {
if (rowCount>1)
fos.write(',');
String json = _jsh.serialize(row_);
fos.write(json.getBytes());
}
fos.flush();
return true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
public boolean addMessage(Message message_) {
if (message_ == null)
return false;
if (!startedResp)
try {
//startResp(os, fjr.isStreaming(), reqId, notif.funcDef());
startedResp = true;
if (fjr.isStreaming()) {
fos.write(fnotif.funcDef());
fos.flush();
} else {
fos.write(String.format("{\"reqId\":\"%s\",\"data\":[", reqId).getBytes());
fos.flush();
}
} catch (IOException e1) {
_logger.warn("Failed to addRow", e1);
return false;
}
fmsgs.add(message_);
try {
if (fjr.isStreaming()) {
if (frows.size() > 0) {
String json = _jsh.serialize(frows);
frows.clear();
fos.write(fnotif.buildNotif(fjr.getReqId(), "row", json, false));
}
String json = _jsh.serialize(fmsgs);
fmsgs.clear();
fos.write(fnotif.buildNotif(fjr.getReqId(), "msg", json, false));
fos.flush();
}
return true;
} catch (IOException e) {
_logger.warn("Failed to write row or message", e);
}
return false;
}
};
final ResponseHandler<Object> fStreamer = streamer;
AjaxContext ctx = new AjaxContext() {
@Override
public ResponseHandler<Object> getResponseHandler() {
return fStreamer;
}
@Override
public HttpServletResponse getHttpServletResponse() {
return resp_;
}
@Override
public HttpServletRequest getHttpServletRequest() {
return req_;
}
@Override
public JsonRequest getRequest() {
return fjr;
}
};
Class<?>[] pts = m.getParameterTypes();
Object[] args = new Object[pts.length];
args[0] = ctx;
for (int ii=1; ii<pts.length; ii++) {
Object par = params.get(ii-1);
String so = _jsh.serialize(par);
args[ii] = _jsh.deserialize(so, pts[ii]);
}
m.invoke(this.targetCmp, args);
} catch (InvocationTargetException e) {
if (streamer != null)
handleException(streamer, serviceName, e.getTargetException());
} catch (Throwable e) {
if (streamer != null)
handleException(streamer, serviceName, e);
} finally {
if (streamer != null) {
try {
streamer.end();
if (jr.isStreaming()) {
if (frows.size() > 0) {
String json = _jsh.serialize(frows);
frows.clear();
os.write(notif.buildNotif(jr.getReqId(), "row", json, false));
}
os.write(notif.buildNotif(jr.getReqId(), "row", null, true));
} else {
os.write("],\"messages\":".getBytes());
os.write(_jsh.serialize(fmsgs).getBytes());
os.write('}');
}
os.flush();
} catch (Exception e) {
_logger.warn("Oh shit! Failed in finally.", e);
//nevermind
}
}
}
}
}