001/** 002 * Copyright 2005-2016 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.log4j.Logger; 019import org.kuali.rice.core.api.util.xml.XmlException; 020import org.kuali.rice.core.api.util.xml.XmlJotter; 021import org.kuali.rice.kew.api.KewApiServiceLocator; 022import org.kuali.rice.kew.api.document.DocumentContent; 023import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent; 024import org.kuali.rice.kew.framework.postprocessor.DeleteEvent; 025import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange; 026import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 027import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 028import org.kuali.rice.kew.postprocessor.DefaultPostProcessor; 029import org.w3c.dom.Document; 030import org.w3c.dom.Element; 031import org.xml.sax.InputSource; 032 033import javax.xml.parsers.DocumentBuilder; 034import javax.xml.parsers.DocumentBuilderFactory; 035import javax.xml.xpath.XPath; 036import javax.xml.xpath.XPathConstants; 037import javax.xml.xpath.XPathExpressionException; 038import javax.xml.xpath.XPathFactory; 039import java.io.ByteArrayOutputStream; 040import java.io.IOException; 041import java.io.InputStream; 042import java.io.InterruptedIOException; 043import java.io.OutputStream; 044import java.io.StringReader; 045import java.lang.reflect.Method; 046import java.net.Socket; 047import java.net.URL; 048import java.rmi.RemoteException; 049import java.util.Timer; 050import java.util.TimerTask; 051 052 053/** 054 * PostProcessor responsible for posting events to a url defined in the EDL doc definition. 055 * @author Kuali Rice Team (rice.collab@kuali.org) 056 */ 057public class EDocLitePostProcessor extends DefaultPostProcessor { 058 private static final Logger LOG = Logger.getLogger(EDocLitePostProcessor.class); 059 private static final Timer TIMER = new Timer(); 060 public static final int SUBMIT_URL_MILLISECONDS_WAIT = 60000; 061 public static final String EVENT_TYPE_ACTION_TAKEN = "actionTaken"; 062 public static final String EVENT_TYPE_DELETE_ROUTE_HEADER = "deleteRouteHeader"; 063 public static final String EVENT_TYPE_ROUTE_LEVEL_CHANGE = "routeLevelChange"; 064 public static final String EVENT_TYPE_ROUTE_STATUS_CHANGE = "statusChange"; 065 066 private static String getURL(Document edlDoc) throws XPathExpressionException { 067 XPath xpath = XPathFactory.newInstance().newXPath(); 068 return (String) xpath.evaluate("//edlContent/edl/eventNotificationURL", edlDoc, XPathConstants.STRING); 069 } 070 071 /** 072 * @param urlstring 073 * @param eventDoc 074 */ 075 private static void submitURL(String urlstring, Document eventDoc) throws IOException { 076 String content; 077 try { 078 content = XmlJotter.jotNode(eventDoc, true); 079 } catch (XmlException te) { 080 LOG.error("Error writing serializing event doc: " + eventDoc); 081 throw te; 082 } 083 byte[] contentBytes = content.getBytes("UTF-8"); 084 085 LOG.debug("submitURL: " + urlstring); 086 URL url = new URL(urlstring); 087 088 String message = "POST " + url.getFile() + " HTTP/1.0\r\n" + 089 "Content-Length: " + contentBytes.length + "\r\n" + 090 "Cache-Control: no-cache\r\n" + 091 "Pragma: no-cache\r\n" + 092 "User-Agent: Java/1.4.2; EDocLitePostProcessor\r\n" + 093 "Host: " + url.getHost() + "\r\n" + 094 "Connection: close\r\n" + 095 "Content-Type: application/x-www-form-urlencoded\r\n\r\n" + 096 content; 097 098 byte[] buf = message.getBytes("UTF-8"); 099 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}