/*
* This file is part of the WfMOpen project.
* Copyright (C) 2001-2003 Danet GmbH (www.danet.de), GS-AN.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id: Servlet.java 2326 2007-03-27 21:59:44Z mlipp $
*
* $Log$
* Revision 1.3 2007/01/19 22:52:58 mlipp
* Updated.
*
* Revision 1.2 2006/09/29 12:32:09 drmlipp
* Consistently using WfMOpen as projct name now.
*
* Revision 1.1.1.2 2004/08/18 15:17:38 drmlipp
* Update to 1.2
*
* Revision 1.19 2004/04/13 15:24:04 lipp
* Improved content-type and debugging support.
*
* Revision 1.18 2004/04/02 12:41:07 lipp
* Added missing xml decalaration.
*
* Revision 1.17 2004/03/18 10:15:26 schlue
* Session invalidation for closed process added.
*
* Revision 1.16 2004/03/04 08:25:23 schlue
* Stylesheet example added (transformation performed by web browser)
*
* Revision 1.15 2004/03/01 14:58:22 schlue
* Chabacc servlet extensions, part 2 completed.
*
* Revision 1.14 2004/02/27 10:20:17 schlue
* Review comments implemented.
*
* Revision 1.13 2004/02/25 16:15:19 schlue
* Chabacc servlet extensions added.
*
* Revision 1.12 2004/02/24 15:20:05 schlue
* Servlet code (initial version) added.
*
* Revision 1.11 2004/01/30 14:36:30 lipp
* Partial implementation of message receipt.
*
* Revision 1.10 2003/10/08 19:13:38 lipp
* Fixed multi caller problem.
*
* Revision 1.9 2003/10/08 15:15:35 lipp
* Updated channel handling.
*
* Revision 1.8 2003/10/07 21:04:59 lipp
* Made process selectable.
*
* Revision 1.7 2003/10/06 20:43:22 lipp
* Continuing channel communication.
*
* Revision 1.6 2003/10/06 18:28:48 lipp
* Added data submission.
*
* Revision 1.5 2003/10/06 15:21:15 lipp
* Use session create method.
*
* Revision 1.4 2003/10/06 13:57:18 lipp
* Finished restructuring.
*
* Revision 1.3 2003/10/05 19:57:20 lipp
* Prepared reorganization.
*
* Revision 1.2 2003/10/05 15:40:18 lipp
* Continuing chabacc implementation.
*
* Revision 1.1 2003/10/02 20:04:28 lipp
* Started channel based access module.
*
*/
package de.danet.an.workflow.tools.chabacc.plain;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;
import java.util.StringTokenizer;
import java.lang.reflect.InvocationTargetException;
import java.rmi.RemoteException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import org.xml.sax.SAXException;
import de.danet.an.workflow.omgcore.AlreadyRunningException;
import de.danet.an.workflow.omgcore.CannotStartException;
import de.danet.an.workflow.omgcore.InvalidDataException;
import de.danet.an.workflow.omgcore.ProcessData;
import de.danet.an.workflow.omgcore.WfProcess;
import de.danet.an.workflow.api.Channel;
import de.danet.an.workflow.api.DefaultProcessData;
import de.danet.an.workflow.api.DefaultRequester;
import de.danet.an.workflow.api.FactoryConfigurationError;
import de.danet.an.workflow.api.InvalidKeyException;
import de.danet.an.workflow.api.MethodInvocationBatch;
import de.danet.an.workflow.api.ProcessDefinitionDirectory;
import de.danet.an.workflow.api.ProcessDirectory;
import de.danet.an.workflow.api.ProcessMgr;
import de.danet.an.workflow.api.SAXEventBuffer;
import de.danet.an.workflow.api.WorkflowService;
import de.danet.an.workflow.api.WorkflowServiceFactory;
/**
* This class provides an interface between the workflow engine (i.e. its
* processes and any HTTP based client (e.g. Web browser application).
*
* @author <a href="mailto:mnl@mnl.de">Michael N. Lipp</a>
* @version $Revision: 2326 $
*/
public class Servlet extends HttpServlet {
private static final org.apache.commons.logging.Log logger
= org.apache.commons.logging.LogFactory.getLog(Servlet.class);
/** Configuration parameter "packageId" */
private String defaultPackageId = "boot";
/** Configuration parameter "processId" */
private String defaultProcessId = "chabacc_http_plain";
/** Cachable singleton "workflow service" */
private WorkflowService wfsCache = null;
/** Local exception for handling processing problems. */
private class ProcessingException extends Exception {
private int statusCode = HttpServletResponse.SC_OK;
/** Create exception always with error message and status code.
* @param message error message
* @param sc status code for servlet response
*/
public ProcessingException(int sc, String message) {
super(message);
statusCode = sc;
}
public int getStatusCode() {
return statusCode;
}
}
/** Container class for process lookup (retrieval or creation). */
private class ProcessLookup {
WfProcess process = null;
String packageId = null;
String processId = null;
String procKey = null;
boolean created = false;
}
/** Container class for reponse information from channel. */
private class ResponseInfo {
String responseData;
String mimeType;
boolean invalidateSession;
}
/**
* Default init method.
*
* @param servletConfig a <code>ServletConfig</code> value
* @exception ServletException if an error occurs
*/
public void init(ServletConfig servletConfig) throws ServletException {
// Read configuration values and overwrite default, if set
if (servletConfig != null) {
String id = servletConfig.getInitParameter
("packagePathSegmentDefault");
if (id != null) {
defaultPackageId = id;
}
id = servletConfig.getInitParameter("processPathSegmentDefault");
if (id != null) {
defaultProcessId = id;
}
}
}
/**
* Receive HTTP requests for a channel based access to a workflow process.
* See user manual (chapter "tools") for a detailed description.
*
* @param request a <code>HttpServletRequest</code> value
* @param response a <code>HttpServletResponse</code> value
* @exception ServletException if an error occurs
* @exception IOException if an error occurs
*/
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String packageID = null;
String processID = null;
String dataItemName = null;
String dataItemValue = null;
boolean waitForResponse = true;
// Scan request params and store data values in a map for later use
ProcessData sendData = new DefaultProcessData();
for (Enumeration e = request.getParameterNames();
e.hasMoreElements();) {
String pn = (String)e.nextElement();
if (pn.equals("WFM_packageID")) {
packageID = request.getParameter(pn);
} else if (pn.equals("WFM_processID")) {
processID = request.getParameter(pn);
} else if (pn.equals("WFM_dataItemName")) {
dataItemName = request.getParameter(pn);
} else if (pn.equals("WFM_dataItemValue")) {
dataItemValue = request.getParameter(pn);
} else if (pn.equals("WFM_waitForResponse")) {
waitForResponse
= (new Boolean (request.getParameter(pn)).booleanValue());
} else {
sendData.put(pn, request.getParameter(pn));
}
}
HttpSession session = request.getSession(true);
if (logger.isDebugEnabled()) {
logger.debug("Post called for session " + session.getId());
}
WorkflowService wfs = getWorkflowService();
if (wfs == null) {
response.sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Unable to retrieve workflow service");
return;
}
Channel channel = null;
boolean invalidateSession = false;
try {
// Retrieve (or create) process
ProcessLookup pl = lookupProcess
(session, packageID, processID,
sendData, dataItemName, dataItemValue);
// Open channel to process (and start process, or send data)
if (waitForResponse) {
channel = openChannel(pl);
}
if (pl.created) {
pl.process.start();
} else {
sendData (session, channel, sendData);
}
// If no response expected, that's it
if (!waitForResponse) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
// forward answer from process
Map data = receiveData (session, channel);
ResponseInfo responseInfo = getResponseInfo(data);
invalidateSession = responseInfo.invalidateSession;
response.setStatus(HttpServletResponse.SC_OK);
String mt = responseInfo.mimeType;
if (mt.indexOf("charset=") < 0) {
mt = mt + "; charset=UTF-8";
}
if (logger.isDebugEnabled ()) {
logger.debug ("Response (of type \"" + mt + "\") is:\n"
+ responseInfo.responseData);
}
response.setContentType(mt);
PrintWriter ow = response.getWriter ();
ow.write(responseInfo.responseData);
ow.close();
return;
} catch (CannotStartException e) {
logger.error(e.getMessage());
response.sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
} catch (AlreadyRunningException e) {
logger.error(e.getMessage());
response.sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
} catch (ProcessingException e) {
response.sendError (e.getStatusCode(), e.getMessage());
return;
} finally {
if (channel != null ) {
wfs.release(channel);
}
if (invalidateSession) {
session.invalidate();
}
}
}
/**
* Create a new process and forward initial response (e.g. HTML start page).
* See user manual (chapter "tools") for a detailed description.
*
* @param request a <code>HttpServletRequest</code> value
* @param response a <code>HttpServletResponse</code> value
* @exception ServletException if an error occurs
* @exception IOException if an error occurs
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String packageID = defaultPackageId;
String processID = defaultProcessId;
String extraPath = request.getPathInfo();
if (extraPath != null) {
// Read package ID and process ID from extra path info
StringTokenizer tok = new StringTokenizer(extraPath, "/");
if (tok.hasMoreTokens()) {
packageID = tok.nextToken();
}
if (tok.hasMoreTokens()) {
processID = tok.nextToken();
}
}
WorkflowService wfs = getWorkflowService();
if (wfs == null) {
response.sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Unable to retrieve workflow service");
return;
}
// Create new process
Channel channel = null;
try {
ProcessData sendData = new DefaultProcessData();
// Create new process
ProcessLookup pl = lookupProcess
(null, packageID, processID, sendData, null, null);
// Build connection to process and start process
channel = openChannel(pl);
if (pl.created) {
pl.process.start();
}
// Send data to process and forward answer from process
if (!pl.created) {
sendData (null, channel, sendData);
}
Map data = receiveData (null, channel);
ResponseInfo responseInfo = getResponseInfo(data);
response.setStatus(HttpServletResponse.SC_OK);
String mt = responseInfo.mimeType;
if (mt.indexOf("charset=") < 0) {
mt = mt + "; charset=UTF-8";
}
if (logger.isDebugEnabled ()) {
logger.debug ("Response (of type \"" + mt + "\") is:\n"
+ responseInfo.responseData);
}
response.setContentType(mt);
PrintWriter ow = response.getWriter ();
ow.write(responseInfo.responseData);
ow.close();
return;
} catch (CannotStartException e) {
logger.error(e.getMessage());
response.sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
} catch (AlreadyRunningException e) {
logger.error(e.getMessage());
response.sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
} catch (ProcessingException e) {
response.sendError (e.getStatusCode(), e.getMessage());
return;
} finally {
if (channel != null ) {
wfs.release(channel);
}
}
}
/**
* Helper for process lookup (retrieval or creation).
* Saves information about the process in session attributes
* "WfM_mgrName" and "WfM_procKey".
*
* @param session current HTTP session
* @param packageID package id of process manager
* @param processID id of process
* @param initData processData for process initialization
* @param dataItemName name of additional process data
* @param dataItemValue value of additional process data
* @exception ProcessingException if an error occurs
*/
private ProcessLookup lookupProcess
(HttpSession session, String packageID, String processID,
ProcessData initData, String dataItemName, String dataItemValue)
throws ProcessingException {
while (true) {
try {
WorkflowService wfs = getWorkflowService();
if (wfs == null) {
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Unable to retrieve workflow service");
}
ProcessLookup pl = new ProcessLookup();
ProcessDefinitionDirectory procDefDir
= wfs.processDefinitionDirectory();
if ( packageID != null && processID != null ) {
ProcessMgr mgr = null;
try {
mgr = procDefDir.processMgr(packageID, processID);
} catch (InvalidKeyException e) {
throw new ProcessingException
(HttpServletResponse.SC_BAD_REQUEST,
e.getMessage());
}
String mgrName = mgr.name();
if (dataItemName != null && dataItemValue != null) {
// Try to connect to existing process
Collection procs = mgr
.findByDataItem(dataItemName, dataItemValue);
if (procs.size() == 1) {
pl.process = (WfProcess)procs.iterator().next();
pl.procKey = pl.process.key();
pl.packageId = packageID;
pl.processId = processID;
if ( session != null) {
session.setAttribute("WfM_mgrName", mgr.name());
session.setAttribute("WfM_procKey", pl.procKey);
}
return pl;
}
if (procs.size() > 1) {
logger.error
("More than one process found for " + mgr.name()
+ ", data item: " + dataItemName + " = "
+ dataItemValue);
throw new ProcessingException
(HttpServletResponse.SC_CONFLICT,
"More than one matching process found");
}
// add to init data for creation
initData.put(dataItemName, dataItemValue);
}
// Not found, create new process
MethodInvocationBatch mib = new MethodInvocationBatch();
mib.addInvocation
(mgr, "createProcess", new String[]
{"de.danet.an.workflow.omgcore.WfRequester"},
new Object[]{new DefaultRequester(wfs)});
mib.addInvocation
(-1, "setProcessContext", new String[]
{"de.danet.an.workflow.omgcore.ProcessData"},
new Object[]{initData}, false);
mib.addInvocation(-2, "key", null, null, false);
MethodInvocationBatch.Result mir
= (MethodInvocationBatch.Result)wfs.executeBatch(mib);
if (mir.hasExceptions ()) {
Exception e = mir.firstException ();
logger.error ("Problem executing batch: "
+ e.getMessage(), e);
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
mir.firstException ().getMessage());
}
pl.process = (WfProcess)mir.result(0);
pl.procKey = mir.resultAsString(2);
pl.packageId = packageID;
pl.processId = processID;
pl.created = true;
if ( session != null) {
session.setAttribute("WfM_mgrName", mgrName);
session.setAttribute("WfM_procKey", pl.procKey);
}
return pl;
}
if (session != null) {
// Retrieve process key from session context
String mgrName
= (String)session.getAttribute("WfM_mgrName");
String procKey
= (String)session.getAttribute("WfM_procKey");
if (mgrName == null || procKey == null) {
throw new ProcessingException
(HttpServletResponse.SC_BAD_REQUEST,
"No process information available");
}
ProcessDirectory procDir = wfs.processDirectory();
try {
pl.process
= procDir.lookupProcess(mgrName, procKey);
pl.procKey = pl.process.key();
} catch (InvalidKeyException e) {
session.invalidate();
throw new ProcessingException
(HttpServletResponse.SC_GONE,
"Process has been removed");
}
StringTokenizer tok = new StringTokenizer(mgrName, "/");
if (tok.hasMoreTokens()) {
pl.packageId = tok.nextToken();
}
if (tok.hasMoreTokens()) {
pl.processId = tok.nextToken();
}
return pl;
}
return pl;
} catch (RemoteException e) {
logger.debug (e.getMessage(), e);
} catch (FactoryConfigurationError e) {
logger.error(e.getMessage());
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
e.getMessage());
} catch (InvocationTargetException e) {
String s = "Unexpected exception: "
+ e.getCause().getMessage();
logger.error(s, e);
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
e.getCause().getMessage());
}
}
}
/**
* Helper for opening a channel to the given process.
* If process has been created, it is started after opening the channel.
*
* @param pl process lookup information
* @exception ProcessingException if an error occurs
*/
private Channel openChannel (ProcessLookup pl)
throws ProcessingException {
while (true) {
try {
WorkflowService wfs = getWorkflowService();
if (wfs == null) {
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Unable to retrieve workflow service");
}
ProcessDefinitionDirectory procDefDir
= wfs.processDefinitionDirectory();
ProcessMgr mgr = null;
try {
mgr = procDefDir.processMgr(pl.packageId, pl.processId);
} catch (InvalidKeyException e) {
throw new ProcessingException
(HttpServletResponse.SC_BAD_REQUEST,
e.getMessage());
}
ProcessDirectory procDir = wfs.processDirectory();
WfProcess proc = null;
try {
proc = procDir.lookupProcess(mgr.name(), pl.procKey);
} catch (InvalidKeyException e) {
throw new ProcessingException
(HttpServletResponse.SC_GONE,
"Process has been removed");
}
return wfs.getChannel(proc, "initiator");
} catch (RemoteException e) {
logger.debug (e.getMessage(), e);
}
}
}
/**
* Helper for sending data to a channel.
*
* @param session current HTTP session
* @param channel channel for sending data
* @param sendData data to be sent on channel
* @exception ProcessingException if an error occurs
*/
private void sendData
(HttpSession session, Channel channel, ProcessData sendData)
throws ProcessingException {
while (true) {
try {
channel.sendMessage(sendData);
break;
} catch (InvalidKeyException e) {
if (session != null) {
session.invalidate();
}
throw new ProcessingException
(HttpServletResponse.SC_GONE,
"Process is no longer accessible: " + e.getMessage());
} catch (InvalidDataException e) {
throw new ProcessingException
(HttpServletResponse.SC_BAD_REQUEST,
e.getMessage());
} catch (RemoteException e) {
logger.debug (e.getMessage(), e);
}
}
}
/**
* Receive data from the given channel.
*
* @param session current HTTP session
* @param channel channel for sending data
* @exception ProcessingException if an error occurs
*/
private Map receiveData (HttpSession session, Channel channel)
throws ProcessingException {
while (true) {
try {
Map data = channel.receiveMessage();
if (data == null) {
throw new InvalidKeyException
("Process has been closed or removed");
}
return data;
} catch (InvalidKeyException e) {
if (session != null) {
session.invalidate();
}
throw new ProcessingException
(HttpServletResponse.SC_GONE,
"Process is no longer accessible: " + e.getMessage());
} catch (RemoteException e) {
logger.debug (e.getMessage(), e);
}
}
}
/**
* Request workflow service once after authentification.
* Thus it performed be called within the servlet's init method
* which may be called during the deployment process.
*/
private synchronized WorkflowService getWorkflowService() {
if ( wfsCache == null ) {
try {
wfsCache = WorkflowServiceFactory.newInstance()
.newWorkflowService();
} catch (FactoryConfigurationError e) {
logger.error(e.getMessage());
}
}
return wfsCache;
}
/**
* Helper to determine complete reponse info from response data map.
*
* @param data response data map
* @exception ProcessingException if an error occurs
*/
private ResponseInfo getResponseInfo(Map data)
throws ProcessingException {
// create and initialize result structure
ResponseInfo responseInfo = new ResponseInfo();
String doctypeSystem = (String)data.get("doctype-system");
String doctypePublic = (String)data.get("doctype-public");
responseInfo.mimeType = (String)data.get("mimeType");
responseInfo.invalidateSession = false;
Object responseData = null;
if (data.size() == 1) {
responseData = data.values().iterator().next();
} else {
responseData = data.get("data");
if (responseData == null) {
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"No data found");
}
if (data.containsKey("invalidateSession")) {
responseInfo.invalidateSession
= ((Boolean)data.get("invalidateSession")).booleanValue();
}
}
// transform data into string
if (responseData instanceof String) {
responseInfo.responseData = (String)responseData;
if (responseInfo.mimeType == null) {
String cmp = (responseInfo.responseData.length() > 50)
? responseInfo.responseData.substring(0,50).toUpperCase()
: responseInfo.responseData.toUpperCase();
if (cmp.startsWith
("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML")
|| cmp.startsWith
("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML")) {
responseInfo.mimeType = "text/html";
} else {
responseInfo.mimeType = "application/data";
}
}
} else if (responseData instanceof SAXEventBuffer) {
CharArrayWriter out = new CharArrayWriter();
try {
SAXEventBuffer eventbuf = (SAXEventBuffer)responseData;
SAXTransformerFactory tf = (SAXTransformerFactory)
TransformerFactory.newInstance();
TransformerHandler th = null;
th = tf.newTransformerHandler();
Transformer xformer = th.getTransformer();
if (doctypeSystem != null) {
xformer.setOutputProperty
(OutputKeys.DOCTYPE_SYSTEM, doctypeSystem);
}
if (doctypePublic != null) {
xformer.setOutputProperty
(OutputKeys.DOCTYPE_PUBLIC, doctypePublic);
}
th.setResult (new StreamResult(out));
eventbuf.emit(th);
} catch (SAXException e) {
String s = "Error generating XML process data: "
+ e.getMessage ();
logger.error (s, e);
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, s);
} catch (TransformerConfigurationException e) {
String s = "Error generating XML process data: "
+ e.getMessage ();
logger.error (s, e);
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, s);
}
responseInfo.responseData = out.toString();
if (responseInfo.mimeType == null) {
responseInfo.mimeType = "text/xml";
}
} else {
String e = "Illegal data type in channel message: "
+ responseData.getClass().getName();
logger.error(e);
throw new ProcessingException
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
return responseInfo;
}
}