View Javadoc

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      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 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      public NotificationMessageContentServiceImpl(GenericDao boDao,  NotificationContentTypeService notificationContentTypeService) {
100         this.boDao = boDao;
101         this.notificationContentTypeService = notificationContentTypeService;
102     }
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         byte[] bytes = notificationMessageAsXml.getBytes();
113 
114         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         byte[] bytes = IOUtils.toByteArray(stream);
126 
127         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             doc = Util.parseWithNotificationEntityResolver(new InputSource(new ByteArrayInputStream(bytes)), true, true, notificationContentTypeService);
144         } catch (ParserConfigurationException pce) {
145             throw new XmlException("Error obtaining XML parser", pce);
146         } catch (SAXException se) {
147             throw new XmlException("Error validating notification request", se);
148         }
149 
150         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         XPath xpath = XPathFactory.newInstance().newXPath();
172         xpath.setNamespaceContext(Util.getNotificationNamespaceContext(doc));
173 
174         /* First parse immediate/primitive Notification member data */
175         LOG.debug("URI: " + xpath.getNamespaceContext().getNamespaceURI("nreq"));
176         try {
177             String channelName = (String) xpath.evaluate("/nreq:notification/nreq:channel", root);
178             LOG.debug("CHANNELNAME: "+ channelName);
179             String producerName = xpath.evaluate("/nreq:notification/nreq:producer", root);
180 
181             List<String> senders = new ArrayList<String>();
182             NodeList nodes = (NodeList) xpath.evaluate("/nreq:notification/nreq:senders/nreq:sender", root, XPathConstants.NODESET);
183             for (int i = 0; i < nodes.getLength(); i++) {
184                 LOG.debug("sender node: " + nodes.item(i));
185                 LOG.debug("sender node VALUE: " + nodes.item(i).getTextContent());
186                 senders.add(nodes.item(i).getTextContent());
187             }
188             nodes = (NodeList) xpath.evaluate("/nreq:notification/nreq:recipients/nreq:group|/nreq:notification/nreq:recipients/nreq:user", root, XPathConstants.NODESET);
189             List<NotificationRecipient> recipients = new ArrayList<NotificationRecipient>();
190             for (int i = 0; i < nodes.getLength(); i++) {
191                 Node node = nodes.item(i);
192                 NotificationRecipient recipient = new NotificationRecipient();
193                 // NOTE: assumes validation has occurred; does not check validity of element name
194                 if (NotificationConstants.RECIPIENT_TYPES.GROUP.equalsIgnoreCase(node.getLocalName())) {
195                     //recipient.setRecipientType(NotificationConstants.RECIPIENT_TYPES.GROUP);
196                     recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
197                     recipient.setRecipientId(KimApiServiceLocator.getGroupService().getGroupByNameAndNamespaceCode(
198                             Utilities.parseGroupNamespaceCode(node.getTextContent()), Utilities.parseGroupName(
199                             node.getTextContent())).getId());
200                 } else if (NotificationConstants.RECIPIENT_TYPES.USER.equalsIgnoreCase(node.getLocalName())){
201                     //recipient.setRecipientType(NotificationConstants.RECIPIENT_TYPES.USER);
202                     recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
203                     recipient.setRecipientId(node.getTextContent());
204                 } else {
205                     throw new XmlException("Invalid 'recipientType' value: '" + node.getLocalName() +
206                 	    "'.  Needs to either be 'user' or 'group'");
207                 }
208                 recipients.add(recipient);
209             }
210 
211             String deliveryType = xpath.evaluate("/nreq:notification/nreq:deliveryType", root);
212             String sendDateTime = xpath.evaluate("/nreq:notification/nreq:sendDateTime", root);
213             String autoRemoveDateTime = xpath.evaluate("/nreq:notification/nreq:autoRemoveDateTime", root);
214 
215             String priorityName = xpath.evaluate("/nreq:notification/nreq:priority", root);
216             String title = xpath.evaluate("/nreq:notification/nreq:title", root);
217             String contentTypeName = xpath.evaluate("/nreq:notification/nreq:contentType", root);
218 
219             /* Construct the Notification business object */
220 
221             Notification notification = new Notification();
222 
223             if (!StringUtils.isBlank(title)) {
224                 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             NotificationChannel channel = Util.retrieveFieldReference("channel", "name", channelName, NotificationChannel.class, boDao);
231             notification.setChannel(channel);
232 
233             NotificationProducer producer = Util.retrieveFieldReference("producer", "name", producerName, NotificationProducer.class, boDao);
234             notification.setProducer(producer);
235 
236             for (String sender: senders) {
237                 NotificationSender ns = new NotificationSender();
238                 LOG.debug("Setting sender: " + sender);
239                 ns.setSenderName(sender);
240                 notification.addSender(ns);
241             }
242 
243             for (NotificationRecipient recipient: recipients) {
244                 LOG.debug("Setting recipient id: "+ recipient.getRecipientId());
245                 notification.addRecipient(recipient);
246             }
247 
248             /* validate the delivery type */
249             if(!NotificationConstants.DELIVERY_TYPES.ACK.equalsIgnoreCase(deliveryType) &&
250                !NotificationConstants.DELIVERY_TYPES.FYI.equalsIgnoreCase(deliveryType)) {
251                 throw new XmlException("Invalid 'deliveryType' value: '" + deliveryType +
252                     "'.  Must be either 'ACK' or 'FYI'.");
253             }
254             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             if(StringUtils.isNotBlank(sendDateTime)) {
261                 try {
262                     d = Util.parseXSDDateTime(sendDateTime);
263                 } catch (ParseException pe) {
264                     throw new XmlException("Invalid 'sendDateTime' value: " + sendDateTime, pe);
265                 }
266                 notification.setSendDateTime(new Timestamp(d.getTime()));
267             }
268             if(StringUtils.isNotBlank(autoRemoveDateTime)) {
269                 try {
270                     d = Util.parseXSDDateTime(autoRemoveDateTime);
271                 } catch (ParseException pe) {
272                     throw new XmlException("Invalid 'autoRemoveDateTime' value: " + autoRemoveDateTime, pe);
273                 }
274                 notification.setAutoRemoveDateTime(new Timestamp(d.getTime()));
275             }
276 
277 
278             /* we have to look up priority and content type in the system also */
279             NotificationPriority priority = Util.retrieveFieldReference("priority", "name", priorityName, NotificationPriority.class, boDao);
280             notification.setPriority(priority);
281 
282             NotificationContentType contentType = Util.retrieveFieldReference("contentType", "name", contentTypeName, NotificationContentType.class, boDao);
283             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             Map<String, String> contentTypeNamespace = new HashMap<String, String>();
309             String ephemeralNamespace = "contentNS_" + System.currentTimeMillis();
310             contentTypeNamespace.put(ephemeralNamespace, CONTENT_TYPE_NAMESPACE_PREFIX + contentType.getName());
311             xpath.setNamespaceContext(new CompoundNamespaceContext(new ConfiguredNamespaceContext(contentTypeNamespace), xpath.getNamespaceContext()));
312             Node contentNode = (Node) xpath.evaluate("/nreq:notification/" + ephemeralNamespace + ":content", root, XPathConstants.NODE);
313             Element contentElement = null;
314             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             if (contentNode == null) {
321                 throw new XmlException("The 'content' element is mandatory.");
322             }
323             if (contentNode != null) {
324                 if (!(contentNode instanceof Element)) {
325                     // don't know what could possibly cause this
326                     throw new XmlException("The 'content' node is not an Element! (???).");
327                 }
328                 contentElement = (Element) contentNode;
329                 /* Take the literal XML content value of the DOM node.
330                    This should be symmetric/reversable */
331                 content = XmlJotter.jotNode(contentNode, true);
332             }
333 
334             notification.setContent(content);
335 
336             LOG.debug("Content type: " + contentType.getName());
337             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             validateContent(notification, contentType.getName(), contentElement, content);
343 
344             return notification;
345         } catch (XPathExpressionException xpee) {
346             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         String contentTypeTitleCase = Character.toTitleCase(contentType.charAt(0)) + contentType.substring(1);
376         String expectedNamespaceURI = CONTENT_TYPE_NAMESPACE_PREFIX + contentTypeTitleCase;
377         String actualNamespaceURI = contentElement.getNamespaceURI();
378         if (!actualNamespaceURI.equals(expectedNamespaceURI)) {
379             throw new XmlException("Namespace URI of 'content' node, '" + actualNamespaceURI + "', does not match expected namespace URI, '" + expectedNamespaceURI + "', for content type '" + contentType + "'");
380         }
381     }
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 	XStream xstream = new XStream(new DomDriver());
389 	xstream.alias("response", NotificationResponse.class);
390 	xstream.alias("status", String.class);
391 	xstream.alias("message", String.class);
392         xstream.alias("notificationId", Long.class);
393 	String xml = xstream.toXML(response);
394 	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 	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 	if(StringUtils.isNotBlank(userRecipientId)) {
419 	    clone.getRecipients().clear();
420 
421 	    NotificationRecipient recipient = new NotificationRecipient();
422 	    recipient.setRecipientId(userRecipientId);
423 	    recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
424 
425 	    clone.getRecipients().add(recipient);
426 	}
427 
428 	// now marshall out to XML
429 	XStream xstream = new XStream(new DomDriver());
430 	xstream.alias("notification", Notification.class);
431 	xstream.alias("channel", NotificationChannel.class);
432 	xstream.alias("contentType", NotificationContentType.class);
433         xstream.alias("title", String.class);
434 	xstream.alias("priority", NotificationPriority.class);
435 	xstream.alias("producer", NotificationProducer.class);
436 	xstream.alias("recipient", NotificationRecipient.class);
437 	xstream.alias("sender", NotificationSender.class);
438 	String xml = xstream.toXML(clone);
439 	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 	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         Notification notification = new Notification();
458 
459         try {
460             doc = Util.parse(new InputSource(new ByteArrayInputStream(xmlAsBytes)), false, false, null);
461         } catch (Exception pce) {
462             throw new XmlException("Error obtaining XML parser", pce);
463         }
464 
465         Element root = doc.getDocumentElement();
466         XPath xpath = XPathFactory.newInstance().newXPath();
467         xpath.setNamespaceContext(Util.getNotificationNamespaceContext(doc));
468 
469         try {
470             // pull data out of the application content
471             String title = ((String) xpath.evaluate("//notification/title", root)).trim();
472 
473             String channelName = ((String) xpath.evaluate("//notification/channel/name", root)).trim();
474 
475             String contentTypeName = ((String) xpath.evaluate("//notification/contentType/name", root)).trim();
476 
477             String priorityName = ((String) xpath.evaluate("//notification/priority/name", root)).trim();
478 
479             List<String> senders = new ArrayList<String>();
480             NodeList senderNodes = (NodeList) xpath.evaluate("//notification/senders/sender/senderName", root, XPathConstants.NODESET);
481             for (int i = 0; i < senderNodes.getLength(); i++) {
482                 senders.add(senderNodes.item(i).getTextContent().trim());
483             }
484 
485             String deliveryType = ((String) xpath.evaluate("//notification/deliveryType", root)).trim();
486             if(deliveryType.equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
487                 deliveryType = NotificationConstants.DELIVERY_TYPES.FYI;
488             } else {
489                 deliveryType = NotificationConstants.DELIVERY_TYPES.ACK;
490             }
491 
492             String sendDateTime = ((String) xpath.evaluate("//notification/sendDateTime", root)).trim();
493 
494             String autoRemoveDateTime = ((String) xpath.evaluate("//notification/autoRemoveDateTime", root)).trim();
495 
496             List<String> userRecipients = new ArrayList<String>();
497             List<String> workgroupRecipients = new ArrayList<String>();
498 
499             NodeList recipientIds = (NodeList) xpath.evaluate("//notification/recipients/recipient/recipientId", root, XPathConstants.NODESET);
500             NodeList recipientTypes = (NodeList) xpath.evaluate("//notification/recipients/recipient/recipientType", root, XPathConstants.NODESET);
501 
502             for (int i = 0; i < recipientIds.getLength(); i++) {
503             	if(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode().equalsIgnoreCase(recipientTypes.item(i).getTextContent().trim())) {
504             	    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             	    workgroupRecipients.add(recipientIds.item(i).getTextContent().trim());
510             	}
511             }
512 
513             String content = ((String) xpath.evaluate("//notification/content", root)).trim();
514 
515             // now populate the notification BO instance
516             NotificationChannel channel = Util.retrieveFieldReference("channel", "name", channelName, NotificationChannel.class, boDao);
517             notification.setChannel(channel);
518 
519             NotificationPriority priority = Util.retrieveFieldReference("priority", "name", priorityName, NotificationPriority.class, boDao);
520             notification.setPriority(priority);
521 
522             NotificationContentType contentType = Util.retrieveFieldReference("contentType", "name", contentTypeName, NotificationContentType.class, boDao);
523             notification.setContentType(contentType);
524 
525             NotificationProducer producer = Util.retrieveFieldReference("producer", "name", NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME,
526         	    NotificationProducer.class, boDao);
527             notification.setProducer(producer);
528 
529             for (String senderName: senders) {
530                 NotificationSender ns = new NotificationSender();
531                 ns.setSenderName(senderName);
532                 notification.addSender(ns);
533             }
534 
535             for (String userRecipientId: userRecipients) {
536                 NotificationRecipient recipient = new NotificationRecipient();
537                 recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
538                 recipient.setRecipientId(userRecipientId);
539                 notification.addRecipient(recipient);
540             }
541 
542             for (String workgroupRecipientId: workgroupRecipients) {
543                 NotificationRecipient recipient = new NotificationRecipient();
544                 recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
545                 recipient.setRecipientId(workgroupRecipientId);
546                 notification.addRecipient(recipient);
547             }
548 
549             if (!StringUtils.isBlank(title)) {
550                 notification.setTitle(title);
551             }
552 
553             notification.setDeliveryType(deliveryType);
554 
555             // simpledateformat is not threadsafe, have to sync and validate
556             synchronized (DATEFORMAT_CURR_TZ) {
557                 Date d = null;
558                 if(StringUtils.isNotBlank(sendDateTime)) {
559                     try {
560                         d = DATEFORMAT_CURR_TZ.parse(sendDateTime);
561                     } catch (ParseException pe) {
562                         LOG.warn("Invalid 'sendDateTime' value: " + sendDateTime, pe);
563                     }
564                     notification.setSendDateTime(new Timestamp(d.getTime()));
565                 }
566 
567                 Date d2 = null;
568                 if(StringUtils.isNotBlank(autoRemoveDateTime)) {
569                     try {
570                         d2 = DATEFORMAT_CURR_TZ.parse(autoRemoveDateTime);
571                     } catch (ParseException pe) {
572                 	LOG.warn("Invalid 'autoRemoveDateTime' value: " + autoRemoveDateTime, pe);
573                     }
574                     notification.setAutoRemoveDateTime(new Timestamp(d2.getTime()));
575                 }
576             }
577 
578             notification.setContent(content);
579 
580             return notification;
581         } catch (XPathExpressionException xpee) {
582             throw new XmlException("Error parsing request", xpee);
583         }
584     }
585 }