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.util;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.config.property.ConfigContext;
021import org.kuali.rice.core.framework.persistence.dao.GenericDao;
022import org.kuali.rice.ken.bo.NotificationBo;
023import org.kuali.rice.ken.bo.NotificationChannelBo;
024import org.kuali.rice.ken.bo.NotificationContentTypeBo;
025import org.kuali.rice.ken.bo.NotificationPriorityBo;
026import org.kuali.rice.ken.bo.NotificationProducerBo;
027import org.kuali.rice.ken.bo.NotificationRecipientBo;
028import org.kuali.rice.ken.bo.NotificationSenderBo;
029import org.kuali.rice.ken.service.NotificationContentTypeService;
030import org.w3c.dom.Document;
031import org.w3c.dom.Element;
032import org.w3c.dom.Node;
033import org.w3c.dom.NodeList;
034import org.xml.sax.EntityResolver;
035import org.xml.sax.ErrorHandler;
036import org.xml.sax.InputSource;
037import org.xml.sax.SAXException;
038import org.xml.sax.SAXParseException;
039
040import javax.xml.namespace.NamespaceContext;
041import javax.xml.parsers.DocumentBuilder;
042import javax.xml.parsers.DocumentBuilderFactory;
043import javax.xml.parsers.ParserConfigurationException;
044import javax.xml.transform.stream.StreamSource;
045import java.io.IOException;
046import java.sql.Timestamp;
047import java.text.DateFormat;
048import java.text.ParseException;
049import java.text.SimpleDateFormat;
050import java.util.ArrayList;
051import java.util.Collections;
052import java.util.Date;
053import java.util.HashMap;
054import java.util.Map;
055import java.util.TimeZone;
056
057/**
058 * A general Utility class for the Notification system.
059 * @author Kuali Rice Team (rice.collab@kuali.org)
060 */
061public 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}