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