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 }