View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.mail.service.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.core.api.mail.EmailContent;
21  import org.kuali.rice.core.api.util.ClasspathOrFileResourceLoader;
22  import org.kuali.rice.core.api.util.RiceConstants;
23  import org.kuali.rice.core.api.util.xml.XmlHelper;
24  import org.kuali.rice.core.api.util.xml.XmlJotter;
25  import org.kuali.rice.coreservice.api.style.StyleService;
26  import org.kuali.rice.kew.api.KewApiConstants;
27  import org.kuali.rice.kew.api.WorkflowRuntimeException;
28  import org.kuali.rice.kew.api.action.ActionItem;
29  import org.kuali.rice.kew.api.util.CodeTranslator;
30  import org.kuali.rice.kew.doctype.bo.DocumentType;
31  import org.kuali.rice.kew.feedback.web.FeedbackForm;
32  import org.kuali.rice.kew.mail.CustomEmailAttribute;
33  import org.kuali.rice.kew.mail.EmailStyleHelper;
34  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
35  import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
36  import org.kuali.rice.kew.service.KEWServiceLocator;
37  import org.kuali.rice.kew.user.UserUtils;
38  import org.kuali.rice.kim.api.group.Group;
39  import org.kuali.rice.kim.api.identity.Person;
40  import org.kuali.rice.kim.api.identity.principal.Principal;
41  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
42  import org.kuali.rice.krad.util.GlobalVariables;
43  import org.kuali.rice.krad.util.ObjectUtils;
44  import org.w3c.dom.Document;
45  import org.w3c.dom.Element;
46  import org.w3c.dom.Node;
47  
48  import javax.xml.parsers.DocumentBuilder;
49  import javax.xml.parsers.DocumentBuilderFactory;
50  import javax.xml.parsers.ParserConfigurationException;
51  import javax.xml.transform.Templates;
52  import javax.xml.transform.TransformerConfigurationException;
53  import javax.xml.transform.TransformerException;
54  import javax.xml.transform.TransformerFactory;
55  import javax.xml.transform.dom.DOMSource;
56  import javax.xml.transform.stream.StreamResult;
57  import javax.xml.transform.stream.StreamSource;
58  import java.io.StringWriter;
59  import java.util.Collection;
60  import java.util.Date;
61  import java.util.Map;
62  
63  
64  
65  /**
66   * EmailContentService that serves EmailContent customizable via XSLT style sheets
67   * The global email style name is: kew.email.style
68   * If this style is not found, the resource 'defaultEmailStyle.xsl' will be retrieved
69   * relative to this class.
70   * @author Kuali Rice Team (rice.collab@kuali.org)
71   */
72  public class StyleableEmailContentServiceImpl extends BaseEmailContentServiceImpl {
73      private static final Logger LOG = Logger.getLogger(StyleableEmailContentServiceImpl.class);
74  
75      protected final String DEFAULT_EMAIL_STYLESHEET_RESOURCE_LOC = "defaultEmailStyle.xsl";
76  
77      protected StyleService styleService;
78      protected EmailStyleHelper styleHelper = new EmailStyleHelper();
79      protected String globalEmailStyleSheet = KewApiConstants.EMAIL_STYLESHEET_NAME;
80  
81      protected RouteHeaderService routeHeaderService;
82  
83      public void setStyleService(StyleService styleService) {
84          this.styleService = styleService;
85      }
86  
87      public void setGlobalEmailStyleSheet(String globalEmailStyleSheet) {
88          this.globalEmailStyleSheet = globalEmailStyleSheet;
89      }
90  
91      protected static DocumentBuilder getDocumentBuilder(boolean coalesce) {
92          try {
93              DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
94              dbf.setCoalescing(coalesce);
95              return dbf.newDocumentBuilder();
96          } catch (ParserConfigurationException e) {
97              String message = "Error constructing document builder";
98              LOG.error(message, e);
99              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 }