All Downloads are FREE. Search and download functionalities are using the official Maven repository.

at.spardat.xma.rpc.RPCServletServer Maven / Gradle / Ivy

There is a newer version: 6.0.2
Show newest version
/*******************************************************************************
 * 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: *

    *
  1. Deserialize a RemoteCallData object from the * ServletInputStream. This is the data transmitted from the client. *
  2. Extract the model changes from RemoteCallData and integrate * the changes in the servers component buddy. *
  3. Execute the server event method, where the control leaves the * XMA-runtime and is handled over to the XMA program. *
  4. 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()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy