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}