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