001/** 002 * Copyright 2005-2015 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.edl.framework.workflow; 017 018import org.apache.logging.log4j.Logger; 019import org.apache.logging.log4j.LogManager; 020import org.kuali.rice.core.api.util.xml.XmlException; 021import org.kuali.rice.core.api.util.xml.XmlJotter; 022import org.kuali.rice.kew.api.KewApiServiceLocator; 023import org.kuali.rice.kew.api.document.DocumentContent; 024import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent; 025import org.kuali.rice.kew.framework.postprocessor.DeleteEvent; 026import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange; 027import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 028import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 029import org.kuali.rice.kew.postprocessor.DefaultPostProcessor; 030import org.w3c.dom.Document; 031import org.w3c.dom.Element; 032import org.xml.sax.InputSource; 033 034import javax.xml.parsers.DocumentBuilder; 035import javax.xml.parsers.DocumentBuilderFactory; 036import javax.xml.xpath.XPath; 037import javax.xml.xpath.XPathConstants; 038import javax.xml.xpath.XPathExpressionException; 039import javax.xml.xpath.XPathFactory; 040import java.io.ByteArrayOutputStream; 041import java.io.IOException; 042import java.io.InputStream; 043import java.io.InterruptedIOException; 044import java.io.OutputStream; 045import java.io.StringReader; 046import java.lang.reflect.Method; 047import java.net.Socket; 048import java.net.URL; 049import java.rmi.RemoteException; 050import java.util.Timer; 051import java.util.TimerTask; 052 053 054/** 055 * PostProcessor responsible for posting events to a url defined in the EDL doc definition. 056 * @author Kuali Rice Team (rice.collab@kuali.org) 057 */ 058public class EDocLitePostProcessor extends DefaultPostProcessor { 059 private static final Logger LOG = LogManager.getLogger(EDocLitePostProcessor.class); 060 private static final Timer TIMER = new Timer(); 061 public static final int SUBMIT_URL_MILLISECONDS_WAIT = 60000; 062 public static final String EVENT_TYPE_ACTION_TAKEN = "actionTaken"; 063 public static final String EVENT_TYPE_DELETE_ROUTE_HEADER = "deleteRouteHeader"; 064 public static final String EVENT_TYPE_ROUTE_LEVEL_CHANGE = "routeLevelChange"; 065 public static final String EVENT_TYPE_ROUTE_STATUS_CHANGE = "statusChange"; 066 067 private static String getURL(Document edlDoc) throws XPathExpressionException { 068 XPath xpath = XPathFactory.newInstance().newXPath(); 069 return (String) xpath.evaluate("//edlContent/edl/eventNotificationURL", edlDoc, XPathConstants.STRING); 070 } 071 072 /** 073 * @param urlstring 074 * @param eventDoc 075 */ 076 private static void submitURL(String urlstring, Document eventDoc) throws IOException { 077 String content; 078 try { 079 content = XmlJotter.jotNode(eventDoc, true); 080 } catch (XmlException te) { 081 LOG.error("Error writing serializing event doc: " + eventDoc); 082 throw te; 083 } 084 byte[] contentBytes = content.getBytes("UTF-8"); 085 086 LOG.debug("submitURL: " + urlstring); 087 URL url = new URL(urlstring); 088 089 String message = "POST " + url.getFile() + " HTTP/1.0\r\n" + 090 "Content-Length: " + contentBytes.length + "\r\n" + 091 "Cache-Control: no-cache\r\n" + 092 "Pragma: no-cache\r\n" + 093 "User-Agent: Java/1.4.2; EDocLitePostProcessor\r\n" + 094 "Host: " + url.getHost() + "\r\n" + 095 "Connection: close\r\n" + 096 "Content-Type: application/x-www-form-urlencoded\r\n\r\n" + 097 content; 098 099 byte[] buf = message.getBytes("UTF-8"); 100 Socket s = new Socket(url.getHost(), url.getPort()); 101 102 /*URLConnection con = url.openConnection(); 103 LOG.debug("got connection: " + con); 104 con.setDoOutput(true); 105 con.setDoInput(true); 106 LOG.debug("setDoOutput(true)"); 107 108 con.setRequestProperty("Connection", "close"); 109 con.setRequestProperty("Content-Length", String.valueOf(buf.length));*/ 110 111 OutputStream os = s.getOutputStream(); 112 try { 113 try { 114 os.write(buf, 0, buf.length); 115 os.flush(); 116 } catch (InterruptedIOException ioe) { 117 LOG.error("IO was interrupted while posting event to url " + urlstring + ": " + ioe.getMessage()); 118 } catch (IOException ioe) { 119 LOG.error("Error posting EDocLite content to url " + urlstring + ioe.getMessage()); 120 } finally { 121 try { 122 LOG.debug("Shutting down output stream"); 123 s.shutdownOutput(); 124 } catch (IOException ioe) { 125 LOG.error("Error shutting down output stream for url " + urlstring + ": " + ioe.getMessage()); 126 } 127 } 128 129 InputStream is = s.getInputStream(); 130 try { 131 132 buf = new byte[1024]; 133 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 134 // this is what actually forces the write on the URLConnection! 135 int read = is.read(buf); 136 if (read != -1) { 137 baos.write(buf, 0, read); 138 } 139 LOG.debug("EDocLite post processor response:\n" + new String(baos.toByteArray())); 140 } catch (InterruptedIOException ioe) { 141 LOG.error("IO was interrupted while reading response from url " + urlstring + ": " + ioe.getMessage()); 142 } catch (IOException ioe) { 143 LOG.error("Error reading response from EDocLite handler url " + urlstring + ioe.getMessage()); 144 } finally { 145 try { 146 LOG.debug("Shutting down input stream"); 147 s.shutdownInput(); 148 } catch (IOException ioe) { 149 LOG.error("Error shutting down input stream for url " + urlstring + ": " + ioe.getMessage()); 150 } 151 } 152 } finally { 153 try { 154 s.close(); 155 } catch (IOException ioe) { 156 LOG.error("Error closing socket", ioe); 157 } 158 } 159 } 160 161 protected static void postEvent(String docId, Object event, String eventName) { 162 try { 163 Document doc = getEDLContent(docId); 164 if(LOG.isDebugEnabled()){ 165 LOG.debug("Submitting doc: " + XmlJotter.jotNode(doc)); 166 } 167 168 String urlstring = getURL(doc); 169 if (org.apache.commons.lang.StringUtils.isEmpty(urlstring)) { 170 LOG.warn("No eventNotificationURL defined in EDLContent"); 171 return; 172 } 173 174 Document eventDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); 175 Element eventE = eventDoc.createElement("event"); 176 eventE.setAttribute("type", eventName); 177 eventDoc.appendChild(eventE); 178 179 Element infoE = (Element) eventDoc.importNode(propertiesToXml(event, "info"), true); 180 Element docIdE = eventDoc.createElement("docId"); 181 docIdE.appendChild(eventDoc.createTextNode(String.valueOf(docId))); 182 infoE.appendChild(docIdE); 183 184 eventE.appendChild(infoE); 185 eventE.appendChild(eventDoc.importNode(doc.getDocumentElement(), true)); 186 187 String query = "docId=" + docId; 188 if (urlstring.indexOf('?') != -1) { 189 urlstring += "&" + query; 190 } else { 191 urlstring += "?" + query; 192 } 193 194 final String _urlstring = urlstring; 195 final Document _eventDoc = eventDoc; 196 // a super cheesy way to enforce asynchronicity/timeout follows: 197 final Thread t = new Thread(new Runnable() { 198 public void run() { 199 try { 200 LOG.debug("Post Event calling url: " + _urlstring); 201 submitURL(_urlstring, _eventDoc); 202 LOG.debug("Post Event done calling url: " + _urlstring); 203 } catch (Exception e) { 204 LOG.error(e.getMessage(), e); 205 } 206 } 207 }); 208 t.setDaemon(true); 209 t.start(); 210 211 // kill the submission thread if it hasn't completed after 1 minute 212 TIMER.schedule(new TimerTask() { 213 public void run() { 214 t.interrupt(); 215 } 216 }, SUBMIT_URL_MILLISECONDS_WAIT); 217 } catch (Exception e) { 218 if (e instanceof RuntimeException) { 219 throw (RuntimeException)e; 220 } 221 throw new RuntimeException(e); 222 } 223 } 224 225 public ProcessDocReport doRouteStatusChange(DocumentRouteStatusChange event) throws RemoteException { 226 LOG.debug("doRouteStatusChange: " + event); 227 postEvent(event.getDocumentId(), event, EVENT_TYPE_ROUTE_STATUS_CHANGE); 228 return new ProcessDocReport(true, ""); 229 } 230 231 public ProcessDocReport doActionTaken(ActionTakenEvent event) throws RemoteException { 232 LOG.debug("doActionTaken: " + event); 233 postEvent(event.getDocumentId(), event, EVENT_TYPE_ACTION_TAKEN); 234 return new ProcessDocReport(true, ""); 235 } 236 237 public ProcessDocReport doDeleteRouteHeader(DeleteEvent event) throws RemoteException { 238 LOG.debug("doDeleteRouteHeader: " + event); 239 postEvent(event.getDocumentId(), event, EVENT_TYPE_DELETE_ROUTE_HEADER); 240 return new ProcessDocReport(true, ""); 241 } 242 243 public ProcessDocReport doRouteLevelChange(DocumentRouteLevelChange event) throws RemoteException { 244 LOG.debug("doRouteLevelChange: " + event); 245 postEvent(event.getDocumentId(), event, EVENT_TYPE_ROUTE_LEVEL_CHANGE); 246 return new ProcessDocReport(true, ""); 247 } 248 249 public static Document getEDLContent(String documentId) { 250 try { 251 DocumentContent documentContent = KewApiServiceLocator.getWorkflowDocumentService().getDocumentContent(documentId); 252 String content = documentContent.getFullContent(); 253 Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(content))); 254 return doc; 255 } catch (Exception e) { 256 if (e instanceof RuntimeException) { 257 throw (RuntimeException)e; 258 } 259 throw new RuntimeException(e); 260 } 261 } 262 263 public static DocumentBuilder getDocumentBuilder() throws Exception { 264 return DocumentBuilderFactory.newInstance().newDocumentBuilder(); 265 } 266 267 private static String lowerCaseFirstChar(String s) { 268 if (s.length() == 0 || Character.isLowerCase(s.charAt(0))) 269 return s; 270 StringBuffer sb = new StringBuffer(s.length()); 271 sb.append(Character.toLowerCase(s.charAt(0))); 272 if (s.length() > 1) { 273 sb.append(s.substring(1)); 274 } 275 return sb.toString(); 276 } 277 278 public static Element propertiesToXml(Object o, String elementName) throws Exception { 279 Class c = o.getClass(); 280 Document doc = getDocumentBuilder().newDocument(); 281 Element wrapper = doc.createElement(elementName); 282 Method[] methods = c.getMethods(); 283 for (int i = 0; i < methods.length; i++) { 284 String name = methods[i].getName(); 285 if ("getClass".equals(name)) 286 continue; 287 if (!name.startsWith("get") || methods[i].getParameterTypes().length > 0) 288 continue; 289 name = name.substring("get".length()); 290 name = lowerCaseFirstChar(name); 291 String value = null; 292 try { 293 Object result = methods[i].invoke(o, null); 294 if (result == null) { 295 LOG.debug("value of " + name + " method on object " + o.getClass() + " is null"); 296 value = ""; 297 } else { 298 value = result.toString(); 299 } 300 Element fieldE = doc.createElement(name); 301 fieldE.appendChild(doc.createTextNode(value)); 302 wrapper.appendChild(fieldE); 303 } catch (RuntimeException e) { 304 LOG.error("Error accessing method '" + methods[i].getName() + " of instance of " + c, e); 305 throw e; 306 } catch (Exception e) { 307 LOG.error("Error accessing method '" + methods[i].getName() + " of instance of " + c, e); 308 } 309 } 310 return wrapper; 311 } 312}