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