001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.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.core.framework.persistence.dao.GenericDao;
026import org.kuali.rice.ken.bo.NotificationBo;
027import org.kuali.rice.ken.bo.NotificationChannelBo;
028import org.kuali.rice.ken.bo.NotificationContentTypeBo;
029import org.kuali.rice.ken.bo.NotificationPriorityBo;
030import org.kuali.rice.ken.bo.NotificationProducerBo;
031import org.kuali.rice.ken.bo.NotificationRecipientBo;
032import org.kuali.rice.ken.bo.NotificationResponseBo;
033import org.kuali.rice.ken.bo.NotificationSenderBo;
034import org.kuali.rice.ken.service.NotificationContentTypeService;
035import org.kuali.rice.ken.service.NotificationMessageContentService;
036import org.kuali.rice.ken.util.CompoundNamespaceContext;
037import org.kuali.rice.ken.util.ConfiguredNamespaceContext;
038import org.kuali.rice.ken.util.NotificationConstants;
039import org.kuali.rice.ken.util.Util;
040import org.kuali.rice.kew.util.Utilities;
041import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
042import org.kuali.rice.kim.api.services.KimApiServiceLocator;
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 BusinessObjectDao persistence layer
087     */
088    private GenericDao boDao;
089    /**
090     * NotificationContentTypeService impl
091     */
092    private NotificationContentTypeService notificationContentTypeService;
093
094    /**
095     * Constructor which takes a GenericDao
096     * Constructs a NotificationMessageContentServiceImpl.java.
097     * @param boDao
098     */
099    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 NotificationBo 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 NotificationBo 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 NotificationBo 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<NotificationRecipientBo> recipients = new ArrayList<NotificationRecipientBo>();
190            for (int i = 0; i < nodes.getLength(); i++) {
191                Node node = nodes.item(i);
192                NotificationRecipientBo recipient = new NotificationRecipientBo();
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().getGroupByNamespaceCodeAndName(
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            NotificationBo notification = new NotificationBo();
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            NotificationChannelBo channel = Util.retrieveFieldReference("channel", "name", channelName, NotificationChannelBo.class, boDao);
231            notification.setChannel(channel);
232
233            NotificationProducerBo producer = Util.retrieveFieldReference("producer", "name", producerName, NotificationProducerBo.class, boDao);
234            notification.setProducer(producer);
235
236            for (String sender: senders) {
237                NotificationSenderBo ns = new NotificationSenderBo();
238                LOG.debug("Setting sender: " + sender);
239                ns.setSenderName(sender);
240                notification.addSender(ns);
241            }
242
243            for (NotificationRecipientBo 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.setSendDateTimeValue(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.setAutoRemoveDateTimeValue(new Timestamp(d.getTime()));
275            }
276
277
278            /* we have to look up priority and content type in the system also */
279            NotificationPriorityBo priority = Util.retrieveFieldReference("priority", "name", priorityName, NotificationPriorityBo.class, boDao);
280            notification.setPriority(priority);
281
282            NotificationContentTypeBo contentType = Util.retrieveFieldReference("contentType", "name", contentTypeName, NotificationContentTypeBo.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(NotificationBo 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.NotificationResponseBo)
386     */
387    public String generateNotificationResponseMessage(NotificationResponseBo response) {
388        XStream xstream = new XStream(new DomDriver());
389        xstream.alias("response", NotificationResponseBo.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.NotificationBo, java.lang.String)
401     */
402    public String generateNotificationMessage(NotificationBo notification, String userRecipientId) {
403        // create a new fresh instance so we don't screw up any references
404        NotificationBo 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            NotificationRecipientBo recipient = new NotificationRecipientBo();
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", NotificationBo.class);
431        xstream.alias("channel", NotificationChannelBo.class);
432        xstream.alias("contentType", NotificationContentTypeBo.class);
433        xstream.alias("title", String.class);
434        xstream.alias("priority", NotificationPriorityBo.class);
435        xstream.alias("producer", NotificationProducerBo.class);
436        xstream.alias("recipient", NotificationRecipientBo.class);
437        xstream.alias("sender", NotificationSenderBo.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.NotificationBo)
445     */
446    public String generateNotificationMessage(NotificationBo 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 NotificationBo parseSerializedNotificationXml(byte[] xmlAsBytes) throws Exception {
456        Document doc;
457        NotificationBo notification = new NotificationBo();
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().getGroupByNamespaceCodeAndName(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            NotificationChannelBo channel = Util.retrieveFieldReference("channel", "name", channelName, NotificationChannelBo.class, boDao);
517            notification.setChannel(channel);
518
519            NotificationPriorityBo priority = Util.retrieveFieldReference("priority", "name", priorityName, NotificationPriorityBo.class, boDao);
520            notification.setPriority(priority);
521
522            NotificationContentTypeBo contentType = Util.retrieveFieldReference("contentType", "name", contentTypeName, NotificationContentTypeBo.class, boDao);
523            notification.setContentType(contentType);
524
525            NotificationProducerBo producer = Util.retrieveFieldReference("producer", "name", NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME,
526                    NotificationProducerBo.class, boDao);
527            notification.setProducer(producer);
528
529            for (String senderName: senders) {
530                NotificationSenderBo ns = new NotificationSenderBo();
531                ns.setSenderName(senderName);
532                notification.addSender(ns);
533            }
534
535            for (String userRecipientId: userRecipients) {
536                NotificationRecipientBo recipient = new NotificationRecipientBo();
537                recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
538                recipient.setRecipientId(userRecipientId);
539                notification.addRecipient(recipient);
540            }
541
542            for (String workgroupRecipientId: workgroupRecipients) {
543                NotificationRecipientBo recipient = new NotificationRecipientBo();
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.setSendDateTimeValue(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.setAutoRemoveDateTimeValue(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}