001/** 002 * Copyright 2005-2017 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}