View Javadoc

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