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