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