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