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