org.simplity.http.HttpAgent Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2015 EXILANT Technologies Private Limited (www.exilant.com)
* Copyright (c) 2016 simplity.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.simplity.http;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Writer;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.simplity.json.JSONWriter;
import org.simplity.kernel.Application;
import org.simplity.kernel.ApplicationError;
import org.simplity.kernel.ClientCacheManager;
import org.simplity.kernel.FormattedMessage;
import org.simplity.kernel.MessageType;
import org.simplity.kernel.ServiceLogger;
import org.simplity.kernel.Tracer;
import org.simplity.kernel.file.FileManager;
import org.simplity.kernel.util.CircularLifo;
import org.simplity.kernel.util.JsonUtil;
import org.simplity.kernel.value.Value;
import org.simplity.service.ServiceAgent;
import org.simplity.service.ServiceData;
import org.simplity.service.ServiceProtocol;
/**
* servlet that is the agent to expose all services on http. receives requests
* for service and responds back with response given by the service. As you can
* see, this class can be easily converted into a servlet and configured to be
* the target URL for client requests. However, there would be several
* infrastructure related issues to be addressed in this layer. Hence we
* recommend that you take care of all that for your project, and call this
* class, rather than we making provisions for all of that
*
* @author simplity.org
*
*/
public class HttpAgent {
/*
* session parameter name with which user token is saved. This token is the
* name under which our global parameters are saved. This indirection is
* used to keep flexibility allow a client to have multiple active sessions.
* sessions, we make this a set of tokens.
*/
private static final String GET = "GET";
/**
* message to be sent to client if there is any internal error
*/
public static final FormattedMessage INTERNAL_ERROR = new FormattedMessage("internalError", MessageType.ERROR,
"We are sorry. There was an internal error on server. Support team has been notified.");
/**
* message to be sent to client if this request requires a login and the
* user has not logged in
*/
public static final FormattedMessage NO_LOGIN = new FormattedMessage("notLoggedIn", MessageType.ERROR,
"You are not logged into the server, or server may have logged-you out as a safety measure after a period of no activity.");
/**
* message to be sent to client if data text was not in order.
*/
public static final FormattedMessage DATA_ERROR = new FormattedMessage("invalidDataFormat", MessageType.ERROR,
"Data text sent from client is not formatted properly. Unable to extract data from the text.");
/**
* message to be used when client has not specified a service
*/
public static final FormattedMessage NO_SERVICE = new FormattedMessage("noService", MessageType.ERROR,
"No service name was specified for this request.");
/**
* message to be used when client's request for login has failed
*/
public static final FormattedMessage LOGIN_FAILED = new FormattedMessage("loginFailed", MessageType.ERROR,
"Invalid Credentials. Login failed.");
/**
* no token from client
*/
public static final FormattedMessage NO_TOKEN = new FormattedMessage("noToken", MessageType.ERROR,
"A valid token for the bckground job is required to get its response.");
/**
* response is not yet available for this token
*/
public static final FormattedMessage NO_RESPONSE = new FormattedMessage("noResponse", MessageType.INFO,
"No response yet from the background job.");
private static final String STILL_PENDING_PREFIX = "{\"" + ServiceProtocol.HEADER_FILE_TOKEN + "\":\"";
private static final String STILL_PENDING_SUFFIX = "\"}";
/**
* parameter name with which userId is to be saved in session. made this
* public filters to observe this convention. Value in session with this
* name implies that the user has logged-in. HttpAgent does not serve unless
* there is a userId associated with the session
*/
public static final String SESSION_NAME_FOR_USER_ID = "_userIdInSession";
/**
* name with session fields for the logged-in users are saved in a Map
*/
public static final String SESSION_NAME_FOR_MAP = "_userSessionMap";
static final String CACHED_TRACES = "CACHED_TRACES";
/**
* set at set-up time in case we are in development mode, and we use a
* default dummyLogin id
*/
private static Value autoLoginUserId;
/**
* cache service responses
*/
private static ClientCacheManager httpCacheManager;
/**
* accumulated traces to be streamed to client when requested.
*/
private static boolean tracesToBeCached;
/**
* serve this service. Main entrance to the server from an http client.
*
* @param req
* http request
* @param resp
* http response
* @throws ServletException
* Servlet exception
* @throws IOException
* IO exception
*
*/
public static void serve(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String fileToken = req.getHeader(ServiceProtocol.HEADER_FILE_TOKEN);
if (fileToken != null) {
Tracer.trace("Checking for pending service with token " + fileToken);
getPendingResponse(req, resp, fileToken);
return;
}
HttpSession session = req.getSession(true);
boolean isGet = GET.equals(req.getMethod());
/*
* get the service name
*/
String serviceName = getServiceName(req);
long startedAt = new Date().getTime();
long elapsed = 0;
Value userId = null;
ServiceData outData = null;
Tracer.startAccumulation();
Tracer.trace("Request received for service " + serviceName);
FormattedMessage message = null;
/*
* let us earnestly try to serve now :-) this do{} is not a loop, but a
* block that helps in handling errors in an elegant way
*/
ServiceData inData = null;
do {
try {
if (serviceName == null) {
message = NO_SERVICE;
break;
}
inData = createServiceData(session, false);
if (inData == null) {
message = NO_LOGIN;
break;
}
userId = inData.getUserId();
String payLoad = null;
if (isGet) {
payLoad = queryToJson(req);
} else {
/*
* try-catch specifically for any possible I/O errors
*/
try {
payLoad = readInput(req);
} catch (Exception e) {
message = DATA_ERROR;
break;
}
}
/*
* we are forced to check payload for the time being for some
* safety
*/
if (payLoad == null || payLoad.isEmpty() || payLoad.equals("undefined") || payLoad.equals("null")) {
payLoad = "{}";
}
inData.setPayLoad(payLoad);
inData.setServiceName(serviceName);
if (httpCacheManager != null) {
outData = httpCacheManager.respond(inData, session);
if (outData != null) {
break;
}
}
outData = ServiceAgent.getAgent().executeService(inData);
/*
* by our convention, server may send data in outData to be set
* to session
*/
if (outData.hasErrors() == false) {
setSessionData(session, outData);
if (httpCacheManager != null) {
httpCacheManager.cache(inData, outData, session);
}
}
} catch (ApplicationError e) {
Application.reportApplicationError(inData, e);
message = INTERNAL_ERROR;
} catch (Exception e) {
Application.reportApplicationError(inData, new ApplicationError(e, "Error while processing request"));
message = INTERNAL_ERROR;
}
} while (false);
elapsed = new Date().getTime() - startedAt;
resp.setHeader(ServiceProtocol.SERVICE_EXECUTION_TIME, elapsed + "");
resp.setContentType("text/json");
String response = null;
FormattedMessage[] messages = null;
if (outData == null) {
if (message == null) {
message = INTERNAL_ERROR;
}
/*
* we got error
*/
Tracer.trace("Error on web tier : " + message.text);
messages = new FormattedMessage[1];
messages[0] = message;
response = getResponseForError(messages);
} else if (outData.hasErrors()) {
Tracer.trace("Service returned with errors");
response = getResponseForError(outData.getMessages());
} else {
/*
* all OK
*/
response = outData.getPayLoad();
Tracer.trace("Service succeeded and has " + (response == null ? "no " : (response.length()) + " chars ")
+ " payload");
}
writeResponse(resp, response);
String trace = Tracer.stopAccumulation();
if (outData != null) {
String serverTrace = outData.getTrace();
if (serverTrace != null) {
trace = "---- Web Tier Trace ---\n" + trace + "\n------ App Tier Trace ----\n" + serverTrace;
}
}
if (tracesToBeCached) {
cacheTraces(session, trace);
}
String uid = userId == null ? "unknown" : userId.toString();
ServiceLogger.pushTraceToLog(serviceName, uid, (int) elapsed, trace);
}
private static String getServiceName(HttpServletRequest req) {
String serviceName = req.getHeader(ServiceProtocol.SERVICE_NAME);
if (serviceName == null) {
serviceName = req.getParameter(ServiceProtocol.SERVICE_NAME);
}
if (serviceName == null) {
serviceName = (String) req.getAttribute(ServiceProtocol.SERVICE_NAME);
}
return serviceName;
}
/**
* get the JSON to be sent back to client in case of errors
*
* @param messages
* @return JSON string for the supplied errors
*/
public static String getResponseForError(FormattedMessage[] messages) {
JSONWriter writer = new JSONWriter();
writer.object();
writer.key(ServiceProtocol.REQUEST_STATUS);
writer.value(ServiceProtocol.STATUS_ERROR);
writer.key(ServiceProtocol.MESSAGES);
JsonUtil.addObject(writer, messages);
writer.endObject();
return writer.toString();
}
/**
* This method can be used in two scenarios.
*
* 1. SSO or some other login mechanism is used that is outside of this
* application. In this case, the registered loginService() inside this
* application does not do any authentication, but it will extract user-data
* for this session. It may also keep track of the SSO token etc..
*
* 2. login is designed as service within this application. In this case,
* securityToekn is the password supplied by the user. registered
* loginService authenticates this password before extracting use data.
*
* In case you use SSO, or any other common utility outside this
* application, we assume that you have authenticated the user, and we will
* be calling the spec
*
* @param loginId
* Login ID
* @param securityToken
* Security Token, optional.
* @param session
* for accessing session
* @return token for this session that needs to be supplied for any service
* under this sessions. null implies that we could not login.
*/
public static String login(String loginId, String securityToken, HttpSession session) {
/*
* we log out from the existing session before attempting to login. This
* is a security requirement. That is, user cannot retain the current
* login while trying to login again
*/
try{
logout(session, false);
}catch(Exception ignore){
//
}
/*
* ask serviceAgent to login.
*/
ServiceData inData = new ServiceData();
inData.put(ServiceProtocol.USER_ID, Value.newTextValue(loginId));
if (securityToken != null) {
inData.put(ServiceProtocol.USER_TOKEN, Value.newTextValue(securityToken));
}
inData.setPayLoad("{}");
ServiceData outData = ServiceAgent.getAgent().login(inData);
if (outData == null || outData.hasErrors()) {
return null;
}
Value userId = outData.getUserId();
if (userId == null) {
/*
* possible that loginService is a custom one. Let us try to fish in
* the Map
*/
Object uid = outData.get(ServiceProtocol.USER_ID);
if (uid == null) {
Tracer.trace(
"Server came back with no userId and hence HttpAgent assumes that the login did not succeed");
return null;
}
if (uid instanceof Value) {
userId = (Value) uid;
} else {
userId = Value.parseObject(uid);
}
}
/*
* create and save new session data
*/
newSession(session, userId);
setSessionData(session, outData);
Tracer.trace("Login succeeded for loginId " + loginId);
String result = outData.getPayLoad();
if (result == null || result.length() == 0) {
result = "{}";
}
return result;
}
/**
* logout after executing desired service
*
* @param session
* http session
* @param timedOut
* is this triggered on session time-out? false means a specific
* logout request from user
*
*/
public static void logout(HttpSession session, boolean timedOut) {
if (session == null) {
return;
}
ServiceData inData = createServiceData(session, true);
if (inData == null) {
Tracer.trace("No active session found, and hence logout not called");
return;
}
if (timedOut) {
inData.put(ServiceProtocol.TIMED_OUT, Value.VALUE_TRUE);
}
ServiceAgent.getAgent().logout(inData);
removeSession(session);
}
/**
* read input stream into a string
*
* @param req
* @throws IOException
*/
private static String readInput(HttpServletRequest req) throws IOException {
BufferedReader reader = null;
StringBuilder sbf = new StringBuilder();
try {
reader = req.getReader();
int ch;
while ((ch = reader.read()) > -1) {
sbf.append((char) ch);
}
reader.close();
return sbf.toString();
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
//
}
}
}
}
private static String queryToJson(HttpServletRequest req) {
JSONWriter writer = new JSONWriter();
writer.object();
/*
* we have to suppress this warning as it is an external call that we
* have no control over
*/
@SuppressWarnings("unchecked")
Map fields = req.getParameterMap();
if (fields != null) {
for (Map.Entry entry : fields.entrySet()) {
writer.key(entry.getKey()).value(entry.getValue()[0]);
}
}
writer.endObject();
return writer.toString();
}
/**
* @param autoUserId
* in case login is disabled and a default loginId is to be used
* for all services
* @param cacher
* http cache manager
* @param cacheTraces
* if true, traces are also saved into a circular buffer that can
* be delivered to the client
*/
public static void setUp(Value autoUserId, ClientCacheManager cacher, boolean cacheTraces) {
autoLoginUserId = autoUserId;
httpCacheManager = cacher;
tracesToBeCached = cacheTraces;
}
/**
*
* @param session
* @param token
* @param inData
* @return
*/
private static ServiceData createServiceData(HttpSession session, boolean forLogout) {
Value userId = (Value) session.getAttribute(SESSION_NAME_FOR_USER_ID);
if (userId == null) {
if(forLogout){
return null;
}
Tracer.trace("Request by non-logged-in session detected.");
if (autoLoginUserId == null) {
Tracer.trace("Login is required.");
return null;
}
login(autoLoginUserId.toString(), null, session);
userId = (Value) session.getAttribute(SESSION_NAME_FOR_USER_ID);
if (userId == null) {
Tracer.trace("autoLoginUserId is set to " + autoLoginUserId
+ " but loginService is probably not accepting this id without credentials. Check your lginServiceName=\"\" in application.xml and ensure that your service clears this dummy userId with no credentials");
return null;
}
Tracer.trace("User " + userId + " auto logged-in");
}
@SuppressWarnings("unchecked")
Map sessionData = (Map) session.getAttribute(SESSION_NAME_FOR_MAP);
if (sessionData == null) {
throw new ApplicationError("Unexpected situation. UserId is located in session, but not map");
}
ServiceData data = new ServiceData(userId, null);
for (Map.Entry entry : sessionData.entrySet()) {
data.put(entry.getKey(), entry.getValue());
}
return data;
}
/**
*
* @param session
* @param token
* - future use
*/
private static void removeSession(HttpSession session) {
Object obj = session.getAttribute(SESSION_NAME_FOR_USER_ID);
if (obj == null) {
Tracer.trace("Remove session : No session to remove");
return;
}
Tracer.trace("Session removed for " + obj);
session.removeAttribute(SESSION_NAME_FOR_USER_ID);
session.removeAttribute(SESSION_NAME_FOR_MAP);
}
/**
*
* @param session
* @param data
*/
private static void setSessionData(HttpSession session, ServiceData data) {
@SuppressWarnings("unchecked")
Map sessionData = (Map) session.getAttribute(SESSION_NAME_FOR_MAP);
if (sessionData == null) {
Tracer.trace("Unexpected situation. setSession invoked with no active session. Action ignored");
} else {
for (String key : data.getFieldNames()) {
sessionData.put(key, data.get(key));
}
}
}
/**
* create a new session for this user. To be used by filter/SSO etc.. if
* there is no specific login process for Simplity application. Note; If a
* loginService is to be executed, then caller should use login() instead of
* this method
*
* @param session
* @param userId
* @return map of global fields that is maintained by Simplity. Any
* parameter in this map is made available to every service request
*/
public static Map newSession(HttpSession session, Value userId) {
Map sessionData = new HashMap();
session.setAttribute(SESSION_NAME_FOR_USER_ID, userId);
session.setAttribute(SESSION_NAME_FOR_MAP, sessionData);
if (tracesToBeCached) {
session.setAttribute(CACHED_TRACES, new CircularLifo());
}
Tracer.trace("New session data created for " + userId);
return sessionData;
}
/**
* get the userId that has logged into this session.
*
* @param session
* can not be null
* @return userId, or null if no login so far in this session
*/
public static Value getLoggedInUser(HttpSession session) {
return (Value) session.getAttribute(SESSION_NAME_FOR_USER_ID);
}
/**
* invalidate any cached response for this service
*
* @param serviceName
* @param session
*/
public static void invalidateCache(String serviceName, HttpSession session) {
if (httpCacheManager != null) {
httpCacheManager.invalidate(serviceName, session);
}
}
/**
* push trace to the buffer in session
*
* @param session
* @param trace
*/
private static void cacheTraces(HttpSession session, String trace) {
Object obj = session.getAttribute(HttpAgent.CACHED_TRACES);
if (obj == null) {
Tracer.trace("Unexpected absence of trace buffer in session. Client will not get traces.");
return;
}
@SuppressWarnings("unchecked")
CircularLifo lifo = ((CircularLifo) obj);
lifo.put(trace);
}
/**
* serve this service. Main entrance to the server from an http client.
*
* @param req
* http request
* @param resp
* http response
* @param fileToken
* this is the token that was returned by an earlier call to
* server. Whenever a service request is for a service indicating
* that the service
* @throws ServletException
* Servlet exception
* @throws IOException
* IO exception
*
*/
public static void getPendingResponse(HttpServletRequest req, HttpServletResponse resp, String fileToken)
throws ServletException, IOException {
FormattedMessage message = null;
ServiceData outData = null;
do {
File file = FileManager.getTempFile(fileToken);
if (file == null || file.length() == 0) {
/*
* trick-design. To be re-factored later. We break with no
* message and no Data to imply a pending status
*/
// message = NO_RESPONSE;
break;
}
Object obj = null;
try {
ObjectInputStream stream = new ObjectInputStream(new FileInputStream(file));
obj = stream.readObject();
stream.close();
} catch (Exception e) {
Application.reportApplicationError(null, new ApplicationError(e, "Error while streaming response"));
message = INTERNAL_ERROR;
break;
}
if (obj instanceof ServiceData == false) {
String text = "Temp file is expected to contain an object instance of ServiceData but we found "
+ obj.getClass().getName();
Application.reportApplicationError(null, new ApplicationError(text));
message = INTERNAL_ERROR;
break;
}
outData = (ServiceData) obj;
} while (false);
String response = null;
if (message != null) {
FormattedMessage[] messages = { message };
response = getResponseForError(messages);
} else if (outData != null) {
if (outData.hasErrors()) {
response = getResponseForError(outData.getMessages());
} else {
response = outData.getPayLoad();
}
} else {
/*
* trick design. no data, no message implies no response available
* yet for this token. We have no way to check whether this is a
* valid token. Feature has to be added.
*
*/
response = STILL_PENDING_PREFIX + fileToken + STILL_PENDING_SUFFIX;
}
writeResponse(resp, response);
if (tracesToBeCached && outData != null) {
cacheTraces(req.getSession(true), outData.getTrace());
}
}
/**
* respond back with the pay load
*
* @param resp
* @param payLoad
* @throws IOException
*/
private static void writeResponse(HttpServletResponse resp, String payLoad) throws IOException {
resp.setContentType("text/json");
resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
resp.setDateHeader("Expires", 0);
Writer writer = resp.getWriter();
if (payLoad == null || payLoad.isEmpty()) {
writer.write("{}");
} else {
writer.write(payLoad);
}
writer.close();
}
/**
* TODO: temp method. Must be refactored
*/
public static void serve(HttpServletRequest req, HttpServletResponse resp, String serviceName) throws ServletException, IOException {
String fileToken = req.getHeader(ServiceProtocol.HEADER_FILE_TOKEN);
if (fileToken != null) {
Tracer.trace("Checking for pending service with token " + fileToken);
getPendingResponse(req, resp, fileToken);
return;
}
HttpSession session = req.getSession(true);
boolean isGet = GET.equals(req.getMethod());
/*
* get the service name
*/
//String serviceName = getServiceName(req);
long startedAt = new Date().getTime();
long elapsed = 0;
Value userId = null;
ServiceData outData = null;
Tracer.startAccumulation();
Tracer.trace("Request received for service " + serviceName);
FormattedMessage message = null;
/*
* let us earnestly try to serve now :-) this do{} is not a loop, but a
* block that helps in handling errors in an elegant way
*/
ServiceData inData = null;
do {
try {
if (serviceName == null) {
message = NO_SERVICE;
break;
}
inData = createServiceData(session, false);
if (inData == null) {
message = NO_LOGIN;
break;
}
userId = inData.getUserId();
String payLoad = null;
if (isGet) {
payLoad = queryToJson(req);
} else {
/*
* try-catch specifically for any possible I/O errors
*/
try {
payLoad = readInput(req);
} catch (Exception e) {
message = DATA_ERROR;
break;
}
}
/*
* we are forced to check payload for the time being for some
* safety
*/
if (payLoad == null || payLoad.isEmpty() || payLoad.equals("undefined") || payLoad.equals("null")) {
payLoad = "{}";
}
inData.setPayLoad(payLoad);
inData.setServiceName(serviceName);
if (httpCacheManager != null) {
outData = httpCacheManager.respond(inData, session);
if (outData != null) {
break;
}
}
outData = ServiceAgent.getAgent().executeService(inData);
/*
* by our convention, server may send data in outData to be set
* to session
*/
if (outData.hasErrors() == false) {
setSessionData(session, outData);
if (httpCacheManager != null) {
httpCacheManager.cache(inData, outData, session);
}
}
} catch (ApplicationError e) {
Application.reportApplicationError(inData, e);
message = INTERNAL_ERROR;
} catch (Exception e) {
Application.reportApplicationError(inData, new ApplicationError(e, "Error while processing request"));
message = INTERNAL_ERROR;
}
} while (false);
elapsed = new Date().getTime() - startedAt;
resp.setHeader(ServiceProtocol.SERVICE_EXECUTION_TIME, elapsed + "");
resp.setContentType("text/json");
String response = null;
FormattedMessage[] messages = null;
if (outData == null) {
if (message == null) {
message = INTERNAL_ERROR;
}
/*
* we got error
*/
Tracer.trace("Error on web tier : " + message.text);
messages = new FormattedMessage[1];
messages[0] = message;
response = getResponseForError(messages);
} else if (outData.hasErrors()) {
Tracer.trace("Service returned with errors");
response = getResponseForError(outData.getMessages());
} else {
/*
* all OK
*/
response = outData.getPayLoad();
Tracer.trace("Service succeeded and has " + (response == null ? "no " : (response.length()) + " chars ")
+ " payload");
}
writeResponse(resp, response);
String trace = Tracer.stopAccumulation();
if (outData != null) {
String serverTrace = outData.getTrace();
if (serverTrace != null) {
trace = "---- Web Tier Trace ---\n" + trace + "\n------ App Tier Trace ----\n" + serverTrace;
}
}
if (tracesToBeCached) {
cacheTraces(session, trace);
}
String uid = userId == null ? "unknown" : userId.toString();
ServiceLogger.pushTraceToLog(serviceName, uid, (int) elapsed, trace);
}
}