View Javadoc

1   /**
2    * Copyright 2005-2012 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 java.io.StringWriter;
19  import java.util.Collection;
20  import java.util.Date;
21  import java.util.Map;
22  
23  import javax.xml.parsers.DocumentBuilder;
24  import javax.xml.parsers.DocumentBuilderFactory;
25  import javax.xml.parsers.ParserConfigurationException;
26  import javax.xml.transform.Templates;
27  import javax.xml.transform.TransformerConfigurationException;
28  import javax.xml.transform.TransformerException;
29  import javax.xml.transform.TransformerFactory;
30  import javax.xml.transform.dom.DOMSource;
31  import javax.xml.transform.stream.StreamResult;
32  import javax.xml.transform.stream.StreamSource;
33  
34  import org.apache.commons.lang.StringUtils;
35  import org.apache.log4j.Logger;
36  import org.kuali.rice.core.api.mail.EmailContent;
37  import org.kuali.rice.coreservice.api.style.StyleService;
38  import org.kuali.rice.core.api.util.RiceConstants;
39  import org.kuali.rice.core.api.util.xml.XmlHelper;
40  import org.kuali.rice.core.api.util.xml.XmlJotter;
41  import org.kuali.rice.kew.api.WorkflowRuntimeException;
42  import org.kuali.rice.kew.api.action.ActionItem;
43  import org.kuali.rice.kew.api.util.CodeTranslator;
44  import org.kuali.rice.kew.doctype.bo.DocumentType;
45  import org.kuali.rice.kew.feedback.web.FeedbackForm;
46  import org.kuali.rice.kew.mail.CustomEmailAttribute;
47  import org.kuali.rice.kew.mail.EmailStyleHelper;
48  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
49  import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
50  import org.kuali.rice.kew.service.KEWServiceLocator;
51  import org.kuali.rice.kew.user.UserUtils;
52  import org.kuali.rice.kew.api.KewApiConstants;
53  import org.kuali.rice.kim.api.identity.Person;
54  import org.kuali.rice.kim.api.identity.principal.Principal;
55  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
56  import org.kuali.rice.krad.util.GlobalVariables;
57  import org.springframework.core.io.DefaultResourceLoader;
58  import org.w3c.dom.Document;
59  import org.w3c.dom.Element;
60  import org.w3c.dom.Node;
61  
62  
63  
64  /**
65   * EmailContentService that serves EmailContent customizable via XSLT style sheets
66   * The global email style name is: kew.email.style
67   * If this style is not found, the resource 'defaultEmailStyle.xsl' will be retrieved
68   * relative to this class.
69   * @author Kuali Rice Team (rice.collab@kuali.org)
70   */
71  public class StyleableEmailContentServiceImpl extends BaseEmailContentServiceImpl {
72      private static final Logger LOG = Logger.getLogger(StyleableEmailContentServiceImpl.class);
73  
74      protected final String DEFAULT_EMAIL_STYLESHEET_RESOURCE_LOC = "defaultEmailStyle.xsl";
75  
76      protected StyleService styleService;
77      protected EmailStyleHelper styleHelper = new EmailStyleHelper();
78      protected String globalEmailStyleSheet = KewApiConstants.EMAIL_STYLESHEET_NAME;
79  
80      protected RouteHeaderService routeHeaderService;
81  
82      public void setStyleService(StyleService styleService) {
83          this.styleService = styleService;
84      }
85  
86      public void setGlobalEmailStyleSheet(String globalEmailStyleSheet) {
87          this.globalEmailStyleSheet = globalEmailStyleSheet;
88      }
89  
90      protected static DocumentBuilder getDocumentBuilder(boolean coalesce) {
91          try {
92              DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
93              dbf.setCoalescing(coalesce);
94              return dbf.newDocumentBuilder();
95          } catch (ParserConfigurationException e) {
96              String message = "Error constructing document builder";
97              LOG.error(message, e);
98              throw new WorkflowRuntimeException(message, e);
99          }
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     	
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     	/**
422     	 * End IU customization
423     	 */
424         return generateEmailContent(styleSheet, doc);
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 }