View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.edl.framework.workflow;
17  
18  import org.apache.log4j.Logger;
19  import org.kuali.rice.core.api.util.xml.XmlException;
20  import org.kuali.rice.core.api.util.xml.XmlJotter;
21  import org.kuali.rice.kew.api.KewApiServiceLocator;
22  import org.kuali.rice.kew.api.document.DocumentContent;
23  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
24  import org.kuali.rice.kew.framework.postprocessor.DeleteEvent;
25  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
26  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
27  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
28  import org.kuali.rice.kew.postprocessor.DefaultPostProcessor;
29  import org.w3c.dom.Document;
30  import org.w3c.dom.Element;
31  import org.xml.sax.InputSource;
32  
33  import javax.xml.parsers.DocumentBuilder;
34  import javax.xml.parsers.DocumentBuilderFactory;
35  import javax.xml.xpath.XPath;
36  import javax.xml.xpath.XPathConstants;
37  import javax.xml.xpath.XPathExpressionException;
38  import javax.xml.xpath.XPathFactory;
39  import java.io.ByteArrayOutputStream;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.InterruptedIOException;
43  import java.io.OutputStream;
44  import java.io.StringReader;
45  import java.lang.reflect.Method;
46  import java.net.Socket;
47  import java.net.URL;
48  import java.rmi.RemoteException;
49  import java.util.Timer;
50  import java.util.TimerTask;
51  
52  
53  /**
54   * PostProcessor responsible for posting events to a url defined in the EDL doc definition.
55   * @author Kuali Rice Team (rice.collab@kuali.org)
56   */
57  public class EDocLitePostProcessor extends DefaultPostProcessor {
58      private static final Logger LOG = Logger.getLogger(EDocLitePostProcessor.class);
59      private static final Timer TIMER = new Timer();
60      public static final int SUBMIT_URL_MILLISECONDS_WAIT = 60000;
61      public static final String EVENT_TYPE_ACTION_TAKEN = "actionTaken";
62      public static final String EVENT_TYPE_DELETE_ROUTE_HEADER = "deleteRouteHeader";
63      public static final String EVENT_TYPE_ROUTE_LEVEL_CHANGE = "routeLevelChange";
64      public static final String EVENT_TYPE_ROUTE_STATUS_CHANGE = "statusChange";
65  
66      private static String getURL(Document edlDoc) throws XPathExpressionException {
67          XPath xpath = XPathFactory.newInstance().newXPath();
68          return (String) xpath.evaluate("//edlContent/edl/eventNotificationURL", edlDoc, XPathConstants.STRING);
69      }
70  
71      /**
72       * @param urlstring
73       * @param eventDoc
74       */
75      private static void submitURL(String urlstring, Document eventDoc) throws IOException {
76          String content;
77          try {
78              content = XmlJotter.jotNode(eventDoc, true);
79          } catch (XmlException te) {
80              LOG.error("Error writing serializing event doc: " + eventDoc);
81              throw te;
82          }
83          byte[] contentBytes = content.getBytes("UTF-8");
84  
85          LOG.debug("submitURL: " + urlstring);
86          URL url = new URL(urlstring);
87  
88          String message = "POST " + url.getFile() + " HTTP/1.0\r\n" +
89                           "Content-Length: " + contentBytes.length + "\r\n" +
90                           "Cache-Control: no-cache\r\n" +
91                           "Pragma: no-cache\r\n" +
92                           "User-Agent: Java/1.4.2; EDocLitePostProcessor\r\n" +
93                           "Host: " + url.getHost() + "\r\n" +
94                           "Connection: close\r\n" +
95                           "Content-Type: application/x-www-form-urlencoded\r\n\r\n" +
96                           content;
97  
98          byte[] buf = message.getBytes("UTF-8");
99          Socket s = new Socket(url.getHost(), url.getPort());
100 
101         /*URLConnection con = url.openConnection();
102         LOG.debug("got connection: " + con);
103         con.setDoOutput(true);
104         con.setDoInput(true);
105         LOG.debug("setDoOutput(true)");
106 
107         con.setRequestProperty("Connection", "close");
108         con.setRequestProperty("Content-Length", String.valueOf(buf.length));*/
109 
110         OutputStream os = s.getOutputStream();
111         try {
112             try {
113                 os.write(buf, 0, buf.length);
114                 os.flush();
115             } catch (InterruptedIOException ioe) {
116                 LOG.error("IO was interrupted while posting event to url " + urlstring + ": " + ioe.getMessage());
117             } catch (IOException ioe) {
118                 LOG.error("Error posting EDocLite content to url " + urlstring + ioe.getMessage());
119             } finally {
120                 try {
121                     LOG.debug("Shutting down output stream");
122                     s.shutdownOutput();
123                 } catch (IOException ioe) {
124                     LOG.error("Error shutting down output stream for url " + urlstring + ": " + ioe.getMessage());
125                 }
126             }
127 
128             InputStream is = s.getInputStream();
129             try {
130 
131                 buf = new byte[1024];
132                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
133                 // this is what actually forces the write on the URLConnection!
134                 int read = is.read(buf);
135                 if (read != -1) {
136                     baos.write(buf, 0, read);
137                 }
138                 LOG.debug("EDocLite post processor response:\n" + new String(baos.toByteArray()));
139             } catch (InterruptedIOException ioe) {
140                 LOG.error("IO was interrupted while reading response from url " + urlstring + ": " + ioe.getMessage());
141             } catch (IOException ioe) {
142                 LOG.error("Error reading response from EDocLite handler url " + urlstring + ioe.getMessage());
143             } finally {
144                 try {
145                     LOG.debug("Shutting down input stream");
146                     s.shutdownInput();
147                 } catch (IOException ioe) {
148                     LOG.error("Error shutting down input stream for url " + urlstring + ": " + ioe.getMessage());
149                 }
150             }
151         } finally {
152             try {
153                 s.close();
154             } catch (IOException ioe) {
155                 LOG.error("Error closing socket", ioe);
156             }
157         }
158     }
159 
160     protected static void postEvent(String docId, Object event, String eventName) {
161     	try {
162     		Document doc = getEDLContent(docId);
163     		if(LOG.isDebugEnabled()){
164     			LOG.debug("Submitting doc: " + XmlJotter.jotNode(doc));
165     		}
166 
167     		String urlstring = getURL(doc);
168     		if (org.apache.commons.lang.StringUtils.isEmpty(urlstring)) {
169     			LOG.warn("No eventNotificationURL defined in EDLContent");
170     			return;
171     		}
172 
173     		Document eventDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
174     		Element eventE = eventDoc.createElement("event");
175     		eventE.setAttribute("type", eventName);
176     		eventDoc.appendChild(eventE);
177 
178     		Element infoE = (Element) eventDoc.importNode(propertiesToXml(event, "info"), true);
179     		Element docIdE = eventDoc.createElement("docId");
180     		docIdE.appendChild(eventDoc.createTextNode(String.valueOf(docId)));
181     		infoE.appendChild(docIdE);
182 
183     		eventE.appendChild(infoE);
184     		eventE.appendChild(eventDoc.importNode(doc.getDocumentElement(), true));
185 
186     		String query = "docId=" + docId;
187     		if (urlstring.indexOf('?') != -1) {
188     			urlstring += "&" + query;
189     		} else {
190     			urlstring += "?" + query;
191     		}
192 
193     		final String _urlstring = urlstring;
194     		final Document _eventDoc = eventDoc;
195     		// a super cheesy way to enforce asynchronicity/timeout follows:
196     		final Thread t = new Thread(new Runnable() {
197     			public void run() {
198     				try {
199     					LOG.debug("Post Event calling url: " + _urlstring);
200     					submitURL(_urlstring, _eventDoc);
201     					LOG.debug("Post Event done calling url: " + _urlstring);
202     				} catch (Exception e) {
203     					LOG.error(e);
204     				}
205     			}
206     		});
207     		t.setDaemon(true);
208     		t.start();
209 
210     		// kill the submission thread if it hasn't completed after 1 minute
211     		TIMER.schedule(new TimerTask() {
212     			public void run() {
213     				t.interrupt();
214     			}
215     		}, SUBMIT_URL_MILLISECONDS_WAIT);
216     	} catch (Exception e) {
217     		if (e instanceof RuntimeException) {
218     			throw (RuntimeException)e;
219     		}
220     		throw new RuntimeException(e);
221     	}
222     }
223 
224     public ProcessDocReport doRouteStatusChange(DocumentRouteStatusChange event) throws RemoteException {
225         LOG.debug("doRouteStatusChange: " + event);
226         postEvent(event.getDocumentId(), event, EVENT_TYPE_ROUTE_STATUS_CHANGE);
227         return new ProcessDocReport(true, "");
228     }
229 
230     public ProcessDocReport doActionTaken(ActionTakenEvent event) throws RemoteException {
231         LOG.debug("doActionTaken: " + event);
232         postEvent(event.getDocumentId(), event, EVENT_TYPE_ACTION_TAKEN);
233         return new ProcessDocReport(true, "");
234     }
235 
236     public ProcessDocReport doDeleteRouteHeader(DeleteEvent event) throws RemoteException {
237         LOG.debug("doDeleteRouteHeader: " + event);
238         postEvent(event.getDocumentId(), event, EVENT_TYPE_DELETE_ROUTE_HEADER);
239         return new ProcessDocReport(true, "");
240     }
241 
242     public ProcessDocReport doRouteLevelChange(DocumentRouteLevelChange event) throws RemoteException {
243         LOG.debug("doRouteLevelChange: " + event);
244         postEvent(event.getDocumentId(), event, EVENT_TYPE_ROUTE_LEVEL_CHANGE);
245         return new ProcessDocReport(true, "");
246     }
247 
248     public static Document getEDLContent(String documentId) {
249     	try {
250     		DocumentContent documentContent = KewApiServiceLocator.getWorkflowDocumentService().getDocumentContent(documentId);
251     		String content = documentContent.getFullContent();
252     		Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(content)));
253     		return doc;
254     	} catch (Exception e) {
255     		if (e instanceof RuntimeException) {
256     			throw (RuntimeException)e;
257     		}
258     		throw new RuntimeException(e);
259     	}
260     }
261 
262     public static DocumentBuilder getDocumentBuilder() throws Exception {
263         return DocumentBuilderFactory.newInstance().newDocumentBuilder();
264     }
265 
266     private static String lowerCaseFirstChar(String s) {
267         if (s.length() == 0 || Character.isLowerCase(s.charAt(0)))
268             return s;
269         StringBuffer sb = new StringBuffer(s.length());
270         sb.append(Character.toLowerCase(s.charAt(0)));
271         if (s.length() > 1) {
272             sb.append(s.substring(1));
273         }
274         return sb.toString();
275     }
276 
277     public static Element propertiesToXml(Object o, String elementName) throws Exception {
278         Class c = o.getClass();
279         Document doc = getDocumentBuilder().newDocument();
280         Element wrapper = doc.createElement(elementName);
281         Method[] methods = c.getMethods();
282         for (int i = 0; i < methods.length; i++) {
283             String name = methods[i].getName();
284             if ("getClass".equals(name))
285                 continue;
286             if (!name.startsWith("get") || methods[i].getParameterTypes().length > 0)
287                 continue;
288             name = name.substring("get".length());
289             name = lowerCaseFirstChar(name);
290             String value = null;
291             try {
292                 Object result = methods[i].invoke(o, null);
293                 if (result == null) {
294                     LOG.debug("value of " + name + " method on object " + o.getClass() + " is null");
295                     value = "";
296                 } else {
297                     value = result.toString();
298                 }
299                 Element fieldE = doc.createElement(name);
300                 fieldE.appendChild(doc.createTextNode(value));
301                 wrapper.appendChild(fieldE);
302             } catch (RuntimeException e) {
303                 LOG.error("Error accessing method '" + methods[i].getName() + " of instance of " + c);
304                 throw e;
305             } catch (Exception e) {
306                 LOG.error("Error accessing method '" + methods[i].getName() + " of instance of " + c);
307             }
308         }
309         return wrapper;
310     }
311 }