001/**
002 * Copyright 2005-2017 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.kew.role;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.exception.RiceRuntimeException;
020import org.kuali.rice.core.api.util.xml.XmlJotter;
021import org.kuali.rice.kew.api.KewApiConstants;
022import org.kuali.rice.kew.api.extension.ExtensionDefinition;
023import org.kuali.rice.kew.engine.RouteContext;
024import org.kuali.rice.kew.rule.XmlConfiguredAttribute;
025import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
026import org.w3c.dom.Document;
027import org.w3c.dom.Element;
028import org.w3c.dom.Node;
029import org.w3c.dom.NodeList;
030import org.xml.sax.InputSource;
031
032import javax.xml.xpath.XPath;
033import javax.xml.xpath.XPathConstants;
034import javax.xml.xpath.XPathExpressionException;
035import java.io.StringReader;
036import java.util.ArrayList;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040
041/**
042 * Resolves qualifiers based on XPath configuration in the resolver's attribute.
043 * 
044 * <p>An example of the xml processed by this attribute follows:
045 * 
046 * <p><pre>
047 * <resolverConfig>
048 *   <baseXPathExpression>/xmlData/chartOrg</baseXPathExpression>
049 *   <attributes name="chart">
050 *     <xPathExpression>./chart</xPathExpression>
051 *   </attributes>
052 *   <attributes name="org">
053 *     <xPathExpression>./org</xPathExpression>
054 *   </attributes>
055 * </resolverConfig>
056 * </pre>
057 * 
058 * <p>There are 2 different types of qualifier resolvers, those that resolve compound
059 * attribute sets and those that resolve simple attribute sets.  A simple attribute
060 * set is one which includes only a single "qualifier" specification.  The example above
061 * is compound because it includes both chart and org.
062 * 
063 * <p>When dealing with compound attribute sets, the baseXPathExpression is used to
064 * define grouping for these compound sets.  It is therefore required that inside each
065 * resulting element retrieved from the baseXPathExpression, there is only a single instance
066 * of each qualifier.  If this is not the case, an error will be thrown.  For the example
067 * above, the following XML would be evaluated successfully:
068 * 
069 * <p><pre>
070 * <xmlData>
071 *   <chartOrg>
072 *     <chart>BL</chart>
073 *     <org>BUS</org>
074 *   </chartOrg>
075 *   <chartOrg>
076 *     <chart>IN</chart>
077 *     <org>MED</org>
078 *   </chartOrg>
079 * </xmlData>
080 * </pre>
081 * 
082 * <p>This would return 2 attributes sets, each with a chart and org in it.  The following
083 * XML would cause the XPathQualifierResolver to throw an exception during processing.
084 * 
085 * <p><pre>
086 * <xmlData>
087 *   <chartOrg>
088 *     <chart>BL</chart>
089 *     <org>BUS</org>
090 *     <chart>IN</chart>
091 *     <org>MED</org>
092 *   </chartOrg>
093 * </xmlData>
094 * </pre>
095 * 
096 * <p>In this case the resolver has no knowledge of how to group chart and org together.
097 * What follows is an example of a resolver using a simple attribute set:
098 * 
099 * <p><pre>
100 * <resolverConfig>
101 *   <baseXPathExpression>/xmlData/accountNumbers</baseXPathExpression>
102 *   <attributes name="accountNumber">
103 *     <xPathExpression>./accountNumber</xPathExpression>
104 *   </attributes>
105 * </resolverConfig>
106 * </pre>
107 * 
108 * <p>In this example, the following XML would return a List containing an Map<String, String>
109 * for each account number when resolved.
110 * 
111 * <p><pre>
112 * <xmlData>
113 *   <accountNumbers>
114 *     <accountNumber>12345</accountNumber>
115 *     <accountNumber>54321</accountNumber>
116 *     <accountNumber>102030</accountNumber>
117 *     <accountNumber>302010</accountNumber>
118 *   </accountNumbers>
119 * </xmlData>
120 * 
121 * <p>The baseXPathExpression is optional and defaults to the root of the document if not specified.
122 * 
123 * @author Kuali Rice Team (rice.collab@kuali.org)
124 */
125public class XPathQualifierResolver implements QualifierResolver, XmlConfiguredAttribute {
126    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(XPathQualifierResolver.class);
127
128        private ExtensionDefinition extensionDefinition;
129
130        public List<Map<String, String>> resolve(RouteContext context) {
131                        ResolverConfig config = parseResolverConfig();
132                        Document xmlContent = context.getDocumentContent().getDocument();
133                        XPath xPath = XPathHelper.newXPath();
134                        boolean isCompoundMap = config.getExpressionMap().size() > 1;
135                        try {
136                                List<Map<String, String>> maps = new ArrayList<Map<String, String>>();
137                                NodeList baseElements = (NodeList)xPath.evaluate(config.getBaseXPathExpression(), xmlContent, XPathConstants.NODESET);
138                                if (LOG.isDebugEnabled()) {
139                                        LOG.debug("Found " + baseElements.getLength() + " baseElements to parse for Map<String, String>s using document XML:" + XmlJotter.jotDocument(xmlContent));
140                                }
141                                for (int index = 0; index < baseElements.getLength(); index++) {
142                                        Node baseNode = baseElements.item(index);
143                                        if (isCompoundMap) {
144                                                handleCompoundMap(baseNode, maps, config, xPath);
145                                        } else {
146                                                handleSimpleMap(baseNode, maps, config, xPath);
147                                        }
148                                }
149                                return maps;
150                        } catch (XPathExpressionException e) {
151                                throw new RiceRuntimeException("Encountered an issue executing XPath.", e);
152                        }
153        }
154        
155        protected void handleCompoundMap(Node baseNode, List<Map<String, String>> maps, ResolverConfig config, XPath xPath) throws XPathExpressionException {
156                Map<String, String> map = new HashMap<String, String>();
157                for (String attributeName : config.getExpressionMap().keySet()) {
158                        String xPathExpression = config.getExpressionMap().get(attributeName);
159                        NodeList attributes = (NodeList)xPath.evaluate(xPathExpression, baseNode, XPathConstants.NODESET);
160                        if (attributes.getLength() > 1) {
161                                throw new RiceRuntimeException("Found more than more XPath result for an attribute in a compound attribute set for attribute: " + attributeName + " with expression " + xPathExpression);
162                        } else if (attributes.getLength() != 0) {
163                                String attributeValue = ((Element)attributes.item(0)).getTextContent();
164                                if (LOG.isDebugEnabled()) {
165                                        LOG.debug("Adding values to compound Map<String, String>: " + attributeName + "::" + attributeValue);
166                                }
167                                map.put(attributeName, attributeValue);
168                        }
169                }
170                maps.add(map);
171        }
172        
173        protected void handleSimpleMap(Node baseNode, List<Map<String, String>> maps, ResolverConfig config, XPath xPath) throws XPathExpressionException {
174                String attributeName = config.getExpressionMap().keySet().iterator().next();
175                String xPathExpression = config.getExpressionMap().get(attributeName);
176                NodeList attributes = (NodeList)xPath.evaluate(xPathExpression, baseNode, XPathConstants.NODESET);
177                for (int index = 0; index < attributes.getLength(); index++) {
178                        Element attributeElement = (Element)attributes.item(index);
179                        Map<String, String> map = new HashMap<String, String>();
180                        String attributeValue = attributeElement.getTextContent();
181                        if (LOG.isDebugEnabled()) {
182                                LOG.debug("Adding values to simple Map<String, String>: " + attributeName + "::" + attributeValue);
183                        }
184                        map.put(attributeName, attributeValue);
185                        maps.add(map);
186                }
187        }
188        
189        protected ResolverConfig parseResolverConfig() {
190                if (extensionDefinition.getConfiguration() != null
191               && extensionDefinition.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA) == null) {
192                        throw new RiceRuntimeException("Failed to locate a RuleAttribute for the given XPathQualifierResolver");
193                }
194                try {
195                        ResolverConfig resolverConfig = new ResolverConfig();
196                        String xmlConfig = extensionDefinition.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA);
197                        XPath xPath = XPathHelper.newXPath();
198                        String baseExpression = xPath.evaluate("//resolverConfig/baseXPathExpression", new InputSource(new StringReader(xmlConfig)));
199                        if (!StringUtils.isEmpty(baseExpression)) {
200                                resolverConfig.setBaseXPathExpression(baseExpression);
201                        }
202            //We need to check for two possible xml configurations
203            //1 - 'attributes'
204            //2- 'qualifier' (legacy)
205                        NodeList qualifiers = (NodeList)xPath.evaluate("//resolverConfig/attributes", new InputSource(new StringReader(xmlConfig)), XPathConstants.NODESET);
206            NodeList qualifiersLegacy = (NodeList)xPath.evaluate("//resolverConfig/qualifier", new InputSource(new StringReader(xmlConfig)), XPathConstants.NODESET);
207
208            if ((qualifiers == null || qualifiers.getLength() == 0) && (qualifiersLegacy == null || qualifiersLegacy.getLength() == 0)) {
209                                throw new RiceRuntimeException("Invalid qualifier resolver configuration.  Must contain at least one qualifier!");
210                        }
211            //check for standard qualifiers (those using 'attributes' xml elements) and add if they exist
212                        for (int index = 0; index < qualifiers.getLength(); index++) {
213                                Element qualifierElement = (Element)qualifiers.item(index);
214                                String name = qualifierElement.getAttribute("name");
215                                NodeList expressions = qualifierElement.getElementsByTagName("xPathExpression");
216                                if (expressions.getLength() != 1) {
217                                        throw new RiceRuntimeException("There should only be a single xPathExpression per qualifier");
218                                }
219                                Element expressionElement = (Element)expressions.item(0);
220                                resolverConfig.getExpressionMap().put(name, expressionElement.getTextContent());
221                        }
222
223            //check for legacy qualifiers (those using 'qualifier' xml elements) and add if they exist
224                        for (int index = 0; index < qualifiersLegacy.getLength(); index++) {
225                                Element qualifierElement = (Element)qualifiersLegacy.item(index);
226                                String name = qualifierElement.getAttribute("name");
227                                NodeList expressions = qualifierElement.getElementsByTagName("xPathExpression");
228                                if (expressions.getLength() != 1) {
229                                        throw new RiceRuntimeException("There should only be a single xPathExpression per qualifier");
230                                }
231                                Element expressionElement = (Element)expressions.item(0);
232                                resolverConfig.getExpressionMap().put(name, expressionElement.getTextContent());
233                        }
234                        if (LOG.isDebugEnabled()) {
235                                LOG.debug("Using Resolver Config Settings: " + resolverConfig.toString());
236                        }
237                        return resolverConfig;
238                } catch (XPathExpressionException e) {
239                        throw new RiceRuntimeException("Encountered an error parsing resolver config.", e);
240                }
241        }
242
243    @Override
244    public void setExtensionDefinition(ExtensionDefinition ruleAttribute) {
245        extensionDefinition = ruleAttribute;
246    }
247
248    class ResolverConfig {
249                private String baseXPathExpression = "/";
250                private Map<String, String> expressionMap = new HashMap<String, String>();
251                public String getBaseXPathExpression() {
252                        return this.baseXPathExpression;
253                }
254                public void setBaseXPathExpression(String baseXPathExpression) {
255                        this.baseXPathExpression = baseXPathExpression;
256                }
257                public Map<String, String> getExpressionMap() {
258                        return this.expressionMap;
259                }
260                public void setExpressionMap(Map<String, String> expressionMap) {
261                        this.expressionMap = expressionMap;
262                }
263                @Override
264                public String toString() {
265                        StringBuffer sb = new StringBuffer();
266                        sb.append(  '\n' );
267                        sb.append("ResolverConfig Parameters\n");
268                        sb.append( "      baseXPathExpression: " + baseXPathExpression + "\n" );
269                        sb.append( "      expressionMap:\n" );
270                        for (Map.Entry<String, String> entry : expressionMap.entrySet()) {
271                                sb.append( "            " + entry.getKey() + ": " + entry.getValue() + "\n" );
272                        }
273                        return sb.toString();
274                }
275        }
276
277}