001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.ken.service.impl;
017
018import com.thoughtworks.xstream.XStream;
019import com.thoughtworks.xstream.io.xml.DomDriver;
020import org.apache.commons.io.IOUtils;
021import org.apache.commons.lang.StringUtils;
022import org.apache.log4j.Logger;
023import org.kuali.rice.core.api.util.xml.XmlException;
024import org.kuali.rice.core.api.util.xml.XmlJotter;
025import org.kuali.rice.ken.bo.NotificationBo;
026import org.kuali.rice.ken.bo.NotificationChannelBo;
027import org.kuali.rice.ken.bo.NotificationContentTypeBo;
028import org.kuali.rice.ken.bo.NotificationPriorityBo;
029import org.kuali.rice.ken.bo.NotificationProducerBo;
030import org.kuali.rice.ken.bo.NotificationRecipientBo;
031import org.kuali.rice.ken.bo.NotificationResponseBo;
032import org.kuali.rice.ken.bo.NotificationSenderBo;
033import org.kuali.rice.ken.service.NotificationContentTypeService;
034import org.kuali.rice.ken.service.NotificationMessageContentService;
035import org.kuali.rice.ken.util.CompoundNamespaceContext;
036import org.kuali.rice.ken.util.ConfiguredNamespaceContext;
037import org.kuali.rice.ken.util.NotificationConstants;
038import org.kuali.rice.ken.util.Util;
039import org.kuali.rice.kew.util.Utilities;
040import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
041import org.kuali.rice.kim.api.services.KimApiServiceLocator;
042import org.kuali.rice.krad.data.DataObjectService;
043import org.w3c.dom.Document;
044import org.w3c.dom.Element;
045import org.w3c.dom.Node;
046import org.w3c.dom.NodeList;
047import org.xml.sax.InputSource;
048import org.xml.sax.SAXException;
049
050import javax.xml.parsers.ParserConfigurationException;
051import javax.xml.xpath.XPath;
052import javax.xml.xpath.XPathConstants;
053import javax.xml.xpath.XPathExpressionException;
054import javax.xml.xpath.XPathFactory;
055import java.io.ByteArrayInputStream;
056import java.io.IOException;
057import java.io.InputStream;
058import java.sql.Timestamp;
059import java.text.DateFormat;
060import java.text.ParseException;
061import java.text.SimpleDateFormat;
062import java.util.ArrayList;
063import java.util.Date;
064import java.util.HashMap;
065import java.util.List;
066import java.util.Map;
067
068/**
069 * NotificationMessageContentService implementation - uses both Xalan and XStream in various places to manage the marshalling/unmarshalling of
070 * Notification data for processing by various components in the system.
071 * @see NotificationMessageContentService
072 * @author Kuali Rice Team (rice.collab@kuali.org)
073 */
074public class NotificationMessageContentServiceImpl implements NotificationMessageContentService {
075    private static final Logger LOG = Logger.getLogger(NotificationMessageContentServiceImpl.class);
076
077    /**
078     * Prefix that content type schemas should start with
079     */
080    static final String CONTENT_TYPE_NAMESPACE_PREFIX = "ns:notification/ContentType";
081
082    // Date format of current timezone necessary for intra-system XML parsing via send form
083    private static final DateFormat DATEFORMAT_CURR_TZ = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
084
085    /**
086     * Our DataObjectService persistence layer
087     */
088    private DataObjectService dataObjectService;
089    /**
090     * NotificationContentTypeService impl
091     */
092    private NotificationContentTypeService notificationContentTypeService;
093
094    /**
095     * Constructor which takes a {@link DataObjectService}
096     * Constructs a NotificationMessageContentServiceImpl.java.
097     * @param dataObjectService persistence layer
098     * @param notificationContentTypeService {@link NotificationContentTypeService}
099     */
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}