View Javadoc

1   /*
2    * Copyright 2007 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl2.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kns.workflow.attribute;
17  
18  import java.io.StringWriter;
19  import java.util.ArrayList;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  import javax.xml.transform.Result;
26  import javax.xml.transform.Source;
27  import javax.xml.transform.TransformerFactory;
28  import javax.xml.transform.dom.DOMSource;
29  import javax.xml.transform.stream.StreamResult;
30  import javax.xml.xpath.XPath;
31  import javax.xml.xpath.XPathConstants;
32  import javax.xml.xpath.XPathExpressionException;
33  
34  import org.apache.commons.lang.StringUtils;
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.kuali.rice.core.util.KeyLabelPair;
38  import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
39  import org.kuali.rice.kew.util.XmlHelper;
40  import org.kuali.rice.kns.lookup.keyvalues.KeyValuesFinder;
41  import org.kuali.rice.kns.service.DataDictionaryService;
42  import org.kuali.rice.kns.service.KNSServiceLocator;
43  import org.w3c.dom.Element;
44  import org.w3c.dom.NamedNodeMap;
45  import org.w3c.dom.Node;
46  import org.w3c.dom.NodeList;
47  
48  
49  public class KualiXmlAttributeHelper {
50      private static Log LOG = LogFactory.getLog(KualiXmlRuleAttributeImpl.class);
51      private static XPath xpath = XPathHelper.newXPath();
52      private static final String testVal = "\'/[^\']*\'";// get the individual xpath tests.
53      private static final String testVal2 = "/[^/]+/" + "*";// have to do this or the compiler gets confused by end comment.
54      private static final String cleanVal = "[^/\']+";// get rid of / and ' in the resulting term.
55      private static final String ruledataVal = "ruledata[^\']*\'([^\']*)";
56      // TODO - enter JIRA
57      // below removes wf:xstreamsafe( and )
58      // below separates each wf:xstreamsafe() section into separate 'finds'
59      private static final Pattern xPathPattern = Pattern.compile(testVal);
60      private static final Pattern termPattern = Pattern.compile(testVal2);
61      private static final Pattern cleanPattern = Pattern.compile(cleanVal);
62      private static final Pattern targetPattern = Pattern.compile(ruledataVal);
63  
64      public static final String ATTRIBUTE_LABEL_BO_REFERENCE_PREFIX = "kuali_dd_label(";
65      public static final String ATTRIBUTE_LABEL_BO_REFERENCE_SUFFIX = ")";
66      public static final String ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_PREFIX = "kuali_dd_short_label(";
67      public static final String ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_SUFFIX = ")";
68      private static final String KUALI_VALUES_FINDER_REFERENCE_PREFIX = "kuali_values_finder_class(";
69      private static final String KUALI_VALUES_FINDER_REFERENCE_SUFFIX = ")";
70      public static final String notFound = "Label Not Found";
71  
72      private String lastXPath = "";
73  
74      /**
75       * This method overrides the super class and modifies the XML that it operates on to put the name and the title in the place
76       * where the super class expects to see them, even though they may no longer exist in the original XML.
77       * 
78       * @see org.kuali.rice.kew.rule.xmlrouting.StandardGenericXMLRuleAttribute#getConfigXML()
79       */
80  
81      public Element processConfigXML(Element root) {
82          return this.processConfigXML(root, null);
83      }
84  
85      /**
86       * This method overrides the super class and modifies the XML that it operates on to put the name and the title in the place
87       * where the super class expects to see them, overwriting the original title in the XML.
88       * 
89       * @see org.kuali.rice.kew.rule.xmlrouting.StandardGenericXMLRuleAttribute#getConfigXML()
90       */
91  
92      public Element processConfigXML(Element root, String[] xpathExpressionElements) {
93  
94          NodeList fields = root.getElementsByTagName("fieldDef");
95          Element theTag = null;
96          String docContent = "";
97  
98  
99          /**
100          * This section will check to see if document content has been defined in the configXML for the document type, by running an
101          * XPath. If this is an empty list the xpath expression in the fieldDef is used to define the xml document content that is
102          * added to the configXML. The xmldocument content is of this form, when in the document configXML. <xmlDocumentContent>
103          * <org.kuali.rice.kns.bo.SourceAccountingLine> <amount> <value>%totaldollarAmount%</value> </amount>
104          * </org.kuali.rice.kns.bo.SourceAccountingLine> </xmlDocumentContent> This class generates this on the fly, by creating an XML
105          * element for each term in the XPath expression. When this doesn't apply XML can be coded in the configXML for the
106          * ruleAttribute.
107          * 
108          * @see org.kuali.rice.kew.plugin.attributes.WorkflowAttribute#getDocContent()
109          */
110 
111 
112         org.w3c.dom.Document xmlDoc = null;
113         if (!xmlDocumentContentExists(root)) { // XML Document content is given because the xpath is non standard
114             fields = root.getElementsByTagName("fieldDef");
115             xmlDoc = root.getOwnerDocument();
116         }
117         for (int i = 0; i < fields.getLength(); i++) { // loop over each fieldDef
118             String name = null;
119             if (!xmlDocumentContentExists(root)) {
120                 theTag = (Element) fields.item(i);
121 
122                 /*
123                  * Even though there may be multiple xpath test, for example one for source lines and one for target lines, the
124                  * xmlDocumentContent only needs one, since it is used for formatting. The first one is arbitrarily selected, since
125                  * they are virtually equivalent in structure, most of the time.
126                  */
127 
128                 List<String> xPathTerms = getXPathTerms(theTag);
129                 if (xPathTerms.size() != 0) {
130                     Node iterNode = xmlDoc.createElement("xmlDocumentContent");
131 
132 
133                     xmlDoc.normalize();
134 
135                     iterNode.normalize();
136 
137                     /*
138                      * Since this method is run once per attribute and there may be multiple fieldDefs, the first fieldDef is used
139                      * to create the configXML.
140                      */
141                     for (int j = 0; j < xPathTerms.size(); j++) {// build the configXML based on the Xpath
142                         // TODO - Fix the document content element generation
143                         iterNode.appendChild(xmlDoc.createElement(xPathTerms.get(j)));
144                         xmlDoc.normalize();
145 
146                         iterNode = iterNode.getFirstChild();
147                         iterNode.normalize();
148 
149                     }
150                     iterNode.setTextContent("%" + xPathTerms.get(xPathTerms.size() - 1) + "%");
151                     root.appendChild(iterNode);
152                 }
153             }
154             theTag = (Element) fields.item(i);
155             // check to see if a values finder is being used to set valid values for a field
156             NodeList displayTagElements = theTag.getElementsByTagName("display");
157             if (displayTagElements.getLength() == 1) {
158                 Element displayTag = (Element) displayTagElements.item(0);
159                 List valuesElementsToAdd = new ArrayList();
160                 for (int w = 0; w < displayTag.getChildNodes().getLength(); w++) {
161                     Node displayTagChildNode = (Node) displayTag.getChildNodes().item(w);
162                     if ((displayTagChildNode != null) && ("values".equals(displayTagChildNode.getNodeName()))) {
163                         if (displayTagChildNode.getChildNodes().getLength() > 0) {
164                             String valuesNodeText = displayTagChildNode.getFirstChild().getNodeValue();
165                             String potentialClassName = getPotentialKualiClassName(valuesNodeText, KUALI_VALUES_FINDER_REFERENCE_PREFIX, KUALI_VALUES_FINDER_REFERENCE_SUFFIX);
166                             if (StringUtils.isNotBlank(potentialClassName)) {
167                                 try {
168                                     Class finderClass = Class.forName((String) potentialClassName);
169                                     KeyValuesFinder finder = (KeyValuesFinder) finderClass.newInstance();
170                                     NamedNodeMap valuesNodeAttributes = displayTagChildNode.getAttributes();
171                                     Node potentialSelectedAttribute = (valuesNodeAttributes != null) ? valuesNodeAttributes.getNamedItem("selected") : null;
172                                     for (Iterator iter = finder.getKeyValues().iterator(); iter.hasNext();) {
173                                         KeyLabelPair keyLabelPair = (KeyLabelPair) iter.next();
174                                         Element newValuesElement = root.getOwnerDocument().createElement("values");
175                                         newValuesElement.appendChild(root.getOwnerDocument().createTextNode(keyLabelPair.getKey().toString()));
176                                         // newValuesElement.setNodeValue(keyLabelPair.getKey().toString());
177                                         newValuesElement.setAttribute("title", keyLabelPair.getLabel());
178                                         if (potentialSelectedAttribute != null) {
179                                             newValuesElement.setAttribute("selected", potentialSelectedAttribute.getNodeValue());
180                                         }
181                                         valuesElementsToAdd.add(newValuesElement);
182                                     }
183                                 }
184                                 catch (ClassNotFoundException cnfe) {
185                                     String errorMessage = "Caught an exception trying to find class '" + potentialClassName + "'";
186                                     LOG.error(errorMessage, cnfe);
187                                     throw new RuntimeException(errorMessage, cnfe);
188                                 }
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                                 }
194                                 catch (IllegalAccessException iae) {
195                                     String errorMessage = "Caught an access exception trying to instantiate class '" + potentialClassName + "'";
196                                     LOG.error(errorMessage, iae);
197                                     throw new RuntimeException(errorMessage, iae);
198                                 }
199                             }
200                             else {
201                                 valuesElementsToAdd.add(displayTagChildNode.cloneNode(true));
202                             }
203                             displayTag.removeChild(displayTagChildNode);
204                         }
205                     }
206                 }
207                 for (Iterator iter = valuesElementsToAdd.iterator(); iter.hasNext();) {
208                     Element valuesElementToAdd = (Element) iter.next();
209                     displayTag.appendChild(valuesElementToAdd);
210                 }
211             }
212             if ((xpathExpressionElements != null) && (xpathExpressionElements.length > 0)) {
213                 NodeList fieldEvaluationElements = theTag.getElementsByTagName("fieldEvaluation");
214                 if (fieldEvaluationElements.getLength() == 1) {
215                     Element fieldEvaluationTag = (Element) fieldEvaluationElements.item(0);
216                     List tagsToAdd = new ArrayList();
217                     for (int w = 0; w < fieldEvaluationTag.getChildNodes().getLength(); w++) {
218                         Node fieldEvaluationChildNode = (Node) fieldEvaluationTag.getChildNodes().item(w);
219                         Element newTagToAdd = null;
220                         if ((fieldEvaluationChildNode != null) && ("xpathexpression".equals(fieldEvaluationChildNode.getNodeName()))) {
221                             newTagToAdd = root.getOwnerDocument().createElement("xpathexpression");
222                             newTagToAdd.appendChild(root.getOwnerDocument().createTextNode(generateNewXpathExpression(fieldEvaluationChildNode.getFirstChild().getNodeValue(), xpathExpressionElements)));
223                             tagsToAdd.add(newTagToAdd);
224                             fieldEvaluationTag.removeChild(fieldEvaluationChildNode);
225                         }
226                     }
227                     for (Iterator iter = tagsToAdd.iterator(); iter.hasNext();) {
228                         Element elementToAdd = (Element) iter.next();
229                         fieldEvaluationTag.appendChild(elementToAdd);
230                     }
231                 }
232             }
233             theTag.setAttribute("title", getBusinessObjectTitle(theTag));
234 
235         }
236         if (LOG.isDebugEnabled()) {
237             LOG.debug(XmlHelper.jotNode(root));
238             StringWriter xmlBuffer = new StringWriter();
239             try {
240 
241                 root.normalize();
242                 Source source = new DOMSource(root);
243                 Result result = new StreamResult(xmlBuffer);
244                 TransformerFactory.newInstance().newTransformer().transform(source, result);
245             }
246             catch (Exception e) {
247                 LOG.debug(" Exception when printing debug XML output " + e);
248             }
249             LOG.debug(xmlBuffer.getBuffer());
250         }
251 
252         return root;
253     }
254 
255     private String generateNewXpathExpression(String currentXpathExpression, String[] newXpathExpressionElements) {
256         StringBuffer returnableString = new StringBuffer();
257         for (int i = 0; i < newXpathExpressionElements.length; i++) {
258             String newXpathElement = newXpathExpressionElements[i];
259             returnableString.append(newXpathElement);
260 
261             /*
262              * Append the given xpath expression onto the end of the stringbuffer only in the following cases - if there is only one
263              * element in the string array - if there is more than one element in the string array and if the current element is not
264              * the last element
265              */
266             if (((i + 1) != newXpathExpressionElements.length) || (newXpathExpressionElements.length == 1)) {
267                 returnableString.append(currentXpathExpression);
268             }
269         }
270         return returnableString.toString();
271     }
272 
273     /**
274      * This method gets all of the text from the xpathexpression element.
275      * 
276      * @param root
277      * @return
278      */
279     private String getXPathText(Element root) {
280         try {
281             String textContent = null;
282             Node node = (Node) xpath.evaluate(".//xpathexpression", root, XPathConstants.NODE);
283             if (node != null) {
284                 textContent = node.getTextContent();
285             }
286             return textContent;
287         }
288         catch (XPathExpressionException e) {
289             LOG.error("No XPath expression text found in element xpathexpression of configXML for document. " + e);
290             return null;
291             // throw e; Just writing labels or doing routing report.
292         }
293     }
294 
295     /**
296      * This method uses an XPath expression to determine if the content of the xmlDocumentContent is empty
297      * 
298      * @param root
299      * @return
300      */
301     private boolean xmlDocumentContentExists(Element root) {
302         try {
303             if (((NodeList) xpath.evaluate("//xmlDocumentContent", root, XPathConstants.NODESET)).getLength() == 0) {
304                 return false;
305             }
306         }
307         catch (XPathExpressionException e) {
308             LOG.error("Error parsing xmlDocumentConfig.  " + e);
309             return false;
310         }
311         return true;
312     }
313 
314     public static String getPotentialKualiClassName(String testString, String prefixIndicator, String suffixIndicator) {
315         if ((StringUtils.isNotBlank(testString)) && (testString.startsWith(prefixIndicator)) && (testString.endsWith(suffixIndicator))) {
316             return testString.substring(prefixIndicator.length(), testString.lastIndexOf(suffixIndicator));
317         }
318         return null;
319     }
320 
321     /**
322      * Method to look up the title of each fieldDef tag in the RuleAttribute xml. This method checks the following items in the
323      * following order:
324      * <ol>
325      * <li>Check for the business object name from {@link #getBusinessObjectName(Element)}. If it is not found or blank and the
326      * 'title' attribute of the fieldDef tag is specified then return the value of the 'title' attribute.
327      * <li>Check for the business object name from {@link #getBusinessObjectName(Element)}. If it is found try getting the data
328      * dictionary label related to the business object name and the attribute name (found in the xpath expression)
329      * <li>Check for the data dictionary title value using the attribute name (found in the xpath expression) and the KFS stand in
330      * business object for attributes (see {@link KFSConstants#STAND_IN_BUSINESS_OBJECT_FOR_ATTRIBUTES}
331      * <li>Check for the data dictionary title value using the xpath attribute name found in the xpath expression section. Use that
332      * attribute name to get the label out of the KFS stand in business object for attributes (see
333      * {@link KFSConstants#STAND_IN_BUSINESS_OBJECT_FOR_ATTRIBUTES}
334      * <li>Check for the data dictionary title value using the xpath attribute name found in the xpath expression in the
335      * wf:ruledata() section. Use that attribute name to get the label out of the KFS stand in business object for attributes (see
336      * {@link KFSConstants#STAND_IN_BUSINESS_OBJECT_FOR_ATTRIBUTES}
337      * </ol>
338      * 
339      * @param root - the element of the fieldDef tag
340      */
341     private String getBusinessObjectTitle(Element root) {
342         String businessObjectName = null;
343         String businessObjectText = root.getAttribute("title");
344         String potentialClassNameLongLabel = getPotentialKualiClassName(businessObjectText, ATTRIBUTE_LABEL_BO_REFERENCE_PREFIX, ATTRIBUTE_LABEL_BO_REFERENCE_SUFFIX);
345         String potentialClassNameShortLabel = getPotentialKualiClassName(businessObjectText, ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_PREFIX, ATTRIBUTE_SHORT_LABEL_BO_REFERENCE_SUFFIX);
346         // we assume they want the long label... but allow for the short label
347         boolean requestedShortLabel = false;
348 
349         if (StringUtils.isNotBlank(potentialClassNameLongLabel)) {
350             businessObjectName = potentialClassNameLongLabel;
351         }
352         else if (StringUtils.isNotBlank(potentialClassNameShortLabel)) {
353             businessObjectName = potentialClassNameShortLabel;
354             requestedShortLabel = true;
355         }
356         if (StringUtils.isNotBlank(businessObjectName)) {
357             DataDictionaryService DDService = KNSServiceLocator.getDataDictionaryService();
358 
359             String title = null;
360             String targetVal = lastXPath; // Assume the attribute is the last term in the XPath expression
361 
362             if (LOG.isErrorEnabled()) {
363                 LOG.debug("Finding title in BO=" + businessObjectName + " ObjectName=" + targetVal);
364             }
365 
366             if (StringUtils.isNotBlank(targetVal)) {
367                 // try to get the label based on the bo name and xpath attribute
368                 if (requestedShortLabel) {
369                     title = DDService.getAttributeShortLabel(businessObjectName, targetVal);
370                 }
371                 else {
372                     title = DDService.getAttributeLabel(businessObjectName, targetVal);
373                 }
374                 if (StringUtils.isNotBlank(title)) {
375                     return title;
376                 }
377             }
378             // try to get the label based on the business object and xpath ruledata section
379             targetVal = getRuleData(root);
380             if (LOG.isErrorEnabled()) {
381                 LOG.debug("Finding title in BO=" + businessObjectName + " ObjectName=" + targetVal);
382             }
383             if (StringUtils.isNotBlank(targetVal)) {
384                 title = DDService.getAttributeLabel(businessObjectName, targetVal);
385                 if (StringUtils.isNotBlank(title)) {
386                     return title;
387                 }
388             }
389             // If haven't found a label yet, its probably because there is no xpath. Use the name attribute to determine the BO
390             // attribute to use.
391             targetVal = root.getAttribute("name");
392             if (LOG.isErrorEnabled()) {
393                 LOG.debug("Finding title in BO=" + businessObjectName + " ObjectName=" + targetVal);
394             }
395             title = DDService.getAttributeLabel(businessObjectName, targetVal);
396 
397             if (StringUtils.isNotBlank(title)) {
398                 return title;
399             }
400         }
401         // return any potentially hard coded title info
402         else if ( (StringUtils.isNotBlank(businessObjectText)) && (StringUtils.isBlank(businessObjectName)) ) {
403         	return businessObjectText;
404         }
405         return notFound;
406 
407     }
408 
409     /**
410      * This method gets the contents of the ruledata function in the xpath statement in the XML
411      * 
412      * @param root
413      * @return
414      */
415     private String getRuleData(Element root) {
416         String xPathRuleTarget = getXPathText(root);
417 
418         // This pattern may need to change to get the last stanza of the xpath
419         if (StringUtils.isNotBlank(xPathRuleTarget)) {
420             Matcher ruleTarget = targetPattern.matcher(xPathRuleTarget);
421             if (ruleTarget.find()) {
422                 xPathRuleTarget = ruleTarget.group(1);
423             }
424         }
425         return xPathRuleTarget;
426     }
427 
428     private List<String> getXPathTerms(Element myTag) {
429 
430         Matcher xPathTarget;
431         String firstMatch;
432         List<String> xPathTerms = new ArrayList();
433         String allText = getXPathText(myTag);// grab the whole xpath expression
434         if (StringUtils.isNotBlank(allText)) {
435             xPathTarget = xPathPattern.matcher(allText);
436             Matcher termTarget;
437             Matcher cleanTarget;
438             int theEnd = 0;// Have to define this or the / gets used up with the match and every other term is returned.
439 
440             xPathTarget.find(theEnd);
441             theEnd = xPathTarget.end() - 1;
442             firstMatch = xPathTarget.group();
443 
444 
445             termTarget = termPattern.matcher(firstMatch);
446             int theEnd2 = 0;
447             while (termTarget.find(theEnd2)) { // get each term, clean them up, and add to the list.
448                 theEnd2 = termTarget.end() - 1;
449                 cleanTarget = cleanPattern.matcher(termTarget.group());
450                 cleanTarget.find();
451                 lastXPath = cleanTarget.group();
452                 xPathTerms.add(lastXPath);
453 
454             }
455         }
456         return xPathTerms;
457     }
458 
459     private String getLastXPath(Element root) {
460         List<String> tempList = getXPathTerms(root);
461         return tempList.get(tempList.size());
462     }
463 }