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 }