at.spardat.xma.rpc.RPCServletServer Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2003, 2007 s IT Solutions AT Spardat GmbH .
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* s IT Solutions AT Spardat GmbH - initial API and implementation
*******************************************************************************/
// @(#) $Id: RPCServletServer.java 7307 2011-03-07 09:35:22Z blichtl $
package at.spardat.xma.rpc;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collection;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import at.spardat.enterprise.exc.AppException;
import at.spardat.enterprise.exc.BaseException;
import at.spardat.enterprise.exc.SysException;
import at.spardat.properties.XProperties;
import at.spardat.xma.RuntimeDefaults;
import at.spardat.xma.component.ComponentServer;
import at.spardat.xma.event.global.GlobalEventManager;
import at.spardat.xma.exception.Codes;
import at.spardat.xma.monitoring.TimeingEvent;
import at.spardat.xma.monitoring.client.ClientTimingEvent;
import at.spardat.xma.page.PageServer;
import at.spardat.xma.security.XMAContext;
import at.spardat.xma.serializer.Deserializer;
import at.spardat.xma.serializer.Serializer;
import at.spardat.xma.serializer.SerializerFactory;
import at.spardat.xma.serializer.SerializerFactoryServer;
import at.spardat.xma.session.XMASessionServer;
import at.spardat.xma.util.ByteArray;
/**
* This servlet is the server side endpoint for XMA remote calls in components.
* The corresponding client class that is the communication partner is
* {@link RemoteCall}.
*
*
* What this servlet roughly does, is:
*
* - Deserialize a RemoteCallData object from the
* ServletInputStream. This is the data transmitted from the client.
*
- Extract the model changes from RemoteCallData and integrate
* the changes in the servers component buddy.
*
- Execute the server event method, where the control leaves the
* XMA-runtime and is handled over to the XMA program.
*
- Pack the changes the server side event method did in a
* {@link RemoteReplyData}object and write it to the servlet output stream.
*
*
* @author YSD, 27.05.2003 21:33:55
*/
public class RPCServletServer extends HttpServlet {
/**
* ServletConfig which is given to me in method init.
*/
private ServletConfig servletConfig_;
/**
* The post method that exactly does the things described above.
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String mp = getMeasurementPraefix(request);
TimeingEvent ev = new TimeingEvent(mp + "rpcServerEnter"); // measurement
// for imcmonitor
try {
doPost0(request, response, mp);
ev.success();
} finally {
// remove the XMASession bound to this thread
XMASessionServer.setSessionAsThreadLocal(null);
ev.failure();
}
}
/**
* @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig sc) throws ServletException {
super.init(sc);
servletConfig_ = sc;
}
/**
* The post method that exactly does the things described above.
*/
protected void doPost0(HttpServletRequest request, HttpServletResponse response,
String measurementPraefix) throws ServletException, IOException {
/**
* construct RemoteReply object
*/
RemoteReply reply = new RemoteReply();
RemoteReplyData replyData = reply.getReplyData();
RemoteCall call = null;
RemoteCallData callData = null;
try {
/**
* read up stream and reconstruct RemoteCall object
*/
call = readRemoteCallFromServletInput(request);
callData = call.getCallData();
/**
* If the call included a last-client-measurement, handle it over to
* ImcMonitor
*/
RemoteCallData.CallMeasurement lastCM = callData.getLastMeasurement();
if (lastCM != null) {
if (lastCM.success_) {
TimeingEvent.success(
measurementPraefix + "rpcClient",
lastCM.durationMSecs_);
} else {
TimeingEvent.failure(
measurementPraefix + "rpcClient",
lastCM.durationMSecs_);
}
reportClientTimingEvents(lastCM.getClientTimingEvents());
}
/**
* get session
*/
XMASessionServer xmaSession = getSession(request);
/**
* permission check
*/
if (!checkPermission(xmaSession, callData)) {
throw new SysException(Codes.getText(Codes.PERMISSION_DENIED)).setCode(
Codes.PERMISSION_DENIED).setShowToEndUser(true);
}
/**
* from now on, only one thread may execute on the session
*/
synchronized (xmaSession) {
/**
* the session is bound to the thread
*/
XMASessionServer.setSessionAsThreadLocal(xmaSession);
/**
* clean up dead components
*/
for (int i = 0; i < callData.deadComponents_.length; i++) {
xmaSession.removeComponent(callData.deadComponents_[i]);
}
/**
* find the right component
*/
ComponentServer dispatchComponent = xmaSession
.getComponent(callData.idComponent_);
if (dispatchComponent == null) {
// there is no component for the provided id. There are two
// reasons for this: Either, the component is stateless.
// Then, it
// is destroyed after every request, so naturally, there is
// none.
// The other possibility is that the component is stateful
// but has
// been newly created at the client.
String clazzNameOfComponent = xmaSession
.getComponentClassName(callData.namComponent_);
try {
Class clazzOfComponent = Class.forName(clazzNameOfComponent);
// the component class requires a constructor with the
// arguments (XMASessionServer, short)
// where the second parameter is the component id
Constructor constructor = clazzOfComponent
.getConstructor(new Class[] { XMASessionServer.class,
short.class });
dispatchComponent = (ComponentServer) constructor
.newInstance(new Object[] { xmaSession,
new Short(callData.idComponent_) });
} catch (Exception ex) {
throw new SysException(ex, "cannot create component of class "
+ clazzNameOfComponent + ", short name: "
+ callData.namComponent_)
.setCode(Codes.SERVER_COMPONENT_CREATE);
}
}
/**
* dispatch call to the component
*/
try {
dispatchRemoteCall(dispatchComponent, call, reply);
setGlobalEvents(reply);
} finally {
try {
// clean up after a server side event
dispatchComponent.cleanUpAfterServerEvent();
} catch (Exception ex) {
ex.printStackTrace(); // just log this problem
}
}
/**
* output some statistics if we are in development environments
*/
XMAContext context = xmaSession.getContext();
boolean doTrace = context.isLocal()
|| XMAContext.devel.equals(context.getEnvironment());
if (doTrace) {
outputPostRPCStatistics(
dispatchComponent,
xmaSession,
callData.eventName_);
}
}
} catch (Exception ex) {
String eventName = "unknown";
if (callData != null && callData.eventName_ != null) {
eventName = callData.eventName_;
}
BaseException toReturn = handleException(ex,eventName);
reply.setException(toReturn);
toReturn = reply.getException(); // may have changed
} catch (LinkageError ex) { // contains NoClassDefFoundError and similar
String eventName = "unknown";
if (callData != null && callData.eventName_ != null) {
eventName = callData.eventName_;
}
BaseException toReturn = handleException(ex,eventName);
reply.setException(toReturn);
toReturn = reply.getException(); // may have changed
}
/**
* Stream result back to the client
*/
SerializerFactory fac = new SerializerFactoryServer();
boolean isBinaryMode = fac.isModeBinary(null);
Serializer serializer = fac.createSerializer(null, 2048);
ByteArray serializerResult = null;
serializer.addHeader();
try {
replyData.externalize(serializer);
serializerResult = serializer.getResult();
} catch (Exception ex) {
throw new SysException(ex, "cannot produce RemoteReturn data stream")
.setCode(Codes.EXT_STREAM_SERVER);
}
/**
* Compress the result if length exceeds some limit
*/
if (isBinaryMode && doCompress(serializerResult.size())) {
serializerResult = serializerResult.getCompressed();
}
if (!isBinaryMode) {
serializerResult.setComputeHeaderLength(false);
}
// write to ServletOuputStream
response.setContentLength(serializerResult.size()); // set content
// length
ServletOutputStream servletOut = response.getOutputStream();
servletOut.write(serializerResult.getBuffer(), 0, serializerResult.size());
}
/**
* here goes the default error handling. Our strategy is to pack the
* exception in the reply object and stream it back to the client.
* Only if this does not work for some reason, we fall back to a
* ServletException
*/
private BaseException handleException(Throwable ex,String eventName) {
BaseException toReturn;
if (ex instanceof BaseException) {
toReturn = (BaseException) ex;
} else {
// the exception is no BaseException; we consider this as server
// runtime error.
toReturn = new SysException(ex, "xma server runtime error")
.setCode(Codes.SERVER_RUNTIME_ERROR);
}
/**
* If the exception is not an AppException, is must be logged here
* to show up in the server log of the application-server.
*/
if (!(toReturn instanceof AppException)) {
ServletContext servletCtx = servletConfig_.getServletContext();
if (servletCtx != null) {
//at session lost do not write any stack trace
if(toReturn.getCode() == Codes.SERVER_INVALID_SESSION){
servletCtx.log("Exception in xmarpc '" + eventName
+ "': " + toReturn.getClass().getName() + " ["
+ Codes.SERVER_INVALID_SESSION + "]: "
+ toReturn.getMessage());
}else{
servletCtx.log("Exception in xmarpc '" + eventName + "'", toReturn);
}
} else {
toReturn.printStackTrace();
}
}
// prepare the exception for migration an put it into the reply
// after this the exception can not be used on the server any more
toReturn.prepareMigration();
return toReturn;
}
/**
* reports the clientTimingEvents to the monitor
* @param clientTimingEvents
* @since version_number
* @author s3460
*/
private void reportClientTimingEvents(ClientTimingEvent[] clientTimingEvents) {
if(clientTimingEvents != null){
for (int i = 0; i < clientTimingEvents.length; i++) {
ClientTimingEvent event = clientTimingEvents[i];
if (event.isSuccess()) {
TimeingEvent.success(event.getVarName(), event.getValue());
} else {
TimeingEvent.failure(event.getVarName(), event.getValue());
}
}
}
}
/**
* Determines if responses to the client of length should be
* compressed. This is driven by the property
* 'at.spardat.xma.RpcCompressionThreshold'. A value of -1 indicates no
* compression. Otherwise, the value of the property indicates the minimum
* length of a stream where compression is activated at.
*/
public static boolean doCompress(int length) {
XProperties node = XProperties.getNodeOfPackage("xma.runtime");
int threshold = node.getInt(
"RpcCompressionThreshold",
RuntimeDefaults.RpcCompressionThreshold);
return length > threshold;
}
/**
* Reads servlet input stream and constructs a RemoteCall object that has
* been sent by the client.
*/
private RemoteCall readRemoteCallFromServletInput(HttpServletRequest request) {
RemoteCall rCall;
try {
ByteArray upstreamBytes = new ByteArray(1024);
upstreamBytes.setHeader(true);
InputStream upstream = request.getInputStream();
upstreamBytes.readFrom(upstream, 4); // read first 4 bytes which is
// the length of the stream
if (upstreamBytes.size() != 4) {
throw new SysException("not even read 4 bytes from ServletInputStream")
.setCode(Codes.READ_LENGTH_SERVER);
}
int length = upstreamBytes.getLengthInHeader();
if (length == -1) {
// length is not set in the header; we read until EOF
upstreamBytes.readFrom(upstream);
} else {
// read the remaining bytes
upstreamBytes.readFrom(upstream, length);
if (upstreamBytes.size() != length) {
throw new SysException(
""
+ upstreamBytes.size()
+ " read from ServletInputStream, but header indicated length of "
+ length).setCode(Codes.READ_SERVER);
}
}
// decompress if compressed
if (upstreamBytes.isCompressed()) {
upstreamBytes = upstreamBytes.getUncompressed();
}
// construct RemoteCall object
rCall = new RemoteCallServer(upstreamBytes);
} catch (Exception ex) {
if (ex instanceof BaseException) {
throw (BaseException) ex;
}
throw new SysException(ex,
"cannot reconstruct RemoteCall object from ServletInputStream")
.setCode(Codes.SERVER_READ_CALL_FROM_SERVLETINPUT);
}
return rCall;
}
/**
* Returns the xma session object or throws an SysException if there is no
* session.
*/
private XMASessionServer getSession(HttpServletRequest request) {
XMASessionServer xmaSession = XMASessionServer
.getXMASession(request.getSession());
if (xmaSession == null) {
throw new SysException(Codes.getText(Codes.SERVER_INVALID_SESSION)).setCode(
Codes.SERVER_INVALID_SESSION).setShowToEndUser(true);
}
return xmaSession;
}
/**
* used in reflection to get the event method
*/
private static Class[] eventMethodArgTypes = new Class[] { RemoteCall.class,
RemoteReply.class };
/**
* This method processes a remote call at the component level. Its task is
* to internalize the model changes from the client, call the programmers
* server side event method and externalize the model changes.
*
* @param component
* the component where to dispatch the call
* @param call
* the remote request object as transmitted from the client
* @param reply
* the remote reply object that is going to hold the reply data
*/
public void dispatchRemoteCall(ComponentServer component, RemoteCall call,
RemoteReply reply) {
RemoteCallData callData = call.getCallData();
RemoteReplyData replyData = reply.getReplyData();
/**
* check if the serial change number transmitted from the client matches
* with that in the server component
*/
if (!component.isStateless() && callData.serverChangeNumber_ != -1
&& !callData.fullSync_) {
if (callData.serverChangeNumber_ != component.getSCN()) {
throw new SysException(Codes.getText(Codes.OUT_OF_SYNC)).setCode(
Codes.OUT_OF_SYNC).setShowToEndUser(true);
}
}
/**
* internalize the model changes
*/
SerializerFactory fac = new SerializerFactoryServer();
try {
Deserializer deser = fac.createDeserializer(null, callData.pageDeltas_);
component.internalize(deser);
// increase server change number
component.incrementSCN();
replyData.serverChangeNumber_ = component.getSCN(); // and
// immediately
// save to
// replyData
component.commit();
} catch (Exception ex) {
component.rollback();
throw new SysException(ex, "cannot internalize deltas from client")
.setCode(Codes.SERVER_INTERNALIZE_DELTAS);
}
/**
* optionally get the target page
*/
PageServer targetPage = null;
if (callData.issuerIsPage_) {
targetPage = (PageServer) component.getPageModel(callData.idPage_);
if (targetPage == null) {
throw new SysException("target dispatch page " + callData.idPage_
+ " cannot be found").setCode(Codes.SERVER_INVALID_PAGE);
}
}
/**
* get Method object via reflection
*/
Method targetMethod = null;
Class targetClass = (callData.issuerIsPage_ ? targetPage.getClass() : component
.getClass());
try {
targetMethod = targetClass
.getMethod(callData.eventName_, eventMethodArgTypes);
} catch (Exception ex) {
throw new SysException(ex, "target dispatch method " + callData.eventName_
+ "(RemoteCall,RemoteReply) not found in class "
+ targetClass.getName()).setCode(Codes.SERVER_METHOD_NOT_FOUND);
}
/**
* call the dispatching method in the component
*/
BaseException eventMethodException = null;
try {
/**
* copy the components properties from the model to the component;
* errors in setting the properties are treated like errors in
* executeRemoteCall
*/
component.model2props();
try {
/**
* delegate to component
*/
component.executeRemoteCall(call, reply, targetPage, targetMethod);
} finally {
/**
* regardless of how the rpc-method terminated, copy the the
* component properties back to the model
*/
component.props2model();
}
} catch (Exception ex) {
if (ex instanceof BaseException) {
eventMethodException = (BaseException) ex;
} else {
// wrap the exception into an SysException
eventMethodException = new SysException(ex,
"server event method terminated with an exception")
.setCode(Codes.SERVER_METHOD_EXECUTE);
}
}
/**
* At this point, the server event method terminated but may have thrown
* an exception if eventMethodException is different from
* null. Either way, the changes to the models must be
* streamed back to the client.
*/
component.incrementSCN();
replyData.serverChangeNumber_ = component.getSCN(); // and immediately
// save to replyData
if (reply.getRollbackModelChanges()) {
component.rollback();
} else {
// stream changes
Serializer ser = fac.createSerializer(null, 2048);
try {
component.externalize(ser, false);
replyData.pageDeltas_ = ser.getResult().getBytes();
} catch (Exception ex) {
// we cannot externalize the changes
// since the client won't get them, we must rollback
component.rollback();
if (eventMethodException == null) {
throw new SysException(ex, "cannot externalize server changes")
.setCode(Codes.SERVER_EXTERNALIZE_DELTAS);
} else {
// mad situation: the server event method terminated badly
// and we cannot externalize
// the changes. We think that eventMethodException is more
// important to the client
// and write ex to System.err
ex.printStackTrace();
}
}
}
if (!component.isStateless()) {
component.commit();
}
// throw the saved eventMethodException
if (eventMethodException != null) {
throw eventMethodException;
}
}
/**
* Writes some counts about the session and component to standard output.
* This information is for diagnostic purpose and should be output at the
* developers seat.
*/
private void outputPostRPCStatistics(ComponentServer dispatchComponent,
XMASessionServer xmaSession, String eventName) {
// session counts
int[] sessionCounts = new int[3];
xmaSession.getCounts(sessionCounts);
System.out.println("RemoteCall: statistics session: " + sessionCounts[0]
+ " components, " + sessionCounts[1] + " pages, " + sessionCounts[2]
+ " bytes.");
System.out.println("RemoteCall: statistics of component "
+ dispatchComponent.getName() + ": "
+ dispatchComponent.getNumPageModels() + " pages, "
+ dispatchComponent.estimateMemory() + " bytes.");
}
/**
* Checks if the logged in user is allowed to perform the given server side
* event.
*
* @param xmaSession
* the session this event belongs to
* @param callData
* specifying component, page and name of the event
* @return true if the logged in user is allowed to perform the event, false
* otherwise
*/
private boolean checkPermission(XMASessionServer xmaSession, RemoteCallData callData) {
String component = callData.getNamComponent();
String page = null;
String event = null;
if (callData.idPage_ != 0) {
try {
Class compClass = Class.forName(xmaSession
.getComponentClassName(component));
Method method = compClass.getMethod(
"getShortModelClass",
new Class[] { short.class });
String pageName = (String) method.invoke(null, new Object[] { new Short(
callData.getIdPageType()) });
page = component + '/' + pageName;
} catch (Exception exc) {
throw new SysException(exc,"xma server runtime error").setCode(Codes.SERVER_RUNTIME_ERROR);
}
event = page + '/' + callData.getEventName();
} else {
event = component + '/' + callData.getEventName();
}
return xmaSession.checkPermission(event);
}
/**
* Returns the praefix of a measurement variable name that contains the
* context path of the web application and looks like:
* "app<contextPath>:"
*/
private static String getMeasurementPraefix(HttpServletRequest req) {
String contextPath = req.getContextPath();
if (contextPath.length() == 0) {
return "app:";
}
if (contextPath.charAt(0) == '/') {
contextPath = contextPath.substring(1);
}
return "app<" + contextPath + ">:";
}
/**
* Retrieves the GlobalEvents from GlobalEventManager and sets them as
* RemoteReply paramter, if any GlobalEvents are existing.
*
* @param reply
* @since version_number
* @author s3460
*/
private void setGlobalEvents(RemoteReply reply) {
if(GlobalEventManager.isGlobalEventActivated()){
reply.getReplyData().setParameterInternal(
RemoteReply.PARAM_GLOBAL_EVENTS,
GlobalEventManager.getInstance().pollEvents());
}
}
}