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 java.io.StringWriter;
019import java.util.Collection;
020import java.util.Date;
021import java.util.Map;
022
023import javax.xml.parsers.DocumentBuilder;
024import javax.xml.parsers.DocumentBuilderFactory;
025import javax.xml.parsers.ParserConfigurationException;
026import javax.xml.transform.Templates;
027import javax.xml.transform.TransformerConfigurationException;
028import javax.xml.transform.TransformerException;
029import javax.xml.transform.TransformerFactory;
030import javax.xml.transform.dom.DOMSource;
031import javax.xml.transform.stream.StreamResult;
032import javax.xml.transform.stream.StreamSource;
033
034import org.apache.commons.lang.StringUtils;
035import org.apache.log4j.Logger;
036import org.kuali.rice.core.api.mail.EmailContent;
037import org.kuali.rice.coreservice.api.style.StyleService;
038import org.kuali.rice.core.api.util.RiceConstants;
039import org.kuali.rice.core.api.util.xml.XmlHelper;
040import org.kuali.rice.core.api.util.xml.XmlJotter;
041import org.kuali.rice.kew.api.WorkflowRuntimeException;
042import org.kuali.rice.kew.api.action.ActionItem;
043import org.kuali.rice.kew.api.util.CodeTranslator;
044import org.kuali.rice.kew.doctype.bo.DocumentType;
045import org.kuali.rice.kew.feedback.web.FeedbackForm;
046import org.kuali.rice.kew.mail.CustomEmailAttribute;
047import org.kuali.rice.kew.mail.EmailStyleHelper;
048import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
049import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
050import org.kuali.rice.kew.service.KEWServiceLocator;
051import org.kuali.rice.kew.user.UserUtils;
052import org.kuali.rice.kew.api.KewApiConstants;
053import org.kuali.rice.kim.api.identity.Person;
054import org.kuali.rice.kim.api.identity.principal.Principal;
055import org.kuali.rice.kim.api.services.KimApiServiceLocator;
056import org.kuali.rice.krad.util.GlobalVariables;
057import org.springframework.core.io.DefaultResourceLoader;
058import org.w3c.dom.Document;
059import org.w3c.dom.Element;
060import 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 */
071public 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}