Coverage Report - org.kuali.rice.ken.service.impl.NotificationMessageContentServiceImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
NotificationMessageContentServiceImpl
0%
0/215
0%
0/52
6.444
 
 1  
 /**
 2  
  * Copyright 2005-2011 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.ken.service.impl;
 17  
 
 18  
 import com.thoughtworks.xstream.XStream;
 19  
 import com.thoughtworks.xstream.io.xml.DomDriver;
 20  
 import org.apache.commons.io.IOUtils;
 21  
 import org.apache.commons.lang.StringUtils;
 22  
 import org.apache.log4j.Logger;
 23  
 import org.kuali.rice.core.api.util.xml.XmlException;
 24  
 import org.kuali.rice.core.api.util.xml.XmlJotter;
 25  
 import org.kuali.rice.core.framework.persistence.dao.GenericDao;
 26  
 import org.kuali.rice.ken.bo.Notification;
 27  
 import org.kuali.rice.ken.bo.NotificationChannel;
 28  
 import org.kuali.rice.ken.bo.NotificationContentType;
 29  
 import org.kuali.rice.ken.bo.NotificationPriority;
 30  
 import org.kuali.rice.ken.bo.NotificationProducer;
 31  
 import org.kuali.rice.ken.bo.NotificationRecipient;
 32  
 import org.kuali.rice.ken.bo.NotificationResponse;
 33  
 import org.kuali.rice.ken.bo.NotificationSender;
 34  
 import org.kuali.rice.ken.service.NotificationContentTypeService;
 35  
 import org.kuali.rice.ken.service.NotificationMessageContentService;
 36  
 import org.kuali.rice.ken.util.CompoundNamespaceContext;
 37  
 import org.kuali.rice.ken.util.ConfiguredNamespaceContext;
 38  
 import org.kuali.rice.ken.util.NotificationConstants;
 39  
 import org.kuali.rice.ken.util.Util;
 40  
 import org.kuali.rice.kew.util.Utilities;
 41  
 import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
 42  
 import org.kuali.rice.kim.api.services.KimApiServiceLocator;
 43  
 import org.w3c.dom.Document;
 44  
 import org.w3c.dom.Element;
 45  
 import org.w3c.dom.Node;
 46  
 import org.w3c.dom.NodeList;
 47  
 import org.xml.sax.InputSource;
 48  
 import org.xml.sax.SAXException;
 49  
 
 50  
 import javax.xml.parsers.ParserConfigurationException;
 51  
 import javax.xml.xpath.XPath;
 52  
 import javax.xml.xpath.XPathConstants;
 53  
 import javax.xml.xpath.XPathExpressionException;
 54  
 import javax.xml.xpath.XPathFactory;
 55  
 import java.io.ByteArrayInputStream;
 56  
 import java.io.IOException;
 57  
 import java.io.InputStream;
 58  
 import java.sql.Timestamp;
 59  
 import java.text.DateFormat;
 60  
 import java.text.ParseException;
 61  
 import java.text.SimpleDateFormat;
 62  
 import java.util.ArrayList;
 63  
 import java.util.Date;
 64  
 import java.util.HashMap;
 65  
 import java.util.List;
 66  
 import java.util.Map;
 67  
 
 68  
 /**
 69  
  * NotificationMessageContentService implementation - uses both Xalan and XStream in various places to manage the marshalling/unmarshalling of
 70  
  * Notification data for processing by various components in the system.
 71  
  * @see NotificationMessageContentService
 72  
  * @author Kuali Rice Team (rice.collab@kuali.org)
 73  
  */
 74  
 public class NotificationMessageContentServiceImpl implements NotificationMessageContentService {
 75  0
     private static final Logger LOG = Logger.getLogger(NotificationMessageContentServiceImpl.class);
 76  
 
 77  
     /**
 78  
      * Prefix that content type schemas should start with
 79  
      */
 80  
     static final String CONTENT_TYPE_NAMESPACE_PREFIX = "ns:notification/ContentType";
 81  
 
 82  
     // Date format of current timezone necessary for intra-system XML parsing via send form
 83  0
     private static final DateFormat DATEFORMAT_CURR_TZ = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
 84  
 
 85  
     /**
 86  
      * Our BusinessObjectDao persistence layer
 87  
      */
 88  
     private GenericDao boDao;
 89  
     /**
 90  
      * NotificationContentTypeService impl
 91  
      */
 92  
     private NotificationContentTypeService notificationContentTypeService;
 93  
 
 94  
     /**
 95  
      * Constructor which takes a GenericDao
 96  
      * Constructs a NotificationMessageContentServiceImpl.java.
 97  
      * @param boDao
 98  
      */
 99  0
     public NotificationMessageContentServiceImpl(GenericDao boDao,  NotificationContentTypeService notificationContentTypeService) {
 100  0
         this.boDao = boDao;
 101  0
         this.notificationContentTypeService = notificationContentTypeService;
 102  0
     }
 103  
 
 104  
     /**
 105  
      * This method implements by taking in a String and then converting that to a byte[];
 106  
      * @see org.kuali.rice.ken.service.NotificationMessageContentService#parseNotificationRequestMessage(java.lang.String)
 107  
      */
 108  
     public Notification parseNotificationRequestMessage(String notificationMessageAsXml) throws IOException, XmlException {
 109  
         // this is sort of redundant...but DOM does not perform validation
 110  
         // so we have to read all the bytes and then hand them to DOM
 111  
         // after our first-pass validation, for a second parse
 112  0
         byte[] bytes = notificationMessageAsXml.getBytes();
 113  
 
 114  0
         return parseNotificationRequestMessage(bytes);
 115  
     }
 116  
 
 117  
     /**
 118  
      * This method implements by taking in an InputStream and then coverting that to a byte[].
 119  
      * @see org.kuali.rice.ken.service.NotificationMessageContentService#parseNotificationRequestMessage(java.io.InputStream)
 120  
      */
 121  
     public Notification parseNotificationRequestMessage(InputStream stream) throws IOException, XmlException {
 122  
         // this is sort of redundant...but DOM does not perform validation
 123  
         // so we have to read all the bytes and then hand them to DOM
 124  
         // after our first-pass validation, for a second parse
 125  0
         byte[] bytes = IOUtils.toByteArray(stream);
 126  
 
 127  0
         return parseNotificationRequestMessage(bytes);
 128  
     }
 129  
 
 130  
     /**
 131  
      * This method is the meat of the notification message parsing.  It uses DOM to parse out the notification
 132  
      * message XML and into a Notification BO.  It handles lookup of reference objects' primary keys so that it
 133  
      * can properly populate the notification object.
 134  
      * @param bytes
 135  
      * @return Notification
 136  
      * @throws IOException
 137  
      * @throws XmlException
 138  
      */
 139  
     private Notification parseNotificationRequestMessage(byte[] bytes) throws IOException, XmlException {
 140  
         /* First we'll fully parse the DOM with validation turned on */
 141  
         Document doc;
 142  
         try {
 143  0
             doc = Util.parseWithNotificationEntityResolver(new InputSource(new ByteArrayInputStream(bytes)), true, true, notificationContentTypeService);
 144  0
         } catch (ParserConfigurationException pce) {
 145  0
             throw new XmlException("Error obtaining XML parser", pce);
 146  0
         } catch (SAXException se) {
 147  0
             throw new XmlException("Error validating notification request", se);
 148  0
         }
 149  
 
 150  0
         Element root = doc.getDocumentElement();
 151  
         /* XPath is namespace-aware, so if the DOM that XPath will be evaluating has fully qualified elements
 152  
            (because, e.g., it has been parsed with a validating DOM parser as above, then we need to set a
 153  
            "NamespaceContext" which essentially declares the defined namespace mappings to XPath.
 154  
 
 155  
            Unfortunately there is no EASY way (that I have found at least) to automatically expose the namespaces
 156  
            that have been discovered in the XML document parsed into DOM to XPath (an oversight in my opinion as
 157  
            this requires duplicate footwork to re-expose known definitions).
 158  
 
 159  
            So what we do is create a set of helper classes that will expose both the "known" core Notification system
 160  
            namespaces, as well as those that can be derived from the DOM Document (Document exposes these but through a
 161  
            different API than XPath NamespaceContext).  We create CompoundNamespaceContext that consists of both of these
 162  
            constituent namespace contexts (so that our core NamespaceContext takes precedent...nobody should be redefining
 163  
            these!).
 164  
 
 165  
            We can *then* use fully qualified XPath expressions like: /nreq:notification/nreq:channel ...
 166  
 
 167  
            (Another alternative would be to REPARSE the incoming XML with validation turned off so we can have simpler XPath
 168  
            expresssions.  This is less correct, but also not ideal as we will want to use qualified XPath expressions with
 169  
            notification content type also)
 170  
          */
 171  0
         XPath xpath = XPathFactory.newInstance().newXPath();
 172  0
         xpath.setNamespaceContext(Util.getNotificationNamespaceContext(doc));
 173  
 
 174  
         /* First parse immediate/primitive Notification member data */
 175  0
         LOG.debug("URI: " + xpath.getNamespaceContext().getNamespaceURI("nreq"));
 176  
         try {
 177  0
             String channelName = (String) xpath.evaluate("/nreq:notification/nreq:channel", root);
 178  0
             LOG.debug("CHANNELNAME: "+ channelName);
 179  0
             String producerName = xpath.evaluate("/nreq:notification/nreq:producer", root);
 180  
 
 181  0
             List<String> senders = new ArrayList<String>();
 182  0
             NodeList nodes = (NodeList) xpath.evaluate("/nreq:notification/nreq:senders/nreq:sender", root, XPathConstants.NODESET);
 183  0
             for (int i = 0; i < nodes.getLength(); i++) {
 184  0
                 LOG.debug("sender node: " + nodes.item(i));
 185  0
                 LOG.debug("sender node VALUE: " + nodes.item(i).getTextContent());
 186  0
                 senders.add(nodes.item(i).getTextContent());
 187  
             }
 188  0
             nodes = (NodeList) xpath.evaluate("/nreq:notification/nreq:recipients/nreq:group|/nreq:notification/nreq:recipients/nreq:user", root, XPathConstants.NODESET);
 189  0
             List<NotificationRecipient> recipients = new ArrayList<NotificationRecipient>();
 190  0
             for (int i = 0; i < nodes.getLength(); i++) {
 191  0
                 Node node = nodes.item(i);
 192  0
                 NotificationRecipient recipient = new NotificationRecipient();
 193  
                 // NOTE: assumes validation has occurred; does not check validity of element name
 194  0
                 if (NotificationConstants.RECIPIENT_TYPES.GROUP.equalsIgnoreCase(node.getLocalName())) {
 195  
                     //recipient.setRecipientType(NotificationConstants.RECIPIENT_TYPES.GROUP);
 196  0
                     recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
 197  0
                     recipient.setRecipientId(KimApiServiceLocator.getGroupService().getGroupByNameAndNamespaceCode(
 198  
                             Utilities.parseGroupNamespaceCode(node.getTextContent()), Utilities.parseGroupName(
 199  
                             node.getTextContent())).getId());
 200  0
                 } else if (NotificationConstants.RECIPIENT_TYPES.USER.equalsIgnoreCase(node.getLocalName())){
 201  
                     //recipient.setRecipientType(NotificationConstants.RECIPIENT_TYPES.USER);
 202  0
                     recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
 203  0
                     recipient.setRecipientId(node.getTextContent());
 204  
                 } else {
 205  0
                     throw new XmlException("Invalid 'recipientType' value: '" + node.getLocalName() +
 206  
                             "'.  Needs to either be 'user' or 'group'");
 207  
                 }
 208  0
                 recipients.add(recipient);
 209  
             }
 210  
 
 211  0
             String deliveryType = xpath.evaluate("/nreq:notification/nreq:deliveryType", root);
 212  0
             String sendDateTime = xpath.evaluate("/nreq:notification/nreq:sendDateTime", root);
 213  0
             String autoRemoveDateTime = xpath.evaluate("/nreq:notification/nreq:autoRemoveDateTime", root);
 214  
 
 215  0
             String priorityName = xpath.evaluate("/nreq:notification/nreq:priority", root);
 216  0
             String title = xpath.evaluate("/nreq:notification/nreq:title", root);
 217  0
             String contentTypeName = xpath.evaluate("/nreq:notification/nreq:contentType", root);
 218  
 
 219  
             /* Construct the Notification business object */
 220  
 
 221  0
             Notification notification = new Notification();
 222  
 
 223  0
             if (!StringUtils.isBlank(title)) {
 224  0
                 notification.setTitle(title);
 225  
             }
 226  
 
 227  
             /* channel and producer require lookups in the system (i.e. we can't just create new instances out of whole cloth), so
 228  
                we call a helper method to retrieve references to the respective objects
 229  
              */
 230  0
             NotificationChannel channel = Util.retrieveFieldReference("channel", "name", channelName, NotificationChannel.class, boDao);
 231  0
             notification.setChannel(channel);
 232  
 
 233  0
             NotificationProducer producer = Util.retrieveFieldReference("producer", "name", producerName, NotificationProducer.class, boDao);
 234  0
             notification.setProducer(producer);
 235  
 
 236  0
             for (String sender: senders) {
 237  0
                 NotificationSender ns = new NotificationSender();
 238  0
                 LOG.debug("Setting sender: " + sender);
 239  0
                 ns.setSenderName(sender);
 240  0
                 notification.addSender(ns);
 241  0
             }
 242  
 
 243  0
             for (NotificationRecipient recipient: recipients) {
 244  0
                 LOG.debug("Setting recipient id: "+ recipient.getRecipientId());
 245  0
                 notification.addRecipient(recipient);
 246  
             }
 247  
 
 248  
             /* validate the delivery type */
 249  0
             if(!NotificationConstants.DELIVERY_TYPES.ACK.equalsIgnoreCase(deliveryType) &&
 250  
                !NotificationConstants.DELIVERY_TYPES.FYI.equalsIgnoreCase(deliveryType)) {
 251  0
                 throw new XmlException("Invalid 'deliveryType' value: '" + deliveryType +
 252  
                     "'.  Must be either 'ACK' or 'FYI'.");
 253  
             }
 254  0
             notification.setDeliveryType(deliveryType);
 255  
 
 256  
             /* If we have gotten this far, then these dates have obviously already passed XML schema validation,
 257  
                but as that may be volatile we make sure to validate programmatically.
 258  
              */
 259  
             Date d;
 260  0
             if(StringUtils.isNotBlank(sendDateTime)) {
 261  
                 try {
 262  0
                     d = Util.parseXSDDateTime(sendDateTime);
 263  0
                 } catch (ParseException pe) {
 264  0
                     throw new XmlException("Invalid 'sendDateTime' value: " + sendDateTime, pe);
 265  0
                 }
 266  0
                 notification.setSendDateTime(new Timestamp(d.getTime()));
 267  
             }
 268  0
             if(StringUtils.isNotBlank(autoRemoveDateTime)) {
 269  
                 try {
 270  0
                     d = Util.parseXSDDateTime(autoRemoveDateTime);
 271  0
                 } catch (ParseException pe) {
 272  0
                     throw new XmlException("Invalid 'autoRemoveDateTime' value: " + autoRemoveDateTime, pe);
 273  0
                 }
 274  0
                 notification.setAutoRemoveDateTime(new Timestamp(d.getTime()));
 275  
             }
 276  
 
 277  
 
 278  
             /* we have to look up priority and content type in the system also */
 279  0
             NotificationPriority priority = Util.retrieveFieldReference("priority", "name", priorityName, NotificationPriority.class, boDao);
 280  0
             notification.setPriority(priority);
 281  
 
 282  0
             NotificationContentType contentType = Util.retrieveFieldReference("contentType", "name", contentTypeName, NotificationContentType.class, boDao);
 283  0
             notification.setContentType(contentType);
 284  
 
 285  
             /* Now handle and validate actual notification content.  This is a tricky part.
 286  
                Our job is to validate the incoming content xml blob.  However that content could be under ANY namespace
 287  
                (since we have pluggable content types).  So how can we construct an XPath expression, that refers to
 288  
                node names that are fully qualified with the correct namespace/uri, when we don't KNOW at this point what that
 289  
                correct namespace URI is?
 290  
 
 291  
                The solution is to use a namespace naming convention coupled with the defined content type name in order to generate
 292  
                the canonical namespace uri for any custom content type.
 293  
 
 294  
                ns:notification/Content<Content Type name>
 295  
 
 296  
                e.g. ns:notification/ContentSimple, or ns:notification/ContentEvent
 297  
 
 298  
                We then construct an "ephemeral" namespace prefix to use in the NamespaceContext/XPath expressions to refer to this namespace URI.
 299  
 
 300  
                e.g. contentNS_<unique number>
 301  
 
 302  
                It doesn't (shouldn't!) matter what this ephemeral namespace is.
 303  
 
 304  
                We then define a temporary NamespaceContext that consists only of this ephemeral namespace mapping, and wrap the existing
 305  
                XPath NamespaceContext (the nice one we set up above to do our original qualifizzizing) with it.  Then we are off and on our
 306  
                way and can use XPath to parse the content type of arbitrary namespace.
 307  
              */
 308  0
             Map<String, String> contentTypeNamespace = new HashMap<String, String>();
 309  0
             String ephemeralNamespace = "contentNS_" + System.currentTimeMillis();
 310  0
             contentTypeNamespace.put(ephemeralNamespace, CONTENT_TYPE_NAMESPACE_PREFIX + contentType.getName());
 311  0
             xpath.setNamespaceContext(new CompoundNamespaceContext(new ConfiguredNamespaceContext(contentTypeNamespace), xpath.getNamespaceContext()));
 312  0
             Node contentNode = (Node) xpath.evaluate("/nreq:notification/" + ephemeralNamespace + ":content", root, XPathConstants.NODE);
 313  0
             Element contentElement = null;
 314  0
             String content = "";
 315  
             /* Since we have had to use <any processContents="lax" minOccurs="1" maxOccurs="1"/> for the content element
 316  
              * (since there is no way to specify a mandatory element of specified name, but unspecified type), we need to
 317  
              * make sure to *programmatically* enforce its existence, since schema won't (the above statement says any
 318  
              * element occuring once, but we don't want "any" element, we want an element named 'content').
 319  
              */
 320  0
             if (contentNode == null) {
 321  0
                 throw new XmlException("The 'content' element is mandatory.");
 322  
             }
 323  0
             if (contentNode != null) {
 324  0
                 if (!(contentNode instanceof Element)) {
 325  
                     // don't know what could possibly cause this
 326  0
                     throw new XmlException("The 'content' node is not an Element! (???).");
 327  
                 }
 328  0
                 contentElement = (Element) contentNode;
 329  
                 /* Take the literal XML content value of the DOM node.
 330  
                    This should be symmetric/reversable */
 331  0
                 content = XmlJotter.jotNode(contentNode, true);
 332  
             }
 333  
 
 334  0
             notification.setContent(content);
 335  
 
 336  0
             LOG.debug("Content type: " + contentType.getName());
 337  0
             LOG.debug("Content: " + content);
 338  
 
 339  
             /* double check that we got content of the type that was declared, not just any valid
 340  
                content type! (e.g., can't send valid Event content for a Simple notification type)
 341  
              */
 342  0
             validateContent(notification, contentType.getName(), contentElement, content);
 343  
 
 344  0
             return notification;
 345  0
         } catch (XPathExpressionException xpee) {
 346  0
             throw new XmlException("Error parsing request", xpee);
 347  
         }
 348  
     }
 349  
 
 350  
 
 351  
 
 352  
     /**
 353  
      * This method validates the content of a notification message by matching up the namespace of the expected content type
 354  
      * to the actual namespace that is passed in as part of the XML message.
 355  
      *
 356  
      * This is possibly redundant because we are using qualified XPath expressions to obtain content under the correct namespace.
 357  
      *
 358  
      * @param notification
 359  
      * @param contentType
 360  
      * @param contentElement
 361  
      * @param content
 362  
      * @throws IOException
 363  
      * @throws XmlException
 364  
      */
 365  
     private void validateContent(Notification notification, String contentType, Element contentElement, String content) throws IOException, XmlException {
 366  
         // this debugging relies on a DOM 3 API that is only available with Xerces 2.7.1+ (TypeInfo)
 367  
         // commented out for now
 368  
         /*LOG.debug(contentElement.getSchemaTypeInfo());
 369  
         LOG.debug(contentElement.getSchemaTypeInfo().getTypeName());
 370  
         LOG.debug(contentElement.getSchemaTypeInfo().getTypeNamespace());
 371  
         LOG.debug(contentElement.getNamespaceURI());
 372  
         LOG.debug(contentElement.getLocalName());
 373  
         LOG.debug(contentElement.getNodeName());*/
 374  
 
 375  0
         String contentTypeTitleCase = Character.toTitleCase(contentType.charAt(0)) + contentType.substring(1);
 376  0
         String expectedNamespaceURI = CONTENT_TYPE_NAMESPACE_PREFIX + contentTypeTitleCase;
 377  0
         String actualNamespaceURI = contentElement.getNamespaceURI();
 378  0
         if (!actualNamespaceURI.equals(expectedNamespaceURI)) {
 379  0
             throw new XmlException("Namespace URI of 'content' node, '" + actualNamespaceURI + "', does not match expected namespace URI, '" + expectedNamespaceURI + "', for content type '" + contentType + "'");
 380  
         }
 381  0
     }
 382  
 
 383  
     /**
 384  
      * This method will marshall out the NotificationResponse object as a String of XML, using XStream.
 385  
      * @see org.kuali.rice.ken.service.NotificationMessageContentService#generateNotificationResponseMessage(org.kuali.rice.ken.bo.NotificationResponse)
 386  
      */
 387  
     public String generateNotificationResponseMessage(NotificationResponse response) {
 388  0
         XStream xstream = new XStream(new DomDriver());
 389  0
         xstream.alias("response", NotificationResponse.class);
 390  0
         xstream.alias("status", String.class);
 391  0
         xstream.alias("message", String.class);
 392  0
         xstream.alias("notificationId", Long.class);
 393  0
         String xml = xstream.toXML(response);
 394  0
         return xml;
 395  
     }
 396  
 
 397  
     /**
 398  
      * This method will marshall out the Notification object as a String of XML, using XStream and replaces the
 399  
      * full recipient list with just a single recipient.
 400  
      * @see org.kuali.rice.ken.service.NotificationMessageContentService#generateNotificationMessage(org.kuali.rice.ken.bo.Notification, java.lang.String)
 401  
      */
 402  
     public String generateNotificationMessage(Notification notification, String userRecipientId) {
 403  
         // create a new fresh instance so we don't screw up any references
 404  0
         Notification clone = Util.cloneNotificationWithoutObjectReferences(notification);
 405  
 
 406  
         /* TODO: modify clone recipient list so that:
 407  
              1. only the specified user is listed as a recipient (no other users or groups)
 408  
              2. if the specified user was resolved from a group, make sure to include
 409  
                 that group in the list so it can be searched against for this
 410  
                 particular per-user notification
 411  
 
 412  
            Group1 --> testuser1 --> "Group1 testuser1"
 413  
                   --> testuser2 --> "Group1 testuser2"
 414  
 
 415  
         */
 416  
 
 417  
         // inject only the single specified recipient
 418  0
         if(StringUtils.isNotBlank(userRecipientId)) {
 419  0
             clone.getRecipients().clear();
 420  
 
 421  0
             NotificationRecipient recipient = new NotificationRecipient();
 422  0
             recipient.setRecipientId(userRecipientId);
 423  0
             recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
 424  
 
 425  0
             clone.getRecipients().add(recipient);
 426  
         }
 427  
 
 428  
         // now marshall out to XML
 429  0
         XStream xstream = new XStream(new DomDriver());
 430  0
         xstream.alias("notification", Notification.class);
 431  0
         xstream.alias("channel", NotificationChannel.class);
 432  0
         xstream.alias("contentType", NotificationContentType.class);
 433  0
         xstream.alias("title", String.class);
 434  0
         xstream.alias("priority", NotificationPriority.class);
 435  0
         xstream.alias("producer", NotificationProducer.class);
 436  0
         xstream.alias("recipient", NotificationRecipient.class);
 437  0
         xstream.alias("sender", NotificationSender.class);
 438  0
         String xml = xstream.toXML(clone);
 439  0
         return xml;
 440  
     }
 441  
 
 442  
     /**
 443  
      * This method will marshall out the Notification object as a String of XML, using XStream.
 444  
      * @see org.kuali.rice.ken.service.NotificationMessageContentService#generateNotificationMessage(org.kuali.rice.ken.bo.Notification)
 445  
      */
 446  
     public String generateNotificationMessage(Notification notification) {
 447  0
         return generateNotificationMessage(notification, null);
 448  
     }
 449  
 
 450  
     /**
 451  
      * Uses XPath to parse out the serialized Notification xml into a Notification instance.
 452  
      * Warning: this method does NOT validate the payload content XML
 453  
      * @see org.kuali.rice.ken.service.NotificationMessageContentService#parseNotificationXml(byte[])
 454  
      */
 455  
     public Notification parseSerializedNotificationXml(byte[] xmlAsBytes) throws Exception {
 456  
         Document doc;
 457  0
         Notification notification = new Notification();
 458  
 
 459  
         try {
 460  0
             doc = Util.parse(new InputSource(new ByteArrayInputStream(xmlAsBytes)), false, false, null);
 461  0
         } catch (Exception pce) {
 462  0
             throw new XmlException("Error obtaining XML parser", pce);
 463  0
         }
 464  
 
 465  0
         Element root = doc.getDocumentElement();
 466  0
         XPath xpath = XPathFactory.newInstance().newXPath();
 467  0
         xpath.setNamespaceContext(Util.getNotificationNamespaceContext(doc));
 468  
 
 469  
         try {
 470  
             // pull data out of the application content
 471  0
             String title = ((String) xpath.evaluate("//notification/title", root)).trim();
 472  
 
 473  0
             String channelName = ((String) xpath.evaluate("//notification/channel/name", root)).trim();
 474  
 
 475  0
             String contentTypeName = ((String) xpath.evaluate("//notification/contentType/name", root)).trim();
 476  
 
 477  0
             String priorityName = ((String) xpath.evaluate("//notification/priority/name", root)).trim();
 478  
 
 479  0
             List<String> senders = new ArrayList<String>();
 480  0
             NodeList senderNodes = (NodeList) xpath.evaluate("//notification/senders/sender/senderName", root, XPathConstants.NODESET);
 481  0
             for (int i = 0; i < senderNodes.getLength(); i++) {
 482  0
                 senders.add(senderNodes.item(i).getTextContent().trim());
 483  
             }
 484  
 
 485  0
             String deliveryType = ((String) xpath.evaluate("//notification/deliveryType", root)).trim();
 486  0
             if(deliveryType.equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
 487  0
                 deliveryType = NotificationConstants.DELIVERY_TYPES.FYI;
 488  
             } else {
 489  0
                 deliveryType = NotificationConstants.DELIVERY_TYPES.ACK;
 490  
             }
 491  
 
 492  0
             String sendDateTime = ((String) xpath.evaluate("//notification/sendDateTime", root)).trim();
 493  
 
 494  0
             String autoRemoveDateTime = ((String) xpath.evaluate("//notification/autoRemoveDateTime", root)).trim();
 495  
 
 496  0
             List<String> userRecipients = new ArrayList<String>();
 497  0
             List<String> workgroupRecipients = new ArrayList<String>();
 498  
 
 499  0
             NodeList recipientIds = (NodeList) xpath.evaluate("//notification/recipients/recipient/recipientId", root, XPathConstants.NODESET);
 500  0
             NodeList recipientTypes = (NodeList) xpath.evaluate("//notification/recipients/recipient/recipientType", root, XPathConstants.NODESET);
 501  
 
 502  0
             for (int i = 0; i < recipientIds.getLength(); i++) {
 503  0
                     if(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode().equalsIgnoreCase(recipientTypes.item(i).getTextContent().trim())) {
 504  0
                         userRecipients.add(recipientIds.item(i).getTextContent().trim());
 505  
                     } else {
 506  
                         //String groupName = recipientIds.item(i).getTextContent().trim();
 507  
                         //KimGroup recipGroup = KimApiServiceLocator.getIdentityManagementService().getGroupByNameAndNamespaceCode(Utilities.parseGroupNamespaceCode(groupName), Utilities.parseGroupName(groupName));
 508  
                         //workgroupRecipients.add(recipGroup.getGroupId());
 509  0
                         workgroupRecipients.add(recipientIds.item(i).getTextContent().trim());
 510  
                     }
 511  
             }
 512  
 
 513  0
             String content = ((String) xpath.evaluate("//notification/content", root)).trim();
 514  
 
 515  
             // now populate the notification BO instance
 516  0
             NotificationChannel channel = Util.retrieveFieldReference("channel", "name", channelName, NotificationChannel.class, boDao);
 517  0
             notification.setChannel(channel);
 518  
 
 519  0
             NotificationPriority priority = Util.retrieveFieldReference("priority", "name", priorityName, NotificationPriority.class, boDao);
 520  0
             notification.setPriority(priority);
 521  
 
 522  0
             NotificationContentType contentType = Util.retrieveFieldReference("contentType", "name", contentTypeName, NotificationContentType.class, boDao);
 523  0
             notification.setContentType(contentType);
 524  
 
 525  0
             NotificationProducer producer = Util.retrieveFieldReference("producer", "name", NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME,
 526  
                     NotificationProducer.class, boDao);
 527  0
             notification.setProducer(producer);
 528  
 
 529  0
             for (String senderName: senders) {
 530  0
                 NotificationSender ns = new NotificationSender();
 531  0
                 ns.setSenderName(senderName);
 532  0
                 notification.addSender(ns);
 533  0
             }
 534  
 
 535  0
             for (String userRecipientId: userRecipients) {
 536  0
                 NotificationRecipient recipient = new NotificationRecipient();
 537  0
                 recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
 538  0
                 recipient.setRecipientId(userRecipientId);
 539  0
                 notification.addRecipient(recipient);
 540  0
             }
 541  
 
 542  0
             for (String workgroupRecipientId: workgroupRecipients) {
 543  0
                 NotificationRecipient recipient = new NotificationRecipient();
 544  0
                 recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
 545  0
                 recipient.setRecipientId(workgroupRecipientId);
 546  0
                 notification.addRecipient(recipient);
 547  0
             }
 548  
 
 549  0
             if (!StringUtils.isBlank(title)) {
 550  0
                 notification.setTitle(title);
 551  
             }
 552  
 
 553  0
             notification.setDeliveryType(deliveryType);
 554  
 
 555  
             // simpledateformat is not threadsafe, have to sync and validate
 556  0
             synchronized (DATEFORMAT_CURR_TZ) {
 557  0
                 Date d = null;
 558  0
                 if(StringUtils.isNotBlank(sendDateTime)) {
 559  
                     try {
 560  0
                         d = DATEFORMAT_CURR_TZ.parse(sendDateTime);
 561  0
                     } catch (ParseException pe) {
 562  0
                         LOG.warn("Invalid 'sendDateTime' value: " + sendDateTime, pe);
 563  0
                     }
 564  0
                     notification.setSendDateTime(new Timestamp(d.getTime()));
 565  
                 }
 566  
 
 567  0
                 Date d2 = null;
 568  0
                 if(StringUtils.isNotBlank(autoRemoveDateTime)) {
 569  
                     try {
 570  0
                         d2 = DATEFORMAT_CURR_TZ.parse(autoRemoveDateTime);
 571  0
                     } catch (ParseException pe) {
 572  0
                         LOG.warn("Invalid 'autoRemoveDateTime' value: " + autoRemoveDateTime, pe);
 573  0
                     }
 574  0
                     notification.setAutoRemoveDateTime(new Timestamp(d2.getTime()));
 575  
                 }
 576  0
             }
 577  
 
 578  0
             notification.setContent(content);
 579  
 
 580  0
             return notification;
 581  0
         } catch (XPathExpressionException xpee) {
 582  0
             throw new XmlException("Error parsing request", xpee);
 583  
         }
 584  
     }
 585  
 }