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}