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.krad.workflow.attribute; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.apache.commons.logging.Log; 020 import org.apache.commons.logging.LogFactory; 021 import org.kuali.rice.core.api.util.KeyValue; 022 import org.kuali.rice.core.api.util.xml.XmlJotter; 023 import org.kuali.rice.kew.rule.xmlrouting.XPathHelper; 024 import org.kuali.rice.krad.keyvalues.KeyValuesFinder; 025 import org.kuali.rice.krad.service.DataDictionaryService; 026 import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 027 import org.w3c.dom.Element; 028 import org.w3c.dom.NamedNodeMap; 029 import org.w3c.dom.Node; 030 import org.w3c.dom.NodeList; 031 032 import javax.xml.transform.Result; 033 import javax.xml.transform.Source; 034 import javax.xml.transform.TransformerFactory; 035 import javax.xml.transform.dom.DOMSource; 036 import javax.xml.transform.stream.StreamResult; 037 import javax.xml.xpath.XPath; 038 import javax.xml.xpath.XPathConstants; 039 import javax.xml.xpath.XPathExpressionException; 040 import java.io.StringWriter; 041 import java.util.ArrayList; 042 import java.util.Iterator; 043 import java.util.List; 044 import java.util.regex.Matcher; 045 import java.util.regex.Pattern; 046 047 048 public class KualiXmlAttributeHelper { 049 private static Log LOG = LogFactory.getLog(KualiXmlAttributeHelper.class); 050 private static XPath xpath = XPathHelper.newXPath(); 051 private static final String testVal = "\'/[^\']*\'";// get the individual xpath tests. 052 private static final String testVal2 = "/[^/]+/" + "*";// have to do this or the compiler gets confused by end comment. 053 private static final String cleanVal = "[^/\']+";// get rid of / and ' in the resulting term. 054 private static final String ruledataVal = "ruledata[^\']*\'([^\']*)"; 055 // TODO - enter JIRA 056 // below removes wf:xstreamsafe( and ) 057 // below separates each wf:xstreamsafe() section into separate 'finds' 058 private static final Pattern xPathPattern = Pattern.compile(testVal); 059 private static final Pattern termPattern = Pattern.compile(testVal2); 060 private static final Pattern cleanPattern = Pattern.compile(cleanVal); 061 private static final Pattern targetPattern = Pattern.compile(ruledataVal); 062 063 public static final String ATTRIBUTE_LABEL_BO_REFERENCE_PREFIX = "kuali_dd_label("; 064 public static final String ATTRIBUTE_LABEL_BO_REFERENCE_SUFFIX = ")"; 065 public static final String ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_PREFIX = "kuali_dd_short_label("; 066 public static final String ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_SUFFIX = ")"; 067 private static final String KUALI_VALUES_FINDER_REFERENCE_PREFIX = "kuali_values_finder_class("; 068 private static final String KUALI_VALUES_FINDER_REFERENCE_SUFFIX = ")"; 069 public static final String notFound = "Label Not Found"; 070 071 private String lastXPath = ""; 072 073 /** 074 * This method overrides the super class and modifies the XML that it operates on to put the name and the title in the place 075 * where the super class expects to see them, even though they may no longer exist in the original XML. 076 * 077 * @see org.kuali.rice.kew.rule.xmlrouting.StandardGenericXMLRuleAttribute#getConfigXML() 078 */ 079 080 public Element processConfigXML(Element root) { 081 return this.processConfigXML(root, null); 082 } 083 084 /** 085 * This method overrides the super class and modifies the XML that it operates on to put the name and the title in the place 086 * where the super class expects to see them, overwriting the original title in the XML. 087 * 088 * @see org.kuali.rice.kew.rule.xmlrouting.StandardGenericXMLRuleAttribute#getConfigXML() 089 */ 090 091 public Element processConfigXML(Element root, String[] xpathExpressionElements) { 092 093 NodeList fields = root.getElementsByTagName("fieldDef"); 094 Element theTag = null; 095 String docContent = ""; 096 097 098 /** 099 * This section will check to see if document content has been defined in the configXML for the document type, by running an 100 * XPath. If this is an empty list the xpath expression in the fieldDef is used to define the xml document content that is 101 * added to the configXML. The xmldocument content is of this form, when in the document configXML. <xmlDocumentContent> 102 * <org.kuali.rice.krad.bo.SourceAccountingLine> <amount> <value>%totaldollarAmount%</value> </amount> 103 * </org.kuali.rice.krad.bo.SourceAccountingLine> </xmlDocumentContent> This class generates this on the fly, by creating an XML 104 * element for each term in the XPath expression. When this doesn't apply XML can be coded in the configXML for the 105 * ruleAttribute. 106 * 107 * @see org.kuali.rice.kew.plugin.attributes.WorkflowAttribute#getDocContent() 108 */ 109 110 111 org.w3c.dom.Document xmlDoc = null; 112 if (!xmlDocumentContentExists(root)) { // XML Document content is given because the xpath is non standard 113 fields = root.getElementsByTagName("fieldDef"); 114 xmlDoc = root.getOwnerDocument(); 115 } 116 for (int i = 0; i < fields.getLength(); i++) { // loop over each fieldDef 117 String name = null; 118 if (!xmlDocumentContentExists(root)) { 119 theTag = (Element) fields.item(i); 120 121 /* 122 * Even though there may be multiple xpath test, for example one for source lines and one for target lines, the 123 * xmlDocumentContent only needs one, since it is used for formatting. The first one is arbitrarily selected, since 124 * they are virtually equivalent in structure, most of the time. 125 */ 126 127 List<String> xPathTerms = getXPathTerms(theTag); 128 if (xPathTerms.size() != 0) { 129 Node iterNode = xmlDoc.createElement("xmlDocumentContent"); 130 131 132 xmlDoc.normalize(); 133 134 iterNode.normalize(); 135 136 /* 137 * Since this method is run once per attribute and there may be multiple fieldDefs, the first fieldDef is used 138 * to create the configXML. 139 */ 140 for (int j = 0; j < xPathTerms.size(); j++) {// build the configXML based on the Xpath 141 // TODO - Fix the document content element generation 142 iterNode.appendChild(xmlDoc.createElement(xPathTerms.get(j))); 143 xmlDoc.normalize(); 144 145 iterNode = iterNode.getFirstChild(); 146 iterNode.normalize(); 147 148 } 149 iterNode.setTextContent("%" + xPathTerms.get(xPathTerms.size() - 1) + "%"); 150 root.appendChild(iterNode); 151 } 152 } 153 theTag = (Element) fields.item(i); 154 // check to see if a values finder is being used to set valid values for a field 155 NodeList displayTagElements = theTag.getElementsByTagName("display"); 156 if (displayTagElements.getLength() == 1) { 157 Element displayTag = (Element) displayTagElements.item(0); 158 List valuesElementsToAdd = new ArrayList(); 159 for (int w = 0; w < displayTag.getChildNodes().getLength(); w++) { 160 Node displayTagChildNode = (Node) displayTag.getChildNodes().item(w); 161 if ((displayTagChildNode != null) && ("values".equals(displayTagChildNode.getNodeName()))) { 162 if (displayTagChildNode.getChildNodes().getLength() > 0) { 163 String valuesNodeText = displayTagChildNode.getFirstChild().getNodeValue(); 164 String potentialClassName = getPotentialKualiClassName(valuesNodeText, KUALI_VALUES_FINDER_REFERENCE_PREFIX, KUALI_VALUES_FINDER_REFERENCE_SUFFIX); 165 if (StringUtils.isNotBlank(potentialClassName)) { 166 try { 167 Class finderClass = Class.forName((String) potentialClassName); 168 KeyValuesFinder finder = (KeyValuesFinder) finderClass.newInstance(); 169 NamedNodeMap valuesNodeAttributes = displayTagChildNode.getAttributes(); 170 Node potentialSelectedAttribute = (valuesNodeAttributes != null) ? valuesNodeAttributes.getNamedItem("selected") : null; 171 for (Iterator iter = finder.getKeyValues().iterator(); iter.hasNext();) { 172 KeyValue keyValue = (KeyValue) iter.next(); 173 Element newValuesElement = root.getOwnerDocument().createElement("values"); 174 newValuesElement.appendChild(root.getOwnerDocument().createTextNode(keyValue.getKey())); 175 // newValuesElement.setNodeValue(KeyValue.getKey().toString()); 176 newValuesElement.setAttribute("title", keyValue.getValue()); 177 if (potentialSelectedAttribute != null) { 178 newValuesElement.setAttribute("selected", potentialSelectedAttribute.getNodeValue()); 179 } 180 valuesElementsToAdd.add(newValuesElement); 181 } 182 } catch (ClassNotFoundException cnfe) { 183 String errorMessage = "Caught an exception trying to find class '" + potentialClassName + "'"; 184 LOG.error(errorMessage, cnfe); 185 throw new RuntimeException(errorMessage, cnfe); 186 } catch (InstantiationException ie) { 187 String errorMessage = "Caught an exception trying to instantiate class '" + potentialClassName + "'"; 188 LOG.error(errorMessage, ie); 189 throw new RuntimeException(errorMessage, ie); 190 } catch (IllegalAccessException iae) { 191 String errorMessage = "Caught an access exception trying to instantiate class '" + potentialClassName + "'"; 192 LOG.error(errorMessage, iae); 193 throw new RuntimeException(errorMessage, iae); 194 } 195 } else { 196 valuesElementsToAdd.add(displayTagChildNode.cloneNode(true)); 197 } 198 displayTag.removeChild(displayTagChildNode); 199 } 200 } 201 } 202 for (Iterator iter = valuesElementsToAdd.iterator(); iter.hasNext();) { 203 Element valuesElementToAdd = (Element) iter.next(); 204 displayTag.appendChild(valuesElementToAdd); 205 } 206 } 207 if ((xpathExpressionElements != null) && (xpathExpressionElements.length > 0)) { 208 NodeList fieldEvaluationElements = theTag.getElementsByTagName("fieldEvaluation"); 209 if (fieldEvaluationElements.getLength() == 1) { 210 Element fieldEvaluationTag = (Element) fieldEvaluationElements.item(0); 211 List tagsToAdd = new ArrayList(); 212 for (int w = 0; w < fieldEvaluationTag.getChildNodes().getLength(); w++) { 213 Node fieldEvaluationChildNode = (Node) fieldEvaluationTag.getChildNodes().item(w); 214 Element newTagToAdd = null; 215 if ((fieldEvaluationChildNode != null) && ("xpathexpression".equals(fieldEvaluationChildNode.getNodeName()))) { 216 newTagToAdd = root.getOwnerDocument().createElement("xpathexpression"); 217 newTagToAdd.appendChild(root.getOwnerDocument().createTextNode(generateNewXpathExpression(fieldEvaluationChildNode.getFirstChild().getNodeValue(), xpathExpressionElements))); 218 tagsToAdd.add(newTagToAdd); 219 fieldEvaluationTag.removeChild(fieldEvaluationChildNode); 220 } 221 } 222 for (Iterator iter = tagsToAdd.iterator(); iter.hasNext();) { 223 Element elementToAdd = (Element) iter.next(); 224 fieldEvaluationTag.appendChild(elementToAdd); 225 } 226 } 227 } 228 theTag.setAttribute("title", getBusinessObjectTitle(theTag)); 229 230 } 231 if (LOG.isDebugEnabled()) { 232 LOG.debug(XmlJotter.jotNode(root)); 233 StringWriter xmlBuffer = new StringWriter(); 234 try { 235 236 root.normalize(); 237 Source source = new DOMSource(root); 238 Result result = new StreamResult(xmlBuffer); 239 TransformerFactory.newInstance().newTransformer().transform(source, result); 240 } catch (Exception e) { 241 LOG.debug(" Exception when printing debug XML output " + e); 242 } 243 LOG.debug(xmlBuffer.getBuffer()); 244 } 245 246 return root; 247 } 248 249 private String generateNewXpathExpression(String currentXpathExpression, String[] newXpathExpressionElements) { 250 StringBuffer returnableString = new StringBuffer(); 251 for (int i = 0; i < newXpathExpressionElements.length; i++) { 252 String newXpathElement = newXpathExpressionElements[i]; 253 returnableString.append(newXpathElement); 254 255 /* 256 * Append the given xpath expression onto the end of the stringbuffer only in the following cases - if there is only one 257 * element in the string array - if there is more than one element in the string array and if the current element is not 258 * the last element 259 */ 260 if (((i + 1) != newXpathExpressionElements.length) || (newXpathExpressionElements.length == 1)) { 261 returnableString.append(currentXpathExpression); 262 } 263 } 264 return returnableString.toString(); 265 } 266 267 /** 268 * This method gets all of the text from the xpathexpression element. 269 * 270 * @param root 271 * @return 272 */ 273 private String getXPathText(Element root) { 274 try { 275 String textContent = null; 276 Node node = (Node) xpath.evaluate(".//xpathexpression", root, XPathConstants.NODE); 277 if (node != null) { 278 textContent = node.getTextContent(); 279 } 280 return textContent; 281 } catch (XPathExpressionException e) { 282 LOG.error("No XPath expression text found in element xpathexpression of configXML for document. " + e); 283 return null; 284 // throw e; Just writing labels or doing routing report. 285 } 286 } 287 288 /** 289 * This method uses an XPath expression to determine if the content of the xmlDocumentContent is empty 290 * 291 * @param root 292 * @return 293 */ 294 private boolean xmlDocumentContentExists(Element root) { 295 try { 296 if (((NodeList) xpath.evaluate("//xmlDocumentContent", root, XPathConstants.NODESET)).getLength() == 0) { 297 return false; 298 } 299 } catch (XPathExpressionException e) { 300 LOG.error("Error parsing xmlDocumentConfig. " + e); 301 return false; 302 } 303 return true; 304 } 305 306 public static String getPotentialKualiClassName(String testString, String prefixIndicator, String suffixIndicator) { 307 if ((StringUtils.isNotBlank(testString)) && (testString.startsWith(prefixIndicator)) && (testString.endsWith(suffixIndicator))) { 308 return testString.substring(prefixIndicator.length(), testString.lastIndexOf(suffixIndicator)); 309 } 310 return null; 311 } 312 313 /** 314 * Method to look up the title of each fieldDef tag in the RuleAttribute xml. This method checks the following items in the 315 * following order: 316 * <ol> 317 * <li>Check for the business object name from {@link #getBusinessObjectName(Element)}. If it is not found or blank and the 318 * 'title' attribute of the fieldDef tag is specified then return the value of the 'title' attribute. 319 * <li>Check for the business object name from {@link #getBusinessObjectName(Element)}. If it is found try getting the data 320 * dictionary label related to the business object name and the attribute name (found in the xpath expression) 321 * <li>Check for the data dictionary title value using the attribute name (found in the xpath expression) and the KFS stand in 322 * business object for attributes (see {@link KFSConstants#STAND_IN_BUSINESS_OBJECT_FOR_ATTRIBUTES} 323 * <li>Check for the data dictionary title value using the xpath attribute name found in the xpath expression section. Use that 324 * attribute name to get the label out of the KFS stand in business object for attributes (see 325 * {@link KFSConstants#STAND_IN_BUSINESS_OBJECT_FOR_ATTRIBUTES} 326 * <li>Check for the data dictionary title value using the xpath attribute name found in the xpath expression in the 327 * wf:ruledata() section. Use that attribute name to get the label out of the KFS stand in business object for attributes (see 328 * {@link KFSConstants#STAND_IN_BUSINESS_OBJECT_FOR_ATTRIBUTES} 329 * </ol> 330 * 331 * @param root - the element of the fieldDef tag 332 */ 333 private String getBusinessObjectTitle(Element root) { 334 String businessObjectName = null; 335 String businessObjectText = root.getAttribute("title"); 336 String potentialClassNameLongLabel = getPotentialKualiClassName(businessObjectText, ATTRIBUTE_LABEL_BO_REFERENCE_PREFIX, ATTRIBUTE_LABEL_BO_REFERENCE_SUFFIX); 337 String potentialClassNameShortLabel = getPotentialKualiClassName(businessObjectText, ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_PREFIX, ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_SUFFIX); 338 // we assume they want the long label... but allow for the short label 339 boolean requestedShortLabel = false; 340 341 if (StringUtils.isNotBlank(potentialClassNameLongLabel)) { 342 businessObjectName = potentialClassNameLongLabel; 343 } else if (StringUtils.isNotBlank(potentialClassNameShortLabel)) { 344 businessObjectName = potentialClassNameShortLabel; 345 requestedShortLabel = true; 346 } 347 if (StringUtils.isNotBlank(businessObjectName)) { 348 DataDictionaryService DDService = KRADServiceLocatorWeb.getDataDictionaryService(); 349 350 String title = null; 351 String targetVal = lastXPath; // Assume the attribute is the last term in the XPath expression 352 353 if (LOG.isErrorEnabled()) { 354 LOG.debug("Finding title in BO=" + businessObjectName + " ObjectName=" + targetVal); 355 } 356 357 if (StringUtils.isNotBlank(targetVal)) { 358 // try to get the label based on the bo name and xpath attribute 359 if (requestedShortLabel) { 360 title = DDService.getAttributeShortLabel(businessObjectName, targetVal); 361 } else { 362 title = DDService.getAttributeLabel(businessObjectName, targetVal); 363 } 364 if (StringUtils.isNotBlank(title)) { 365 return title; 366 } 367 } 368 // try to get the label based on the business object and xpath ruledata section 369 targetVal = getRuleData(root); 370 if (LOG.isErrorEnabled()) { 371 LOG.debug("Finding title in BO=" + businessObjectName + " ObjectName=" + targetVal); 372 } 373 if (StringUtils.isNotBlank(targetVal)) { 374 title = DDService.getAttributeLabel(businessObjectName, targetVal); 375 if (StringUtils.isNotBlank(title)) { 376 return title; 377 } 378 } 379 // If haven't found a label yet, its probably because there is no xpath. Use the name attribute to determine the BO 380 // attribute to use. 381 targetVal = root.getAttribute("name"); 382 if (LOG.isErrorEnabled()) { 383 LOG.debug("Finding title in BO=" + businessObjectName + " ObjectName=" + targetVal); 384 } 385 title = DDService.getAttributeLabel(businessObjectName, targetVal); 386 387 if (StringUtils.isNotBlank(title)) { 388 return title; 389 } 390 } 391 // return any potentially hard coded title info 392 else if ((StringUtils.isNotBlank(businessObjectText)) && (StringUtils.isBlank(businessObjectName))) { 393 return businessObjectText; 394 } 395 return notFound; 396 397 } 398 399 /** 400 * This method gets the contents of the ruledata function in the xpath statement in the XML 401 * 402 * @param root 403 * @return 404 */ 405 private String getRuleData(Element root) { 406 String xPathRuleTarget = getXPathText(root); 407 408 // This pattern may need to change to get the last stanza of the xpath 409 if (StringUtils.isNotBlank(xPathRuleTarget)) { 410 Matcher ruleTarget = targetPattern.matcher(xPathRuleTarget); 411 if (ruleTarget.find()) { 412 xPathRuleTarget = ruleTarget.group(1); 413 } 414 } 415 return xPathRuleTarget; 416 } 417 418 private List<String> getXPathTerms(Element myTag) { 419 420 Matcher xPathTarget; 421 String firstMatch; 422 List<String> xPathTerms = new ArrayList(); 423 String allText = getXPathText(myTag);// grab the whole xpath expression 424 if (StringUtils.isNotBlank(allText)) { 425 xPathTarget = xPathPattern.matcher(allText); 426 Matcher termTarget; 427 Matcher cleanTarget; 428 int theEnd = 0;// Have to define this or the / gets used up with the match and every other term is returned. 429 430 xPathTarget.find(theEnd); 431 theEnd = xPathTarget.end() - 1; 432 firstMatch = xPathTarget.group(); 433 434 435 termTarget = termPattern.matcher(firstMatch); 436 int theEnd2 = 0; 437 while (termTarget.find(theEnd2)) { // get each term, clean them up, and add to the list. 438 theEnd2 = termTarget.end() - 1; 439 cleanTarget = cleanPattern.matcher(termTarget.group()); 440 cleanTarget.find(); 441 lastXPath = cleanTarget.group(); 442 xPathTerms.add(lastXPath); 443 444 } 445 } 446 return xPathTerms; 447 } 448 449 private String getLastXPath(Element root) { 450 List<String> tempList = getXPathTerms(root); 451 return tempList.get(tempList.size()); 452 } 453 }