View Javadoc

1   /*
2    * Copyright 2007 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.util;
17  
18  import java.io.ByteArrayOutputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.StringWriter;
22  import java.sql.Timestamp;
23  import java.text.DateFormat;
24  import java.text.ParseException;
25  import java.text.SimpleDateFormat;
26  import java.util.ArrayList;
27  import java.util.Date;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.TimeZone;
31  
32  import javax.xml.namespace.NamespaceContext;
33  import javax.xml.parsers.DocumentBuilder;
34  import javax.xml.parsers.DocumentBuilderFactory;
35  import javax.xml.parsers.ParserConfigurationException;
36  import javax.xml.transform.OutputKeys;
37  import javax.xml.transform.Result;
38  import javax.xml.transform.Source;
39  import javax.xml.transform.Transformer;
40  import javax.xml.transform.TransformerException;
41  import javax.xml.transform.TransformerFactory;
42  import javax.xml.transform.dom.DOMSource;
43  import javax.xml.transform.stream.StreamResult;
44  import javax.xml.transform.stream.StreamSource;
45  
46  import org.apache.commons.lang.StringUtils;
47  import org.apache.log4j.Logger;
48  import org.apache.xerces.jaxp.JAXPConstants;
49  import org.kuali.rice.core.config.ConfigContext;
50  import org.kuali.rice.core.dao.GenericDao;
51  import org.kuali.rice.ken.bo.Notification;
52  import org.kuali.rice.ken.bo.NotificationChannel;
53  import org.kuali.rice.ken.bo.NotificationContentType;
54  import org.kuali.rice.ken.bo.NotificationPriority;
55  import org.kuali.rice.ken.bo.NotificationProducer;
56  import org.kuali.rice.ken.bo.NotificationRecipient;
57  import org.kuali.rice.ken.bo.NotificationSender;
58  import org.kuali.rice.ken.service.NotificationContentTypeService;
59  import org.w3c.dom.Document;
60  import org.w3c.dom.Element;
61  import org.w3c.dom.Node;
62  import org.w3c.dom.NodeList;
63  import org.xml.sax.EntityResolver;
64  import org.xml.sax.ErrorHandler;
65  import org.xml.sax.InputSource;
66  import org.xml.sax.SAXException;
67  import org.xml.sax.SAXParseException;
68  
69  /**
70   * A general Utility class for the Notification system.
71   * @author Kuali Rice Team (rice.collab@kuali.org)
72   */
73  public final class Util {
74      private static final Logger LOG = Logger.getLogger(Util.class);
75  
76      //public static final EntityResolver ENTITY_RESOLVER = new ClassLoaderEntityResolver("schema", "notification");
77      public static final NamespaceContext NOTIFICATION_NAMESPACE_CONTEXT;
78  
79      //  XSD Zulu (UTC) date format necessary for XML request messages
80      private static final DateFormat DATEFORMAT_ZULU = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
81      
82      // Server current date time
83      private static final DateFormat DATEFORMAT_CURR_TZ = new SimpleDateFormat(
84      "MM/dd/yyyy hh:mm a");
85      
86      static {
87          Map<String, String> prefixToNS = new HashMap<String, String>();
88          prefixToNS.put("nreq", "ns:notification/NotificationRequest");
89          NOTIFICATION_NAMESPACE_CONTEXT = new ConfiguredNamespaceContext(prefixToNS);
90  
91          // set the timezone to Zulu for the XML/XSD formatter
92          DATEFORMAT_ZULU.setTimeZone(TimeZone.getTimeZone("UTC"));
93          // set the timezone for current time
94           
95      }
96  
97      /**
98       * @return the name of the user configured to be the Notification system user
99       */
100     public static String getNotificationSystemUser() {
101         String system_user = ConfigContext.getCurrentContextConfig().getProperty(NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_PARAM);
102         if (system_user == null) {
103             system_user = NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER;
104         }
105         return system_user;
106     }
107 
108     /**
109      * Parses a date/time string under XSD dateTime type syntax
110      * @see #DATEFORMAT_ZULU
111      * @param dateTimeString an XSD dateTime-formatted String
112      * @return a Date representing the time value of the String parameter 
113      * @throws ParseException if an error occurs during parsing 
114      */
115     public static Date parseXSDDateTime(String dateTimeString) throws ParseException {
116         synchronized (DATEFORMAT_ZULU) {
117             return DATEFORMAT_ZULU.parse(dateTimeString);
118         }
119     }
120 
121     /**
122      * Formats a Date into XSD dateTime format
123      * @param d the date value to format
124      * @return date value formatted into XSD dateTime format
125      */
126     public static String toXSDDateTimeString(Date d) {
127         synchronized (DATEFORMAT_ZULU) {
128             return DATEFORMAT_ZULU.format(d);
129         }
130     }
131     
132     /**
133      * Returns the current date formatted for the UI
134      * @return the current date formatted for the UI
135      */
136     public static String getCurrentDateTime() {
137         return toUIDateTimeString(new Date());
138     }
139     
140     /**
141      * Returns the specified date formatted for the UI
142      * @return the specified date formatted for the UI
143      */
144     public static String toUIDateTimeString(Date d) {
145         synchronized (DATEFORMAT_CURR_TZ) {
146            return DATEFORMAT_CURR_TZ.format(d);
147         }
148     }
149 
150     /**
151      * Parses the string in UI date time format
152      * @return the date parsed from UI date time format
153      */
154     public static Date parseUIDateTime(String s) throws ParseException {
155         synchronized (DATEFORMAT_CURR_TZ) {
156            return DATEFORMAT_CURR_TZ.parse(s);
157         }
158     }
159 
160     /**
161      * Returns a compound NamespaceContext that defers to the preconfigured notification namespace context
162      * first, then delegates to the document prefix/namespace definitions second.
163      * @param doc the Document to use for prefix/namespace resolution
164      * @return  compound NamespaceContext
165      */
166     public static NamespaceContext getNotificationNamespaceContext(Document doc) {
167         return new CompoundNamespaceContext(NOTIFICATION_NAMESPACE_CONTEXT, new DocumentNamespaceContext(doc));
168     }
169 
170     /**
171      * Returns an EntityResolver to resolve XML entities (namely schema resources) in the notification system
172      * @param notificationContentTypeService the NotificationContentTypeService
173      * @return an EntityResolver to resolve XML entities (namely schema resources) in the notification system
174      */
175     public static EntityResolver getNotificationEntityResolver(NotificationContentTypeService notificationContentTypeService) {
176         return new CompoundEntityResolver(new ClassLoaderEntityResolver("schema", "notification"),
177                                           new ContentTypeEntityResolver(notificationContentTypeService));
178     }
179 
180     /**
181      * transformContent - transforms xml content in notification to a string
182      * using the xsl in the datastore for a given documentType
183      * @param notification
184      * @return
185      */
186     public static String transformContent(Notification notification) {
187         NotificationContentType contentType = notification.getContentType();
188         String xsl = contentType.getXsl();
189         
190         LOG.debug("xsl: "+xsl);
191         
192         XslSourceResolver xslresolver = new XslSourceResolver();
193         //StreamSource xslsource = xslresolver.resolveXslFromFile(xslpath);
194         StreamSource xslsource = xslresolver.resolveXslFromString(xsl);
195         String content = notification.getContent();
196         LOG.debug("xslsource:"+xslsource.toString());
197         
198         String contenthtml = new String();
199         try {
200           ContentTransformer transformer = new ContentTransformer(xslsource);
201           contenthtml = transformer.transform(content);
202           LOG.debug("html: "+contenthtml);
203         } catch (IOException ex) {
204             LOG.error("IOException transforming document",ex);
205         } catch (Exception ex) {
206             LOG.error("Exception transforming document",ex);
207         } 
208         return contenthtml;
209     }
210 
211     /**
212      * This method uses DOM to parse the input source of XML.
213      * @param source the input source
214      * @param validate whether to turn on validation
215      * @param namespaceAware whether to turn on namespace awareness
216      * @return Document the parsed (possibly validated) document
217      * @throws ParserConfigurationException
218      * @throws IOException
219      * @throws SAXException
220      */
221     public static Document parse(final InputSource source, boolean validate, boolean namespaceAware, EntityResolver entityResolver) throws ParserConfigurationException, IOException, SAXException {
222         // TODO: optimize this
223         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
224         dbf.setValidating(validate);
225         dbf.setNamespaceAware(namespaceAware);
226         dbf.setAttribute(JAXPConstants.JAXP_SCHEMA_LANGUAGE, JAXPConstants.W3C_XML_SCHEMA);
227         DocumentBuilder db = dbf.newDocumentBuilder();
228         if (entityResolver != null) {
229             db.setEntityResolver(entityResolver);
230         }
231         db.setErrorHandler(new ErrorHandler() {
232             public void warning(SAXParseException se) {
233                 LOG.warn("Warning parsing xml doc " + source, se);
234             }
235             public void error(SAXParseException se) throws SAXException {
236                 LOG.error("Error parsing xml doc " + source, se);
237                 throw se;
238             }
239             public void fatalError(SAXParseException se) throws SAXException {
240                 LOG.error("Fatal error parsing xml doc " + source, se);
241                 throw se;
242             }
243         });
244         return db.parse(source);
245     }
246 
247     /**
248      * This method uses DOM to parse the input source of XML, supplying a notification-system-specific
249      * entity resolver.
250      * @param source the input source
251      * @param validate whether to turn on validation
252      * @param namespaceAware whether to turn on namespace awareness
253      * @return Document the parsed (possibly validated) document
254      * @throws ParserConfigurationException
255      * @throws IOException
256      * @throws SAXException
257      */
258     public static Document parseWithNotificationEntityResolver(final InputSource source, boolean validate, boolean namespaceAware, NotificationContentTypeService notificationContentTypeService) throws ParserConfigurationException, IOException, SAXException {
259         return parse(source, validate, namespaceAware, getNotificationEntityResolver(notificationContentTypeService));
260     }
261 
262     /**
263      * This method returns the value of the first chile of the element node.
264      * @param element
265      * @return String
266      */
267     public static String getTextContent(org.w3c.dom.Element element) {
268         NodeList children = element.getChildNodes();
269         Node node = children.item(0);
270         return node.getNodeValue();
271     }
272 
273     /**
274      * Returns a node child with the specified tag name of the specified parent node,
275      * or null if no such child node is found. 
276      * @param parent the parent node
277      * @param name the name of the child node
278      * @return child node if found, null otherwise
279      */
280     public static Element getChildElement(Node parent, String name) {
281         NodeList childList = parent.getChildNodes();
282         for (int i = 0; i < childList.getLength(); i++) {
283             Node node = childList.item(i);
284             // we must test against NodeName, not just LocalName
285             // LocalName seems to be null - I am guessing this is because
286             // the DocumentBuilderFactory is not "namespace aware"
287             // although I would have expected LocalName to default to
288             // NodeName
289             if (node.getNodeType() == Node.ELEMENT_NODE
290                 && (name.equals(node.getLocalName())
291                    || name.equals(node.getNodeName()))) {
292                 return (Element) node;
293             }
294         }
295         return null;
296     }
297     
298     /**
299      * Returns the text value of a child element with the given name, of the given parent element,
300      * or null if the child does not exist or does not have a child text node
301      * @param parent parent element
302      * @param name name of child element
303      * @return the text value of a child element with the given name, of the given parent element,
304      * or null if the child does not exist or does not have a child text node
305      */
306     public static String getChildElementTextValue(Node parent, String name) {
307         Element child = getChildElement(parent, name);
308         if (child == null) {
309             return null;
310         }
311         return child.getTextContent();
312     }
313 
314     /**
315      * Reads the entire contents of a stream and returns a byte array
316      * 
317      * @param stream
318      *            the stream to read fully
319      * @return a byte array containing the contents of the stream
320      * @throws IOException
321      */
322     public static byte[] readFully(InputStream stream) throws IOException {
323         byte[] buf = new byte[1024];
324         ByteArrayOutputStream baos = new ByteArrayOutputStream();
325         int read;
326         while ((read = stream.read(buf)) != -1) {
327             baos.write(buf, 0, read);
328         }
329         return baos.toByteArray();
330     }
331 
332     /**
333      * Serializes a node to XML (without indentation)
334      * @param node the node to serialize
335      * @return the serialized node
336      * @throws TransformerException if transformation fails
337      */
338     public static String writeNode(org.w3c.dom.Node node) throws TransformerException {
339         return writeNode(node, false);
340     }
341 
342     /**
343      * Serializes a node to XML
344      * @param node the node to serialize
345      * @param indent whether to apply indentation to the output
346      * @return the serialized node
347      * @throws TransformerException if transformation fails
348      */
349     public static String writeNode(org.w3c.dom.Node node, boolean indent) throws TransformerException {
350         Source source = new DOMSource(node);
351         StringWriter writer = new StringWriter();
352         Result result = new StreamResult(writer);
353         Transformer transformer = TransformerFactory.newInstance().newTransformer();
354         transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
355         if (indent) {
356             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
357         }
358         transformer.transform(source, result);
359         return writer.toString();
360     }
361     
362     /**
363      * This method will clone a given Notification object, one level deep, returning a fresh new instance 
364      * without any references.
365      * @param notification the object to clone
366      * @return Notification a fresh instance
367      */
368     public static final Notification cloneNotificationWithoutObjectReferences(Notification notification) {
369 	Notification clone = new Notification();
370 	
371 	// handle simple data types first
372         if(notification.getCreationDateTime() != null) {
373             clone.setCreationDateTime(new Timestamp(notification.getCreationDateTime().getTime()));
374         }
375 	if(notification.getAutoRemoveDateTime() != null) {
376 	    clone.setAutoRemoveDateTime(new Timestamp(notification.getAutoRemoveDateTime().getTime()));
377 	}
378 	clone.setContent(new String(notification.getContent()));
379 	clone.setDeliveryType(new String(notification.getDeliveryType()));
380 	if(notification.getId() != null) {
381 	    clone.setId(new Long(notification.getId()));
382 	}
383 	clone.setProcessingFlag(new String(notification.getProcessingFlag()));
384 	if(notification.getSendDateTime() != null) {
385 	    clone.setSendDateTime(new Timestamp(notification.getSendDateTime().getTime()));
386 	}
387 	
388         clone.setTitle(notification.getTitle());
389         
390 	// now take care of the channel
391 	NotificationChannel channel = new NotificationChannel();
392 	channel.setId(new Long(notification.getChannel().getId()));
393 	channel.setName(new String(notification.getChannel().getName()));
394 	channel.setDescription(new String(notification.getChannel().getDescription()));
395 	channel.setSubscribable(new Boolean(notification.getChannel().isSubscribable()).booleanValue());
396 	clone.setChannel(channel);
397 	
398 	// handle the content type
399 	NotificationContentType contentType = new NotificationContentType();
400 	contentType.setId(new Long(notification.getContentType().getId()));
401 	contentType.setDescription(new String(notification.getContentType().getDescription()));
402 	contentType.setName(new String(notification.getContentType().getName()));
403 	contentType.setNamespace(new String(notification.getContentType().getNamespace()));
404 	clone.setContentType(contentType);
405 	
406 	// take care of the prioirity
407 	NotificationPriority priority = new NotificationPriority();
408 	priority.setDescription(new String(notification.getPriority().getDescription()));
409 	priority.setId(new Long(notification.getPriority().getId()));
410 	priority.setName(new String(notification.getPriority().getName()));
411 	priority.setOrder(new Integer(notification.getPriority().getOrder()));
412 	clone.setPriority(priority);
413 	
414 	// take care of the producer
415 	NotificationProducer producer = new NotificationProducer();
416 	producer.setDescription(new String(notification.getProducer().getDescription()));
417 	producer.setId(new Long(notification.getProducer().getId()));
418 	producer.setName(new String(notification.getProducer().getName()));
419 	producer.setContactInfo(new String(notification.getProducer().getContactInfo()));
420 	clone.setProducer(producer);
421 	
422 	// process the list of recipients now
423 	ArrayList<NotificationRecipient> recipients = new ArrayList<NotificationRecipient>();
424 	for(int i = 0; i < notification.getRecipients().size(); i++) {
425 	    NotificationRecipient recipient = notification.getRecipient(i);
426 	    NotificationRecipient cloneRecipient = new NotificationRecipient();
427 	    cloneRecipient.setRecipientId(new String(recipient.getRecipientId()));
428 	    cloneRecipient.setRecipientType(new String(recipient.getRecipientType()));
429 	    
430 	    recipients.add(cloneRecipient);
431 	}
432 	clone.setRecipients(recipients);
433 	
434 	// process the list of senders now
435 	ArrayList<NotificationSender> senders = new ArrayList<NotificationSender>();
436 	for(int i = 0; i < notification.getSenders().size(); i++) {
437 	    NotificationSender sender = notification.getSender(i);
438 	    NotificationSender cloneSender = new NotificationSender();
439 	    cloneSender.setSenderName(new String(sender.getSenderName()));
440 	    
441 	    senders.add(cloneSender);
442 	}
443 	clone.setSenders(senders);
444 	
445 	return clone;
446     }
447     
448     /**
449      * This method generically retrieves a reference to foreign key objects that are part of the content, to get 
450      * at the reference objects' pk fields so that those values can be used to store the notification with proper 
451      * foreign key relationships in the database.
452      * @param <T>
453      * @param fieldName
454      * @param keyName
455      * @param keyValue
456      * @param clazz
457      * @param boDao
458      * @return T
459      * @throws IllegalArgumentException
460      */
461     public static <T> T retrieveFieldReference(String fieldName, String keyName, String keyValue, Class clazz, GenericDao boDao) throws IllegalArgumentException {
462         LOG.debug(fieldName + " key value: " + keyValue);
463         if (StringUtils.isBlank(keyValue)) {
464             throw new IllegalArgumentException(fieldName + " must be specified in notification");
465         }
466         Map<String, Object> keys = new HashMap<String, Object>(1);
467         keys.put(keyName, keyValue);
468         T reference = (T) boDao.findByPrimaryKey(clazz, keys);
469         if (reference == null) {
470             throw new IllegalArgumentException(fieldName + " '" + keyValue + "' not found");
471         }
472         return reference;
473     }
474     
475     /**
476      * This method searches for an exception of the specified type in the stack trace of the given
477      * exception.
478      * @param topLevelException the exception whose stack to traverse
479      * @param exceptionClass the exception class to look for
480      * @return the first instance of an exception of the specified class if found, or null otherwise
481      */
482     public static <T extends Throwable> T findExceptionInStack(Throwable topLevelException, Class<T> exceptionClass) {
483         Throwable t = topLevelException;
484         while (t != null) {
485             if (exceptionClass.isAssignableFrom(t.getClass())) return (T) t;
486             t = t.getCause();
487         }
488         return null;
489     }
490 }