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     */
016    package org.kuali.rice.kew.mail.service.impl;
017    
018    import java.io.StringWriter;
019    import java.util.Collection;
020    import java.util.Date;
021    import java.util.Map;
022    
023    import javax.xml.parsers.DocumentBuilder;
024    import javax.xml.parsers.DocumentBuilderFactory;
025    import javax.xml.parsers.ParserConfigurationException;
026    import javax.xml.transform.Templates;
027    import javax.xml.transform.TransformerConfigurationException;
028    import javax.xml.transform.TransformerException;
029    import javax.xml.transform.TransformerFactory;
030    import javax.xml.transform.dom.DOMSource;
031    import javax.xml.transform.stream.StreamResult;
032    import javax.xml.transform.stream.StreamSource;
033    
034    import org.apache.commons.lang.StringUtils;
035    import org.apache.log4j.Logger;
036    import org.kuali.rice.core.api.mail.EmailContent;
037    import org.kuali.rice.coreservice.api.style.StyleService;
038    import org.kuali.rice.core.api.util.RiceConstants;
039    import org.kuali.rice.core.api.util.xml.XmlHelper;
040    import org.kuali.rice.core.api.util.xml.XmlJotter;
041    import org.kuali.rice.kew.api.WorkflowRuntimeException;
042    import org.kuali.rice.kew.api.action.ActionItem;
043    import org.kuali.rice.kew.api.util.CodeTranslator;
044    import org.kuali.rice.kew.doctype.bo.DocumentType;
045    import org.kuali.rice.kew.feedback.web.FeedbackForm;
046    import org.kuali.rice.kew.mail.CustomEmailAttribute;
047    import org.kuali.rice.kew.mail.EmailStyleHelper;
048    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
049    import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
050    import org.kuali.rice.kew.service.KEWServiceLocator;
051    import org.kuali.rice.kew.user.UserUtils;
052    import org.kuali.rice.kew.api.KewApiConstants;
053    import org.kuali.rice.kim.api.identity.Person;
054    import org.kuali.rice.kim.api.identity.principal.Principal;
055    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
056    import org.kuali.rice.krad.util.GlobalVariables;
057    import org.springframework.core.io.DefaultResourceLoader;
058    import org.w3c.dom.Document;
059    import org.w3c.dom.Element;
060    import org.w3c.dom.Node;
061    
062    
063    
064    /**
065     * EmailContentService that serves EmailContent customizable via XSLT style sheets
066     * The global email style name is: kew.email.style
067     * If this style is not found, the resource 'defaultEmailStyle.xsl' will be retrieved
068     * relative to this class.
069     * @author Kuali Rice Team (rice.collab@kuali.org)
070     */
071    public class StyleableEmailContentServiceImpl extends BaseEmailContentServiceImpl {
072        private static final Logger LOG = Logger.getLogger(StyleableEmailContentServiceImpl.class);
073    
074        protected final String DEFAULT_EMAIL_STYLESHEET_RESOURCE_LOC = "defaultEmailStyle.xsl";
075    
076        protected StyleService styleService;
077        protected EmailStyleHelper styleHelper = new EmailStyleHelper();
078        protected String globalEmailStyleSheet = KewApiConstants.EMAIL_STYLESHEET_NAME;
079    
080        protected RouteHeaderService routeHeaderService;
081    
082        public void setStyleService(StyleService styleService) {
083            this.styleService = styleService;
084        }
085    
086        public void setGlobalEmailStyleSheet(String globalEmailStyleSheet) {
087            this.globalEmailStyleSheet = globalEmailStyleSheet;
088        }
089    
090        protected static DocumentBuilder getDocumentBuilder(boolean coalesce) {
091            try {
092                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
093                dbf.setCoalescing(coalesce);
094                return dbf.newDocumentBuilder();
095            } catch (ParserConfigurationException e) {
096                String message = "Error constructing document builder";
097                LOG.error(message, e);
098                throw new WorkflowRuntimeException(message, e);
099            }
100        }
101    
102        protected static void addObjectXML(Document doc, Object o, Node node, String name) throws Exception {
103            Element element = XmlHelper.propertiesToXml(doc, o, name);
104    
105            if (LOG.isDebugEnabled()) {
106                LOG.debug(XmlJotter.jotNode(element));
107            }
108    
109            if (node == null) {
110                node = doc;
111            }
112    
113            node.appendChild(element);
114        }
115    
116        protected static void addTextElement(Document doc, Element baseElement, String elementName, Object elementData) {
117            Element element = doc.createElement(elementName);
118            String dataValue = "";
119            if (elementData != null) {
120                    dataValue = elementData.toString();
121            }
122            element.appendChild(doc.createTextNode(dataValue));
123            baseElement.appendChild(element);
124        }
125    
126        protected static void addCDataElement(Document doc, Element baseElement, String elementName, Object elementData) {
127            Element element = doc.createElement(elementName);
128            String dataValue = "";
129            if (elementData != null) {
130                dataValue = elementData.toString();
131            }
132            element.appendChild(doc.createCDATASection(dataValue));
133            baseElement.appendChild(element);
134        }
135    
136        protected static void addTimestampElement(Document doc, Element baseElement, String elementName, Date elementData) {
137            addTextElement(doc, baseElement, elementName, RiceConstants.getDefaultDateFormat().format(elementData));
138        }
139    
140        protected static void addDelegatorElement(Document doc, Element baseElement, ActionItem actionItem) {
141            Element delegatorElement = doc.createElement("delegator");
142            if ( (actionItem.getDelegatorPrincipalId() != null) && (actionItem.getDelegatorPrincipalId() != null) ) {
143                // add empty delegator element
144                baseElement.appendChild(delegatorElement);
145                return;
146            }
147            String delegatorType = "";
148            String delegatorId = "";
149            String delegatorDisplayValue = "";
150            if (actionItem.getDelegatorPrincipalId() != null) {
151                delegatorType = "user";
152                delegatorId = actionItem.getDelegatorPrincipalId();
153                Principal delegator = KimApiServiceLocator.getIdentityService().getPrincipal(delegatorId);
154                
155                if (delegator == null) {
156                    LOG.error("Cannot find user for id " + delegatorId);
157                    delegatorDisplayValue = "USER NOT FOUND";
158                } else {
159                    delegatorDisplayValue = UserUtils.getTransposedName(GlobalVariables.getUserSession(), delegator);
160                }
161            } else if (actionItem.getDelegatorPrincipalId() != null) {
162                delegatorType = "workgroup";
163                delegatorId = actionItem.getDelegatorGroupId().toString();
164                delegatorDisplayValue = KimApiServiceLocator.getGroupService().getGroup(actionItem.getDelegatorGroupId()).getName();
165            }
166            delegatorElement.setAttribute("type", delegatorType);
167            // add the id element
168            Element idElement = doc.createElement("id");
169            idElement.appendChild(doc.createTextNode(delegatorId));
170            delegatorElement.appendChild(idElement);
171            // add the display value element
172            Element displayValElement = doc.createElement("displayValue");
173            displayValElement.appendChild(doc.createTextNode(delegatorDisplayValue));
174            delegatorElement.appendChild(displayValElement);
175            baseElement.appendChild(delegatorElement);
176        }
177    
178        protected static void addWorkgroupRequestElement(Document doc, Element baseElement, ActionItem actionItem) {
179            Element workgroupElement = doc.createElement("workgroupRequest");
180            if (actionItem.getGroupId() != null) {
181                // add the id element
182                Element idElement = doc.createElement("id");
183                idElement.appendChild(doc.createTextNode(actionItem.getGroupId()));
184                workgroupElement.appendChild(idElement);
185                // add the display value element
186                Element displayValElement = doc.createElement("displayValue");
187                displayValElement.appendChild(doc.createTextNode(actionItem.getGroupId()));
188                workgroupElement.appendChild(displayValElement);
189            }
190            baseElement.appendChild(workgroupElement);
191        }
192    
193        /**
194         * This method is used to add the given {@link ActionItem} to the given {@link org.w3c.dom.Document} in a summarized
195         * form for use in weekly or daily type reminder e-mails.
196         *
197         * @param doc - Document to have the ActionItem added to
198         * @param actionItem - the action item being added
199         * @param user - the current user
200         * @param node - the node object to add the actionItem XML to (defaults to the doc variable if null is passed in)
201         * @throws Exception
202         */
203        protected void addSummarizedActionItem(Document doc, ActionItem actionItem, Person user, Node node, DocumentRouteHeaderValue routeHeader) throws Exception {
204            if (node == null) {
205                node = doc;
206            }
207    
208            Element root = doc.createElement("summarizedActionItem");
209    
210            // add in all items from action list as preliminary default dataset
211            addTextElement(doc, root, "documentId", actionItem.getDocumentId());
212            addTextElement(doc, root, "docName", actionItem.getDocName());
213            addCDataElement(doc, root, "docLabel", actionItem.getDocLabel());
214            addCDataElement(doc, root, "docTitle", actionItem.getDocTitle());
215            //DocumentRouteHeaderValue routeHeader = getRouteHeader(actionItem);
216            addTextElement(doc, root, "docRouteStatus", routeHeader.getDocRouteStatus());
217            addCDataElement(doc, root, "routeStatusLabel", routeHeader.getRouteStatusLabel());
218            addTextElement(doc, root, "actionRequestCd", actionItem.getActionRequestCd());
219            addTextElement(doc, root, "actionRequestLabel", CodeTranslator.getActionRequestLabel(
220                    actionItem.getActionRequestCd()));
221            addDelegatorElement(doc, root, actionItem);
222            addTimestampElement(doc, root, "createDate", routeHeader.getCreateDate());
223            addWorkgroupRequestElement(doc, root, actionItem);
224            if (actionItem.getDateTimeAssigned() != null)
225                addTimestampElement(doc, root, "dateAssigned", actionItem.getDateTimeAssigned().toDate());
226    
227            node.appendChild(root);
228        }
229    
230        public DocumentRouteHeaderValue getRouteHeader(ActionItem actionItem) {
231            if (routeHeaderService == null) {
232                    routeHeaderService = KEWServiceLocator.getRouteHeaderService();
233            }
234            return routeHeaderService.getRouteHeader(actionItem.getDocumentId());
235        }
236    
237        protected Map<String,DocumentRouteHeaderValue> getRouteHeaders(Collection<ActionItem> actionItems) {
238            if (routeHeaderService == null) {
239                    routeHeaderService = KEWServiceLocator.getRouteHeaderService();
240            }
241            return routeHeaderService.getRouteHeadersForActionItems(actionItems);
242        }
243        
244        protected static String transform(Templates style, Document doc) {
245            StringWriter writer = new StringWriter();
246            StreamResult result = new StreamResult(writer);
247    
248            try {
249                style.newTransformer().transform(new DOMSource(doc), result);
250                return writer.toString();
251            } catch (TransformerException te) {
252                String message = "Error transforming DOM";
253                LOG.error(message, te);
254                throw new WorkflowRuntimeException(message, te);
255            }
256        }
257    
258        /**
259         * This method retrieves the style from the system using the given name. If none is found the default style xsl file
260         * defined by {@link #DEFAULT_EMAIL_STYLESHEET_RESOURCE_LOC} is used.
261         *
262         * @param styleName
263         * @return a valid {@link javax.xml.transform.Templates} using either the given styleName or the default xsl style file
264         */
265        protected Templates getStyle(String styleName) {
266            Templates style = null;
267            try {
268                style = styleService.getStyleAsTranslet(styleName);
269            } catch (TransformerConfigurationException tce) {
270                String message = "Error obtaining style '" + styleName + "', using default";
271                LOG.error(message, tce);
272                // throw new WorkflowRuntimeException("Error obtaining style '" + styleName + "'", tce);
273            }
274    
275            if (style == null) {
276                LOG.warn("Could not find specified style, " + styleName + ", using default");
277                try {
278    
279                    style = TransformerFactory.newInstance().newTemplates(new StreamSource(new DefaultResourceLoader().getResource("classpath:org/kuali/rice/kew/mail/" + DEFAULT_EMAIL_STYLESHEET_RESOURCE_LOC).getInputStream()));
280                } catch (Exception tce) {
281                    String message = "Error obtaining default style from resource: " + DEFAULT_EMAIL_STYLESHEET_RESOURCE_LOC;
282                    LOG.error(message, tce);
283                    throw new WorkflowRuntimeException("Error obtaining style '" + styleName + "'", tce);
284                }
285            }
286            return style;
287        }
288    
289        protected EmailContent generateEmailContent(String styleName, Document doc) {
290            Templates style = getStyle(styleName);
291            return styleHelper.generateEmailContent(style, doc);
292        }
293    
294        protected EmailContent generateReminderForActionItems(Person user, Collection<ActionItem> actionItems, String name, String style) {
295            DocumentBuilder db = getDocumentBuilder(false);
296            Document doc = db.newDocument();
297            Element element = doc.createElement(name);
298            Map<String,DocumentRouteHeaderValue> routeHeaders = getRouteHeaders(actionItems);
299            
300            setStandardAttributes(element);
301            doc.appendChild(element);
302    
303            try {
304                addObjectXML(doc, user, element, "user");
305                for (ActionItem actionItem: actionItems) {
306                    try {
307                        addSummarizedActionItem(doc, actionItem, user, element, routeHeaders.get(actionItem.getDocumentId()));
308                    } catch (Exception e) {
309                        String message = "Error generating XML for action item: " + actionItem;
310                        LOG.error(message, e);
311                        throw new WorkflowRuntimeException(e);
312                    }
313                }
314    
315            } catch (Exception e) {
316                String message = "Error generating XML for action items: " + actionItems;
317                LOG.error(message, e);
318                throw new WorkflowRuntimeException(e);
319            }
320    
321            return generateEmailContent(style, doc);
322        }
323    
324        protected void setStandardAttributes(Element e) {
325            e.setAttribute("env", getDeploymentEnvironment());
326            e.setAttribute("applicationEmailAddress", getApplicationEmailAddress());
327            e.setAttribute("actionListUrl", getActionListUrl());
328            e.setAttribute("preferencesUrl", getPreferencesUrl());
329        }
330    
331        /**
332         * This method generates an {@link EmailContent} object using the given parameters.  Part of this operation includes
333         * serializing the given {@link ActionItem} to XML. The following objects and methods are included in the serialization:
334         *
335         * <ul>
336         * <li>{@link Person}</li>
337         * <li>{@link Person#getPrincipalName()}</li>
338         * <li>{@link DocumentRouteHeaderValue}</li>
339         * <li>{@link DocumentRouteHeaderValue#getInitiatorUser()}</li>
340         * <li>{@link DocumentRouteHeaderValue#getDocumentType()}</li>
341         * <li>{@link Person}</li>
342         * </ul>
343         *
344         * @param user - the current user
345         * @param actionItem - the action item being added
346         * @param documentType - the document type that the custom email style sheet will come from
347         * @param node - the node object to add the actionItem XML to (defaults to the doc variable if null is passed in)
348         * @throws Exception
349         */
350        @Override
351            public EmailContent generateImmediateReminder(Person user, ActionItem actionItem, DocumentType documentType) {
352            if (user != null) {
353                LOG.info("Starting generation of immediate email reminder...");
354                LOG.info("Action Id: " + actionItem.getId() +
355                             ";  ActionRequestId: " + actionItem.getActionRequestId() + 
356                             ";  Action Item Principal Id: " + actionItem.getPrincipalId());
357                LOG.info("User Principal Id: " + user.getPrincipalId());
358                // change style name based on documentType when configurable email style on document is implemented...
359                String styleSheet = documentType.getCustomEmailStylesheet();
360                LOG.debug(documentType.getName() + " style: " + styleSheet);
361                if (styleSheet == null) {
362                    styleSheet = globalEmailStyleSheet;
363                }
364    
365                LOG.info("generateImmediateReminder using style sheet: "+ styleSheet + " for Document Type " + documentType.getName());
366                // return generateReminderForActionItems(user, actionItems, "immediateReminder", styleSheet);
367                DocumentBuilder db = getDocumentBuilder(false);
368                Document doc = db.newDocument();
369                Element element = doc.createElement("immediateReminder");
370                setStandardAttributes(element);
371                doc.appendChild(element);
372    
373                try {
374                    addObjectXML(doc, user, element, "user");
375                    // addActionItem(doc, actionItem, user, node);
376                    Node node = element;
377                    if (node == null) {
378                        node = doc;
379                    }
380    
381                    Element root = doc.createElement("actionItem");
382                    // append the custom body and subject if they exist
383                    try {
384                        CustomEmailAttribute customEmailAttribute = getCustomEmailAttribute(user, actionItem);
385                        if (customEmailAttribute != null) {
386                            String customBody = customEmailAttribute.getCustomEmailBody();
387                            if (!org.apache.commons.lang.StringUtils.isEmpty(customBody)) {
388                                Element bodyElement = doc.createElement("customBody");
389                                bodyElement.appendChild(doc.createTextNode(customBody));
390                                root.appendChild(bodyElement);
391                            }
392                            String customEmailSubject = customEmailAttribute.getCustomEmailSubject();
393                            if (!org.apache.commons.lang.StringUtils.isEmpty(customEmailSubject)) {
394                                Element subjectElement = doc.createElement("customSubject");
395                                subjectElement.appendChild(doc.createTextNode(customEmailSubject));
396                                root.appendChild(subjectElement);
397                            }
398                        }
399                    } catch (Exception e) {
400                        LOG.error("Error when checking for custom email body and subject.", e);
401                    }
402                    Person person = KimApiServiceLocator.getPersonService().getPerson(actionItem.getPrincipalId());
403                    DocumentRouteHeaderValue header = getRouteHeader(actionItem);
404                    // keep adding stuff until we have all the xml we need to formulate the message :/
405                    addObjectXML(doc, actionItem, root, "actionItem");
406                    addObjectXML(doc, person, root, "actionItemPerson");
407                    addTextElement(doc, root, "actionItemPrincipalId", person.getPrincipalId());
408                    addTextElement(doc, root, "actionItemPrincipalName", person.getPrincipalName());
409                    addDocumentHeaderXML(doc, header, root, "doc");
410                    addObjectXML(doc, header.getInitiatorPrincipal(), root, "docInitiator");
411                    addTextElement(doc, root, "docInitiatorDisplayName", header.getInitiatorDisplayName());
412                    addObjectXML(doc, header.getDocumentType(), root, "documentType");
413    
414                    node.appendChild(root);
415                } catch (Exception e) {
416                    String message = "Error generating immediate reminder XML for action item: " + actionItem;
417                    LOG.error(message, e);
418                    throw new WorkflowRuntimeException(e);
419                }
420                LOG.info("Leaving generation of immeidate email reminder...");
421                return generateEmailContent(styleSheet, doc);
422            }
423            LOG.info("Skipping generation of immediate email reminder due to the user being null");
424            return null;
425        }
426        
427        /**
428         * This method handles converting the DocumentRouteHeaderValue into an XML representation.  The reason we can't just use
429         * propertiesToXml like we have elsewhere is because the doc header has a String attached to it that has the XML document
430         * content in it.  The default serialization of this will serialize this as a String so we will end up with escaped XML
431         * in our output which we won't be able to process with the email stylesheet.  So we need to read the xml content from
432         * the document and parse it into a DOM object so it can be appended to our output.
433         */
434        protected void addDocumentHeaderXML(Document document, DocumentRouteHeaderValue documentHeader, Node node, String elementName) throws Exception {
435            Element element = XmlHelper.propertiesToXml(document, documentHeader, elementName);
436            // now we need to "fix" the xml document content because it's going to be in there as escaped XML
437            Element docContentElement = (Element)element.getElementsByTagName("docContent").item(0);
438            String documentContent = docContentElement.getTextContent();
439            
440            if (!StringUtils.isBlank(documentContent) && documentContent.startsWith("<")) {
441                    Document documentContentXML = XmlHelper.readXml(documentContent);
442                    Element documentContentElement = documentContentXML.getDocumentElement();
443                    documentContentElement = (Element)document.importNode(documentContentElement, true);
444            
445                    // remove the old, bad text content
446                    docContentElement.removeChild(docContentElement.getFirstChild());
447            
448                    // replace with actual XML
449                    docContentElement.appendChild(documentContentElement);
450            } else {
451                // in this case it means that the XML is encrypted, unfortunately, we have no way to decrypt it since
452                // the key is stored in the client application.  We will just include the doc content since none of our
453                // current IU clients will be using this feature right away
454    
455                // remove the old, bad text content
456                docContentElement.removeChild(docContentElement.getFirstChild());
457            }
458            
459            if (LOG.isDebugEnabled()) {
460                LOG.debug(XmlJotter.jotNode(element));
461            }
462    
463            node.appendChild(element);
464        }
465    
466        @Override
467            public EmailContent generateWeeklyReminder(Person user, Collection<ActionItem> actionItems) {
468            return generateReminderForActionItems(user, actionItems, "weeklyReminder", globalEmailStyleSheet);
469        }
470    
471        @Override
472            public EmailContent generateDailyReminder(Person user, Collection<ActionItem> actionItems) {
473            return generateReminderForActionItems(user, actionItems, "dailyReminder", globalEmailStyleSheet);
474        }
475    
476        @Override
477            public EmailContent generateFeedback(FeedbackForm form) {
478            DocumentBuilder db = getDocumentBuilder(true);
479            Document doc = db.newDocument();
480            String styleSheet = globalEmailStyleSheet;
481    
482            // if the doc type is specified, see if that doc has a custom email stylesheet and use it
483            // NOTE: do we need to do this for feedback? presumably feedback will be going back to admins
484            /*String docTypeName = form.getDocumentType();
485            if (!StringUtils.isBlank(docTypeName)) {
486                DocumentType docType = KEWServiceLocator.getDocumentTypeService().findByName(docTypeName);
487                if (docType == null) {
488                    LOG.error("User specified document type '" + docTypeName + "' in feedback form, but the document type was not found in the system");
489                } else {
490                    if (docType.getCustomEmailStylesheet() != null) {
491                        styleSheet = docType.getCustomEmailStylesheet();
492                    }
493                }
494            }*/
495            LOG.info("form: " + form.getDocumentId());
496            try {
497                addObjectXML(doc, form, null, "feedback");
498            } catch (Exception e) {
499                String message = "Error generating XML for feedback form: " + form;
500                LOG.error(message, e);
501                throw new WorkflowRuntimeException(message, e);
502            }
503            setStandardAttributes(doc.getDocumentElement());
504    
505            return generateEmailContent(styleSheet, doc);
506        }
507    }