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    }