View Javadoc

1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.docsearch.xml;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.core.api.config.ConfigurationException;
21  import org.kuali.rice.core.api.impex.xml.XmlConstants;
22  import org.kuali.rice.core.api.util.ConcreteKeyValue;
23  import org.kuali.rice.core.api.util.KeyValue;
24  import org.kuali.rice.core.api.util.xml.XmlHelper;
25  import org.kuali.rice.core.api.util.xml.XmlJotter;
26  import org.kuali.rice.kew.api.KewApiConstants;
27  import org.kuali.rice.kew.api.extension.ExtensionDefinition;
28  import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
29  import org.kuali.rice.kew.util.Utilities;
30  import org.kuali.rice.kim.api.group.Group;
31  import org.kuali.rice.kim.api.group.GroupService;
32  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
33  import org.kuali.rice.krad.UserSession;
34  import org.kuali.rice.krad.util.GlobalVariables;
35  import org.w3c.dom.Attr;
36  import org.w3c.dom.Element;
37  import org.w3c.dom.NamedNodeMap;
38  import org.w3c.dom.Node;
39  import org.w3c.dom.NodeList;
40  import org.xml.sax.InputSource;
41  
42  import javax.xml.parsers.DocumentBuilderFactory;
43  import javax.xml.parsers.ParserConfigurationException;
44  import javax.xml.transform.TransformerException;
45  import javax.xml.xpath.XPath;
46  import javax.xml.xpath.XPathConstants;
47  import javax.xml.xpath.XPathExpressionException;
48  import java.io.BufferedReader;
49  import java.io.StringReader;
50  import java.util.ArrayList;
51  import java.util.Collection;
52  import java.util.Collections;
53  import java.util.HashMap;
54  import java.util.LinkedHashMap;
55  import java.util.List;
56  import java.util.Map;
57  
58  /**
59   * Immutable object that encapsulates the XML searchable attribute content
60   */
61  class XMLSearchableAttributeContent {
62      private static final Logger LOG = Logger.getLogger(XMLSearchableAttributeContent.class);
63  
64      private ExtensionDefinition def;
65      private Element attributeConfig;
66      private Node searchingConfig;
67      private String searchContent;
68      private Map<String, FieldDef> fieldDefs;
69  
70      XMLSearchableAttributeContent(ExtensionDefinition ed) {
71          this.def = ed;
72      }
73  
74      XMLSearchableAttributeContent(String configXML) throws TransformerException {
75          this.attributeConfig = XmlHelper.readXml(configXML).getDocumentElement();
76      }
77  
78      XMLSearchableAttributeContent(Element configXML) {
79          if (configXML == null) {
80              throw new IllegalArgumentException("Configuration element must not be nil");
81          }
82          this.attributeConfig = configXML;
83      }
84  
85      Node getSearchingConfig() throws XPathExpressionException, ParserConfigurationException {
86          if (searchingConfig == null) {
87              XPath xpath = XPathHelper.newXPath();
88              // technically this should probably only be "searchingConfig", and not search the whole tree
89              String searchingConfigExpr = "//searchingConfig";
90              searchingConfig = (Node) xpath.evaluate(searchingConfigExpr, getAttributeConfig(), XPathConstants.NODE);
91          }
92          return searchingConfig;
93      }
94  
95      String getSearchContent() throws XPathExpressionException, ParserConfigurationException {
96          if (searchContent == null) {
97              Node cfg = getSearchingConfig();
98              XPath xpath = XPathHelper.newXPath();
99              Node n = (Node) xpath.evaluate("xmlSearchContent", cfg, XPathConstants.NODE);
100             if (n != null) {
101                 StringBuilder sb = new StringBuilder();
102                 NodeList list = n.getChildNodes();
103                 for (int i = 0; i < list.getLength(); i++) {
104                     sb.append(XmlJotter.jotNode(list.item(i)));
105                 }
106                 this.searchContent = sb.toString();
107             }
108         }
109         return searchContent;
110     }
111 
112     String generateSearchContent(Map<String, String> properties) throws XPathExpressionException, ParserConfigurationException {
113         if (properties == null) {
114             properties = new HashMap<String, String>();
115         }
116         // implementation quirk: if no fields were present, empty search content was returned
117         List<FieldDef> fields = getFieldDefList();
118         if (fields.size() == 0) {
119             return "";
120         }
121 
122         String searchContent = getSearchContent();
123 
124         // custom search content template is provided, evaluate it
125         if (searchContent != null) {
126             String generatedContent = searchContent;
127             // if properties have been passed in, perform string replacement
128             // NOTE: should default field <value>s also be used for substitution in addition to given properties?
129             // Implementation note: if we want to be 100% backwards compatible we can't simply use a global StrSubstitutor
130             // to replace all variables.  The implementation actually only replaces variables that are names of
131             // *defined fields*; that means properties for fields which are not present on the attribute are NOT replaced.
132             for (FieldDef field: fields) {
133                 if (StringUtils.isNotBlank(field.name)) {
134                     String propValue = properties.get(field.name);
135                     if (StringUtils.isNotBlank(propValue)) {
136                         generatedContent = generatedContent.replaceAll("%" + field.name + "%", propValue);
137                     }
138                 }
139             }
140             return generatedContent;
141         } else { // use a default format
142             // Standard doc content if no doc content is found in the searchingConfig xml.
143             StringBuilder buf = new StringBuilder("<xmlRouting>");
144             for (FieldDef field: fields) {
145                 if (StringUtils.isNotBlank(field.name)) {
146                     String propValue = properties.get(field.name);
147                     if (StringUtils.isNotBlank(propValue)) {
148                         buf.append("<field name=\"");
149                         buf.append(field.name);
150                         buf.append("\"><value>");
151                         buf.append(propValue);
152                         buf.append("</value></field>");
153                     }
154                 }
155             }
156             buf.append("</xmlRouting>");
157             return buf.toString();
158         }
159     }
160 
161     /**
162      * Returns a non-null but possibly empty list of FieldDefs
163      * @return
164      * @throws XPathExpressionException
165      * @throws ParserConfigurationException
166      */
167     List<FieldDef> getFieldDefList() throws XPathExpressionException, ParserConfigurationException {
168         return Collections.unmodifiableList(new ArrayList<FieldDef>(getFieldDefs().values()));
169     }
170 
171     Map<String, FieldDef> getFieldDefs() throws XPathExpressionException, ParserConfigurationException {
172         if (fieldDefs == null) {
173             fieldDefs = new LinkedHashMap<String, FieldDef>();
174             XPath xpath = XPathHelper.newXPath();
175             Node searchingConfig = getSearchingConfig();
176             if (searchingConfig != null) {
177                 NodeList list = (NodeList) xpath.evaluate("fieldDef", searchingConfig, XPathConstants.NODESET);
178                 for (int i = 0; i < list.getLength(); i++) {
179                     FieldDef def = new FieldDef(list.item(i));
180                     fieldDefs.put(def.name, def);
181                 }
182             }
183         }
184         return fieldDefs;
185     }
186 
187     protected Element getAttributeConfig() {
188         if (attributeConfig == null) {
189             try {
190                 String xmlConfigData = def.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA);
191                 this.attributeConfig = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(xmlConfigData)))).getDocumentElement();
192             } catch (Exception e) {
193                 String ruleAttrStr = (def == null ? null : def.getName());
194                 LOG.error("error parsing xml data from search attribute: " + ruleAttrStr, e);
195                 throw new RuntimeException("error parsing xml data from searchable attribute: " + ruleAttrStr, e);
196             }
197         }
198         return attributeConfig;
199     }
200 
201     /**
202      * Encapsulates a field definition
203      */
204     static class FieldDef {
205         final String name;
206         final String title;
207         final String defaultValue;
208         final Display display;
209         final Validation validation;
210         final Visibility visibility;
211         final SearchDefinition searchDefinition;
212         final String fieldEvaluationExpr;
213         final Boolean showResultColumn;
214         final Lookup lookup;
215 
216         FieldDef(Node n) throws XPathExpressionException {
217             XPath xpath = XPathHelper.newXPath();
218             this.name = getStringAttr(n, "name");
219             this.title= getStringAttr(n, "title");
220             this.defaultValue = getNodeText(xpath, n, "value");
221             this.fieldEvaluationExpr = getNodeText(xpath, n, "fieldEvaluation/xpathexpression");
222             this.showResultColumn = getBoolean(xpath, n, "resultColumn/@show");
223             // TODO: it might be better to invert responsibility here
224             // so we can assign null values for missing entries (at least those we don't expect defaults for)
225             this.display = new Display(xpath, n);
226             this.validation = new Validation(xpath, n);
227             this.visibility = new Visibility(xpath, n);
228             this.searchDefinition = new SearchDefinition(xpath, n);
229             this.lookup = new Lookup(xpath, n, name);
230         }
231 
232         /**
233          * Returns whether this field should be displayed in search results.  If 'resultColumn/@show' is explicitly defined
234          * this value will be used, otherwise it will defer to the field criteria visibility, defaulting to 'true' if unset
235          * @return
236          */
237         boolean isDisplayedInSearchResults() {
238             return showResultColumn != null ? showResultColumn : (visibility.visible != null ? visibility.visible : true);
239         }
240 
241         /**
242          * Encapsulates display definition
243          */
244         static class Display {
245             final String type;
246             final String meta;
247             final String formatter;
248             final Collection<KeyValue> options;
249             final Collection<String> selectedOptions;
250 
251             Display(XPath xpath, Node n) throws XPathExpressionException {
252                 type = getNodeText(xpath, n, "display/type");
253                 meta = getNodeText(xpath, n, "display/meta");
254                 formatter = getNodeText(xpath, n, "display/formatter");
255                 Collection<KeyValue> options = new ArrayList<KeyValue>();
256                 Collection<String> selectedOptions = new ArrayList<String>();
257                 
258                 NodeList nodes = (NodeList) xpath.evaluate("display[1]/values", n, XPathConstants.NODESET);
259                 for (int i = 0; i < nodes.getLength(); i++) {
260                     Node node = nodes.item(i);
261                     boolean selected = getBooleanAttr(node, "selected", false);
262                     String title = getStringAttr(node, "title");
263                     // TODO: test this - intent is that value without text content results in blank entry?
264                     // this is to allow an empty drop down choice and can probably implemented in a better way
265                     String value = node.getTextContent();
266                     if (value == null) {
267                         value = "";
268                     }
269                     options.add(new ConcreteKeyValue(value, title));
270                     if (selected) {
271                         selectedOptions.add(node.getTextContent());
272                     }
273                 }
274 
275                 this.options = Collections.unmodifiableCollection(options);
276                 this.selectedOptions = Collections.unmodifiableCollection(selectedOptions);
277             }
278         }
279 
280         /**
281          * Encapsulates validation definition
282          */
283         static class Validation {
284             final boolean required;
285             final String regex;
286             final String message;
287 
288             Validation(XPath xpath, Node n) throws XPathExpressionException {
289                 required = Boolean.parseBoolean(getNodeText(xpath, n, "validation/@required"));
290                 regex = getNodeText(xpath, n, "validation/regex");
291                 message = getNodeText(xpath, n, "validation/message");
292             }
293         }
294 
295         /**
296          * Encapsulates visibility definition
297          */
298         static class Visibility {
299             final Boolean visible;
300             final String type;
301             final String groupName;
302             final String groupNamespace;
303 
304             Visibility(XPath xpath, Node n) throws XPathExpressionException {
305                 Boolean visible = null;
306                 String type = null;
307                 String groupName = null;
308                 String groupNamespace = null;
309                 Node node = (Node) xpath.evaluate("(visibility/field | visibility/column | visibility/fieldAndColumn)", n, XPathConstants.NODE); // NODE - just use first one
310                 if (node != null && node instanceof Element) {
311                     Element visibilityEl = (Element) node;
312                     type = visibilityEl.getNodeName();
313                     Attr attr = visibilityEl.getAttributeNode("visible");
314                     if (attr != null) {
315                         visible = Boolean.valueOf(attr.getValue());
316                     }
317                     Node groupMember = (Node) xpath.evaluate("(" + XmlConstants.IS_MEMBER_OF_GROUP + "|" + XmlConstants.IS_MEMBER_OF_WORKGROUP + ")", visibilityEl, XPathConstants.NODE);
318                     if (groupMember != null && groupMember instanceof Element) {
319                         Element groupMemberEl = (Element) groupMember;
320                         boolean group_def_found = false;
321                         if (XmlConstants.IS_MEMBER_OF_GROUP.equals(groupMember.getNodeName())) {
322                             group_def_found = true;
323                             groupName = Utilities.substituteConfigParameters(groupMember.getTextContent().trim());
324                             groupNamespace = Utilities.substituteConfigParameters(groupMemberEl.getAttribute(XmlConstants.NAMESPACE)).trim();
325                         } else if (XmlConstants.IS_MEMBER_OF_WORKGROUP.equals(groupMember.getNodeName())) {
326                             group_def_found = true;
327                             LOG.warn("Rule Attribute XML is using deprecated element '" + XmlConstants.IS_MEMBER_OF_WORKGROUP +
328                                      "', please use '" + XmlConstants.IS_MEMBER_OF_GROUP + "' instead.");
329                             String workgroupName = Utilities.substituteConfigParameters(groupMember.getTextContent());
330                             groupNamespace = Utilities.parseGroupNamespaceCode(workgroupName);
331                             groupName = Utilities.parseGroupName(workgroupName);
332                         }
333                         if (group_def_found) {
334                             if (StringUtils.isEmpty(groupName) || StringUtils.isEmpty(groupNamespace)) {
335                                 throw new RuntimeException("Both group name and group namespace must be present for group-based visibility.");
336                             }
337 
338                             GroupService groupService = KimApiServiceLocator.getGroupService();
339                             Group group = groupService.getGroupByNamespaceCodeAndName(groupNamespace, groupName);
340                             UserSession session = GlobalVariables.getUserSession();
341                             if (session != null) {
342                              visible =  group == null ? false : groupService.isMemberOfGroup(session.getPerson().getPrincipalId(), group.getId());
343                             }
344                         }
345                     }
346                 }
347                 this.visible = visible;
348                 this.type = type;
349                 this.groupName = groupName;
350                 this.groupNamespace = groupNamespace;
351             }
352         }
353 
354         /**
355          * Encapsulates a SearchDefinition
356          */
357         static class SearchDefinition {
358             final RangeOptions DEFAULTS = new RangeOptions(null, false, false);
359             /**
360              * The field search data type.  Guaranteed to be defined (defaulted if missing).
361              */
362             final String dataType;
363             final boolean rangeSearch;
364             final RangeOptions searchDef;
365             final RangeOptions rangeDef;
366             final RangeBound lowerBound;
367             final RangeBound upperBound;
368 
369             SearchDefinition(XPath xpath, Node n) throws XPathExpressionException {
370                 String dataType = KewApiConstants.SearchableAttributeConstants.DEFAULT_SEARCHABLE_ATTRIBUTE_TYPE_NAME;
371                 // if element is missing outright, omit the defaults as well as it cannot be a ranged search
372                 // caller should check whether this is a ranged search
373                 RangeOptions searchDefDefaults = new RangeOptions();
374                 RangeOptions rangeDef = null;
375                 RangeBound lowerBound = null;
376                 RangeBound upperBound = null;
377                 boolean rangeSearch = false;
378                 Node searchDefNode = (Node) xpath.evaluate("searchDefinition", n, XPathConstants.NODE);
379                 if (searchDefNode != null) {
380                     String s = getStringAttr(searchDefNode, "dataType");
381                     // TODO: empty data type should really be invalid or default to something (String?)
382                     if (StringUtils.isNotEmpty(s)) {
383                         dataType = s;
384                     }
385                     // clearly there is a conflict if rangeSearch is false while range bounds are defined!
386                     // this is not currently enforced
387                     rangeSearch = getBooleanAttr(searchDefNode, "rangeSearch", false);
388 
389                     searchDefDefaults = new RangeOptions(xpath, searchDefNode, DEFAULTS);
390                     Node rangeDefinition = (Node) xpath.evaluate("rangeDefinition", searchDefNode, XPathConstants.NODE);
391                     // if range definition element is present, bounds derive settings from range definition
392                     if (rangeDefinition != null) {
393                         rangeDef = new RangeOptions(xpath, rangeDefinition, searchDefDefaults);
394                         Node lower = (Node) xpath.evaluate("lower", rangeDefinition, XPathConstants.NODE);
395                         lowerBound = lower == null ? new RangeBound(defaultInclusive(rangeDef, true)) : new RangeBound(xpath, lower, defaultInclusive(rangeDef, true));
396                         Node upper = (Node) xpath.evaluate("upper", rangeDefinition, XPathConstants.NODE);
397                         upperBound = upper == null ? new RangeBound(defaultInclusive(rangeDef, false)) : new RangeBound(xpath, upper, defaultInclusive(rangeDef, false));
398                     } else if (rangeSearch) {
399                         // otherwise if range search is specified but no rangedefinition element is present,
400                         // bounds use options from search definition element
401                         lowerBound = new RangeBound(defaultInclusive(searchDefDefaults, true));
402                         upperBound = new RangeBound(defaultInclusive(searchDefDefaults, false));
403                     }
404                 }
405                 this.dataType = dataType;
406                 this.rangeSearch = rangeSearch;
407                 this.searchDef = searchDefDefaults;
408                 this.rangeDef = rangeDef;
409                 this.lowerBound = lowerBound;
410                 this.upperBound = upperBound;
411             }
412 
413             private static BaseRangeOptions defaultInclusive(BaseRangeOptions opts, boolean inclusive) {
414                 boolean inc = opts.inclusive == null ? inclusive : opts.inclusive;
415                 return new BaseRangeOptions(inc, opts.datePicker);
416             }
417 
418             /**
419              * Returns the most specific global/non-bounds options
420              */
421             public RangeOptions getRangeBoundOptions() {
422                 return rangeDef == null ? searchDef : rangeDef;
423             }
424 
425             /**
426              * Whether this appears to be a ranged search
427              */
428             public boolean isRangedSearch() {
429                 // this is a ranged search if
430                 // 1) searchDefinition declares this is a rangeSearch
431                 // OR
432                 // 2) rangeDefinition/bounds are present in searchDefinition
433                 return this.rangeSearch || (rangeDef != null);
434             }
435 
436             /**
437              * Base range options class used by search/range definition and bounds elements
438              */
439             static class BaseRangeOptions {
440                 protected final Boolean inclusive;
441                 protected final Boolean datePicker;
442 
443                 BaseRangeOptions() {
444                     this.inclusive = this.datePicker = null;
445                 }
446                 BaseRangeOptions(Boolean inclusive, Boolean datePicker) {
447                     this.inclusive = inclusive;
448                     this.datePicker = datePicker;
449                 }
450                 BaseRangeOptions(BaseRangeOptions defaults) {
451                     this.inclusive = defaults.inclusive;
452                     this.datePicker = defaults.datePicker;
453                 }
454                 BaseRangeOptions(XPath xpath, Node n, BaseRangeOptions defaults) {
455                     this.inclusive = getBooleanAttr(n, "inclusive", defaults.inclusive);
456                     this.datePicker = getBooleanAttr(n, "datePicker", defaults.datePicker);
457                 }
458             }
459 
460             /**
461              * Reads inclusive, caseSensitive, and datePicker options from attributes of
462              * search definition and range definition elements.
463              */
464             static class RangeOptions extends BaseRangeOptions {
465                 protected final Boolean caseSensitive;
466                 RangeOptions() {
467                     super();
468                     this.caseSensitive = null;
469                 }
470                 RangeOptions(Boolean inclusive, Boolean caseSensitive, Boolean datePicker) {
471                     super(inclusive, datePicker);
472                     this.caseSensitive = caseSensitive;
473                 }
474                 RangeOptions(RangeOptions defaults) {
475                     super(defaults);
476                     this.caseSensitive = defaults.caseSensitive;
477                 }
478                 RangeOptions(XPath xpath, Node n, RangeOptions defaults) {
479                     super(xpath, n, defaults);
480                     this.caseSensitive = getBooleanAttr(n, "caseSensitive", defaults.caseSensitive);
481                 }
482             }
483 
484             /**
485              * Adds label to BaseRangeOptions
486              */
487             static class RangeBound extends BaseRangeOptions {
488                 final String label;
489                 RangeBound(BaseRangeOptions defaults) {
490                     super(defaults);
491                     this.label = null;
492                 }
493                 RangeBound(XPath xpath, Node n, BaseRangeOptions defaults) {
494                     super(xpath, n, defaults);
495                     this.label = getStringAttr(n, "label");
496                 }
497             }
498         }
499 
500         /**
501          * Encapsulates a lookup definition
502          * <lookup businessObjectClass="org.kuali.rice.kew.docsearch.xml.MyLookupable">
503          *   <fieldConversions>
504          *     <fieldConversion lookupFieldName="chart" localFieldName="MyBean.data"/>
505          *   </fieldConversions>
506          * </lookup>
507          */
508         static class Lookup {
509             final String dataObjectClass;
510             final Map<String, String> fieldConversions;
511 
512             Lookup(XPath xpath, Node n, String fieldName) throws XPathExpressionException {
513                 String dataObjectClass = null;
514                 Map<String, String> fieldConversions = new HashMap<String, String>();
515 
516                 Node lookupNode = (Node) xpath.evaluate("lookup", n, XPathConstants.NODE);
517                 if (lookupNode != null) {
518                     NamedNodeMap quickfinderAttributes = lookupNode.getAttributes();
519                     Node dataObjectNode = quickfinderAttributes.getNamedItem("dataObjectClass");
520                     if (dataObjectNode == null) {
521                         // for legacy compatibility, though businessObjectClass is deprecated
522                         dataObjectNode = quickfinderAttributes.getNamedItem("businessObjectClass");
523                         if (dataObjectNode != null) {
524                             LOG.warn("Field is using deprecated 'businessObjectClass' instead of 'dataObjectClass' for lookup definition, field name is: " + fieldName);
525                         } else {
526                             throw new ConfigurationException("Failed to locate 'dataObjectClass' for lookup definition.");
527                         }
528                     }
529                     dataObjectClass = dataObjectNode.getNodeValue();
530                     NodeList list = (NodeList) xpath.evaluate("fieldConversions/fieldConversion", lookupNode, XPathConstants.NODESET);
531                     for (int i = 0; i < list.getLength(); i++) {
532                         Node fieldConversionChildNode = list.item(i);
533                         NamedNodeMap fieldConversionAttributes = fieldConversionChildNode.getAttributes();
534                         // TODO: no validation on these attrs, could throw NPE
535                         String lookupFieldName = fieldConversionAttributes.getNamedItem("lookupFieldName").getNodeValue();
536                         String localFieldName = fieldConversionAttributes.getNamedItem("localFieldName").getNodeValue();
537                         fieldConversions.put(lookupFieldName, localFieldName);
538                     }
539                 }
540                 
541                 this.dataObjectClass = dataObjectClass;
542                 this.fieldConversions = Collections.unmodifiableMap(fieldConversions);
543             }
544         }
545     }
546 
547     private static Boolean getBooleanAttr(Node n, String attributeName, Boolean dflt) {
548         String nodeValue = getStringAttr(n, attributeName);
549         return nodeValue == null ? dflt : Boolean.valueOf(nodeValue);
550     }
551 
552     private static String getStringAttr(Node n, String attributeName) {
553         Node attr = n.getAttributes().getNamedItem(attributeName);
554         return attr == null ? null : attr.getNodeValue();
555     }
556 
557     private static String getNodeText(XPath xpath, Node n, String expression) throws XPathExpressionException {
558         Node node = (Node) xpath.evaluate(expression, n, XPathConstants.NODE);
559         if (node == null) return null;
560         return node.getTextContent();
561     }
562 
563     private static Boolean getBoolean(XPath xpath, Node n, String expression) throws XPathExpressionException {
564         String val = getNodeText(xpath, n, expression);
565         return val == null ? null : Boolean.valueOf(val);
566     }
567 }