001    /**
002     * Copyright 2005-2012 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     */
016    package org.kuali.rice.ken.util;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.log4j.Logger;
020    import org.kuali.rice.core.api.config.property.ConfigContext;
021    import org.kuali.rice.core.framework.persistence.dao.GenericDao;
022    import org.kuali.rice.ken.bo.NotificationBo;
023    import org.kuali.rice.ken.bo.NotificationChannelBo;
024    import org.kuali.rice.ken.bo.NotificationContentTypeBo;
025    import org.kuali.rice.ken.bo.NotificationPriorityBo;
026    import org.kuali.rice.ken.bo.NotificationProducerBo;
027    import org.kuali.rice.ken.bo.NotificationRecipientBo;
028    import org.kuali.rice.ken.bo.NotificationSenderBo;
029    import org.kuali.rice.ken.service.NotificationContentTypeService;
030    import org.w3c.dom.Document;
031    import org.w3c.dom.Element;
032    import org.w3c.dom.Node;
033    import org.w3c.dom.NodeList;
034    import org.xml.sax.EntityResolver;
035    import org.xml.sax.ErrorHandler;
036    import org.xml.sax.InputSource;
037    import org.xml.sax.SAXException;
038    import org.xml.sax.SAXParseException;
039    
040    import javax.xml.namespace.NamespaceContext;
041    import javax.xml.parsers.DocumentBuilder;
042    import javax.xml.parsers.DocumentBuilderFactory;
043    import javax.xml.parsers.ParserConfigurationException;
044    import javax.xml.transform.stream.StreamSource;
045    import java.io.IOException;
046    import java.sql.Timestamp;
047    import java.text.DateFormat;
048    import java.text.ParseException;
049    import java.text.SimpleDateFormat;
050    import java.util.ArrayList;
051    import java.util.Collections;
052    import java.util.Date;
053    import java.util.HashMap;
054    import java.util.Map;
055    import java.util.TimeZone;
056    
057    /**
058     * A general Utility class for the Notification system.
059     * @author Kuali Rice Team (rice.collab@kuali.org)
060     */
061    public final class Util {
062        private static final Logger LOG = Logger.getLogger(Util.class);
063        
064        public static final java.lang.String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
065        public static final java.lang.String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
066    
067        public static final NamespaceContext NOTIFICATION_NAMESPACE_CONTEXT
068            = new ConfiguredNamespaceContext(Collections.singletonMap("nreq", "ns:notification/NotificationRequest"));
069    
070        private static final String ZULU_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
071        private static final TimeZone ZULU_TZ = TimeZone.getTimeZone("UTC");
072    
073        private static final String CURR_TZ_FORMAT = "MM/dd/yyyy hh:mm a";
074        
075            private Util() {
076                    throw new UnsupportedOperationException("do not call");
077            }
078    
079        /**
080         * @return the name of the user configured to be the Notification system user
081         */
082        public static String getNotificationSystemUser() {
083            String system_user = ConfigContext.getCurrentContextConfig().getProperty(NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_PARAM);
084            if (system_user == null) {
085                system_user = NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER;
086            }
087            return system_user;
088        }
089    
090        /**
091         * Parses a date/time string under XSD dateTime type syntax
092         * @see #ZULU_FORMAT
093         * @param dateTimeString an XSD dateTime-formatted String
094         * @return a Date representing the time value of the String parameter 
095         * @throws ParseException if an error occurs during parsing 
096         */
097        public static Date parseXSDDateTime(String dateTimeString) throws ParseException {
098                return createZulu().parse(dateTimeString);
099        }
100    
101        /**
102         * Formats a Date into XSD dateTime format
103         * @param d the date value to format
104         * @return date value formatted into XSD dateTime format
105         */
106        public static String toXSDDateTimeString(Date d) {
107            return createZulu().format(d);
108        }
109        
110        /**
111         * Returns the current date formatted for the UI
112         * @return the current date formatted for the UI
113         */
114        public static String getCurrentDateTime() {
115            return toUIDateTimeString(new Date());
116        }
117        
118        /**
119         * Returns the specified date formatted for the UI
120         * @return the specified date formatted for the UI
121         */
122        public static String toUIDateTimeString(Date d) {
123            return createCurrTz().format(d);
124        }
125    
126        /**
127         * Parses the string in UI date time format
128         * @return the date parsed from UI date time format
129         */
130        public static Date parseUIDateTime(String s) throws ParseException {
131            return createCurrTz().parse(s);
132        }
133    
134        /**
135         * Returns a compound NamespaceContext that defers to the preconfigured notification namespace context
136         * first, then delegates to the document prefix/namespace definitions second.
137         * @param doc the Document to use for prefix/namespace resolution
138         * @return  compound NamespaceContext
139         */
140        public static NamespaceContext getNotificationNamespaceContext(Document doc) {
141            return new CompoundNamespaceContext(NOTIFICATION_NAMESPACE_CONTEXT, new DocumentNamespaceContext(doc));
142        }
143    
144        /**
145         * Returns an EntityResolver to resolve XML entities (namely schema resources) in the notification system
146         * @param notificationContentTypeService the NotificationContentTypeService
147         * @return an EntityResolver to resolve XML entities (namely schema resources) in the notification system
148         */
149        public static EntityResolver getNotificationEntityResolver(NotificationContentTypeService notificationContentTypeService) {
150            return new CompoundEntityResolver(new ClassLoaderEntityResolver("schema", "notification"),
151                                              new ContentTypeEntityResolver(notificationContentTypeService));
152        }
153    
154        /**
155         * transformContent - transforms xml content in notification to a string
156         * using the xsl in the datastore for a given documentType
157         * @param notification
158         * @return
159         */
160        public static String transformContent(NotificationBo notification) {
161            NotificationContentTypeBo contentType = notification.getContentType();
162            String xsl = contentType.getXsl();
163            
164            LOG.debug("xsl: "+xsl);
165            
166            XslSourceResolver xslresolver = new XslSourceResolver();
167            //StreamSource xslsource = xslresolver.resolveXslFromFile(xslpath);
168            StreamSource xslsource = xslresolver.resolveXslFromString(xsl);
169            String content = notification.getContent();
170            LOG.debug("xslsource:"+xslsource.toString());
171            
172            String contenthtml = new String();
173            try {
174              ContentTransformer transformer = new ContentTransformer(xslsource);
175              contenthtml = transformer.transform(content);
176              LOG.debug("html: "+contenthtml);
177            } catch (IOException ex) {
178                LOG.error("IOException transforming document",ex);
179            } catch (Exception ex) {
180                LOG.error("Exception transforming document",ex);
181            } 
182            return contenthtml;
183        }
184    
185        /**
186         * This method uses DOM to parse the input source of XML.
187         * @param source the input source
188         * @param validate whether to turn on validation
189         * @param namespaceAware whether to turn on namespace awareness
190         * @return Document the parsed (possibly validated) document
191         * @throws ParserConfigurationException
192         * @throws IOException
193         * @throws SAXException
194         */
195        public static Document parse(final InputSource source, boolean validate, boolean namespaceAware, EntityResolver entityResolver) throws ParserConfigurationException, IOException, SAXException {
196            // TODO: optimize this
197            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
198            dbf.setValidating(validate);
199            dbf.setNamespaceAware(namespaceAware);
200            dbf.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
201            DocumentBuilder db = dbf.newDocumentBuilder();
202            if (entityResolver != null) {
203                db.setEntityResolver(entityResolver);
204            }
205            db.setErrorHandler(new ErrorHandler() {
206                public void warning(SAXParseException se) {
207                    LOG.warn("Warning parsing xml doc " + source, se);
208                }
209                public void error(SAXParseException se) throws SAXException {
210                    LOG.error("Error parsing xml doc " + source, se);
211                    throw se;
212                }
213                public void fatalError(SAXParseException se) throws SAXException {
214                    LOG.error("Fatal error parsing xml doc " + source, se);
215                    throw se;
216                }
217            });
218            return db.parse(source);
219        }
220    
221        /**
222         * This method uses DOM to parse the input source of XML, supplying a notification-system-specific
223         * entity resolver.
224         * @param source the input source
225         * @param validate whether to turn on validation
226         * @param namespaceAware whether to turn on namespace awareness
227         * @return Document the parsed (possibly validated) document
228         * @throws ParserConfigurationException
229         * @throws IOException
230         * @throws SAXException
231         */
232        public static Document parseWithNotificationEntityResolver(final InputSource source, boolean validate, boolean namespaceAware, NotificationContentTypeService notificationContentTypeService) throws ParserConfigurationException, IOException, SAXException {
233            return parse(source, validate, namespaceAware, getNotificationEntityResolver(notificationContentTypeService));
234        }
235    
236        /**
237         * Returns a node child with the specified tag name of the specified parent node,
238         * or null if no such child node is found. 
239         * @param parent the parent node
240         * @param name the name of the child node
241         * @return child node if found, null otherwise
242         */
243        public static Element getChildElement(Node parent, String name) {
244            NodeList childList = parent.getChildNodes();
245            for (int i = 0; i < childList.getLength(); i++) {
246                Node node = childList.item(i);
247                // we must test against NodeName, not just LocalName
248                // LocalName seems to be null - I am guessing this is because
249                // the DocumentBuilderFactory is not "namespace aware"
250                // although I would have expected LocalName to default to
251                // NodeName
252                if (node.getNodeType() == Node.ELEMENT_NODE
253                    && (name.equals(node.getLocalName())
254                       || name.equals(node.getNodeName()))) {
255                    return (Element) node;
256                }
257            }
258            return null;
259        }
260        
261        /**
262         * This method will clone a given Notification object, one level deep, returning a fresh new instance 
263         * without any references.
264         * @param notification the object to clone
265         * @return Notification a fresh instance
266         */
267        public static final NotificationBo cloneNotificationWithoutObjectReferences(NotificationBo notification) {
268            NotificationBo clone = new NotificationBo();
269            
270            // handle simple data types first
271            if(notification.getCreationDateTime() != null) {
272                clone.setCreationDateTimeValue(new Timestamp(notification.getCreationDateTimeValue().getTime()));
273            }
274            if(notification.getAutoRemoveDateTime() != null) {
275                clone.setAutoRemoveDateTimeValue(new Timestamp(notification.getAutoRemoveDateTimeValue().getTime()));
276            }
277            clone.setContent(new String(notification.getContent()));
278            clone.setDeliveryType(new String(notification.getDeliveryType()));
279            if(notification.getId() != null) {
280                clone.setId(new Long(notification.getId()));
281            }
282            clone.setProcessingFlag(new String(notification.getProcessingFlag()));
283            if(notification.getSendDateTimeValue() != null) {
284                clone.setSendDateTimeValue(new Timestamp(notification.getSendDateTimeValue().getTime()));
285            }
286            
287            clone.setTitle(notification.getTitle());
288            
289            // now take care of the channel
290            NotificationChannelBo channel = new NotificationChannelBo();
291            channel.setId(new Long(notification.getChannel().getId()));
292            channel.setName(new String(notification.getChannel().getName()));
293            channel.setDescription(new String(notification.getChannel().getDescription()));
294            channel.setSubscribable(new Boolean(notification.getChannel().isSubscribable()).booleanValue());
295            clone.setChannel(channel);
296            
297            // handle the content type
298            NotificationContentTypeBo contentType = new NotificationContentTypeBo();
299            contentType.setId(new Long(notification.getContentType().getId()));
300            contentType.setDescription(new String(notification.getContentType().getDescription()));
301            contentType.setName(new String(notification.getContentType().getName()));
302            contentType.setNamespace(new String(notification.getContentType().getNamespace()));
303            clone.setContentType(contentType);
304            
305            // take care of the prioirity
306            NotificationPriorityBo priority = new NotificationPriorityBo();
307            priority.setDescription(new String(notification.getPriority().getDescription()));
308            priority.setId(new Long(notification.getPriority().getId()));
309            priority.setName(new String(notification.getPriority().getName()));
310            priority.setOrder(new Integer(notification.getPriority().getOrder()));
311            clone.setPriority(priority);
312            
313            // take care of the producer
314            NotificationProducerBo producer = new NotificationProducerBo();
315            producer.setDescription(new String(notification.getProducer().getDescription()));
316            producer.setId(new Long(notification.getProducer().getId()));
317            producer.setName(new String(notification.getProducer().getName()));
318            producer.setContactInfo(new String(notification.getProducer().getContactInfo()));
319            clone.setProducer(producer);
320            
321            // process the list of recipients now
322            ArrayList<NotificationRecipientBo> recipients = new ArrayList<NotificationRecipientBo>();
323            for(int i = 0; i < notification.getRecipients().size(); i++) {
324                NotificationRecipientBo recipient = notification.getRecipient(i);
325                NotificationRecipientBo cloneRecipient = new NotificationRecipientBo();
326                cloneRecipient.setRecipientId(new String(recipient.getRecipientId()));
327                cloneRecipient.setRecipientType(new String(recipient.getRecipientType()));
328                
329                recipients.add(cloneRecipient);
330            }
331            clone.setRecipients(recipients);
332            
333            // process the list of senders now
334            ArrayList<NotificationSenderBo> senders = new ArrayList<NotificationSenderBo>();
335            for(int i = 0; i < notification.getSenders().size(); i++) {
336                NotificationSenderBo sender = notification.getSender(i);
337                NotificationSenderBo cloneSender = new NotificationSenderBo();
338                cloneSender.setSenderName(new String(sender.getSenderName()));
339                
340                senders.add(cloneSender);
341            }
342            clone.setSenders(senders);
343            
344            return clone;
345        }
346        
347        /**
348         * This method generically retrieves a reference to foreign key objects that are part of the content, to get 
349         * at the reference objects' pk fields so that those values can be used to store the notification with proper 
350         * foreign key relationships in the database.
351         * @param <T>
352         * @param fieldName
353         * @param keyName
354         * @param keyValue
355         * @param clazz
356         * @param boDao
357         * @return T
358         * @throws IllegalArgumentException
359         */
360        public static <T> T retrieveFieldReference(String fieldName, String keyName, String keyValue, Class clazz, GenericDao boDao) throws IllegalArgumentException {
361            LOG.debug(fieldName + " key value: " + keyValue);
362            if (StringUtils.isBlank(keyValue)) {
363                throw new IllegalArgumentException(fieldName + " must be specified in notification");
364            }
365            Map<String, Object> keys = new HashMap<String, Object>(1);
366            keys.put(keyName, keyValue);
367            T reference = (T) boDao.findByPrimaryKey(clazz, keys);
368            if (reference == null) {
369                throw new IllegalArgumentException(fieldName + " '" + keyValue + "' not found");
370            }
371            return reference;
372        }
373    
374        /** date formats are not thread safe so creating a new one each time it is needed. */
375        private static DateFormat createZulu() {
376            final DateFormat df = new SimpleDateFormat(ZULU_FORMAT);
377            df.setTimeZone(ZULU_TZ);
378            return df;
379        }
380    
381        /** date formats are not thread safe so creating a new one each time it is needed. */
382        private static DateFormat createCurrTz() {
383            return new SimpleDateFormat(CURR_TZ_FORMAT);
384        }
385    }