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