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