Permalink
Browse files

Better error messages when someone tries to get an invalid @@... subv…

…ariable of an XML DOM node (now it's not issued by the XPath implementation, which just sees it as a syntactical error). Some optimizations and cleanups regarding the matching of special keys (@@... and some more) in freemarker.ext.dom.
  • Loading branch information...
1 parent 28ac1ab commit 55b09e8dee070b490d38f850261f1accb278928a ddekany committed Jan 10, 2017
@@ -22,6 +22,7 @@
import java.util.List;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import freemarker.ext.dom._ExtDomApi;
import freemarker.template.*;
import freemarker.template.utility.StringUtil;
@@ -135,7 +136,7 @@ public Object exec(List names) throws TemplateModelException {
}
} else {
for (int j = 0; j < names.size(); j++) {
- if (StringUtil.matchesName((String) names.get(j), nodeName, nsURI, env)) {
+ if (_ExtDomApi.matchesName((String) names.get(j), nodeName, nsURI, env)) {
result.add(tnm);
break;
}
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.ext.dom;
+
+/**
+ * The special hash keys that start with "@@".
+ */
+enum AtAtKey {
+
+ MARKUP("@@markup"),
+ NESTED_MARKUP("@@nested_markup"),
+ ATTRIBUTES_MARKUP("@@attributes_markup"),
+ TEXT("@@text"),
+ START_TAG("@@start_tag"),
+ END_TAG("@@end_tag"),
+ QNAME("@@qname"),
+ NAMESPACE("@@namespace"),
+ LOCAL_NAME("@@local_name"),
+ ATTRIBUTES("@@"),
+ PREVIOUS_SIGNIFICANT("@@previous_significant"),
+ NEXT_SIGNIFICANT("@@next_significant");
+
+ private final String key;
+
+ public String getKey() {
+ return key;
+ }
+
+ private AtAtKey(String key) {
+ this.key = key;
+ }
+
+ public static boolean containsKey(String key) {
+ for (AtAtKey item : AtAtKey.values()) {
+ if (item.getKey().equals(key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
@@ -52,7 +52,7 @@ public TemplateModel get(String key) throws TemplateModelException {
} else if (key.equals("**")) {
NodeList nl = ((Document) node).getElementsByTagName("*");
return new NodeListModel(nl, this);
- } else if (StringUtil.isXMLID(key)) {
+ } else if (DomStringUtil.isXMLID(key)) {
ElementModel em = (ElementModel) NodeModel.wrap(((Document) node).getDocumentElement());
if (em.matchesName(key, Environment.getCurrentEnvironment())) {
return em;
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.dom;
+
+import freemarker.core.Environment;
+import freemarker.template.Template;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't.
+ */
+final class DomStringUtil {
+
+ private DomStringUtil() {
+ // Not meant to be instantiated
+ }
+
+ static boolean isXMLID(String name) {
+ return isXMLID(name, 0);
+ }
+
+ /**
+ * Check if the subvariable name is just an element name, or a more complex XPath expression.
+ *
+ * @param firstCharIdx The index of the character in the string parameter that we treat as the beginning of the
+ * string to check. This is to spare substringing that has become more expensive in Java 7.
+ *
+ * @return whether the name is a valid XML element name. (This routine might only be 99% accurate. REVISIT)
+ */
+ static boolean isXMLID(String name, int firstCharIdx) {
+ int ln = name.length();
+ for (int i = firstCharIdx; i < ln; i++) {
+ char c = name.charAt(i);
+ if (i == firstCharIdx && (c == '-' || c == '.' || Character.isDigit(c))) {
+ return false;
+ }
+ if (!Character.isLetterOrDigit(c) && c != '_' && c != '-' && c != '.') {
+ if (c == ':') {
+ if (i + 1 < ln && name.charAt(i + 1) == ':') {
+ // "::" is used in XPath
+ return false;
+ }
+ // We don't return here, as a lonely ":" is allowed.
+ } else {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return whether the qname matches the combination of nodeName, nsURI, and environment prefix settings.
+ */
+ static boolean matchesName(String qname, String nodeName, String nsURI, Environment env) {
+ String defaultNS = env.getDefaultNS();
+ if ((defaultNS != null) && defaultNS.equals(nsURI)) {
+ return qname.equals(nodeName)
+ || qname.equals(Template.DEFAULT_NAMESPACE_PREFIX + ":" + nodeName);
+ }
+ if ("".equals(nsURI)) {
+ if (defaultNS != null) {
+ return qname.equals(Template.NO_NS_PREFIX + ":" + nodeName);
+ } else {
+ return qname.equals(nodeName) || qname.equals(Template.NO_NS_PREFIX + ":" + nodeName);
+ }
+ }
+ String prefix = env.getPrefixForNamespace(nsURI);
+ if (prefix == null) {
+ return false; // Is this the right thing here???
+ }
+ return qname.equals(prefix + ":" + nodeName);
+ }
+
+}
@@ -19,6 +19,8 @@
package freemarker.ext.dom;
+import java.util.Collections;
+
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -33,9 +35,6 @@
import freemarker.template.TemplateSequenceModel;
import freemarker.template.utility.StringUtil;
-import java.util.ArrayList;
-import java.util.Collections;
-
class ElementModel extends NodeModel implements TemplateScalarModel {
public ElementModel(Element element) {
@@ -69,68 +68,69 @@ public TemplateModel get(String key) throws TemplateModelException {
}
}
return ns;
- }
- if (key.equals("**")) {
- Element elem = (Element) node;
- return new NodeListModel(elem.getElementsByTagName("*"), this);
- }
- if (key.startsWith("@")) {
- if (key.equals("@@") || key.equals("@*")) {
- return new NodeListModel(node.getAttributes(), this);
- }
- if (key.equals("@@start_tag")) {
- NodeOutputter nodeOutputter = new NodeOutputter(node);
- return new SimpleScalar(nodeOutputter.getOpeningTag((Element) node));
- }
- if (key.equals("@@end_tag")) {
- NodeOutputter nodeOutputter = new NodeOutputter(node);
- return new SimpleScalar(nodeOutputter.getClosingTag((Element) node));
- }
- if (key.equals("@@attributes_markup")) {
- StringBuilder buf = new StringBuilder();
- NodeOutputter nu = new NodeOutputter(node);
- nu.outputContent(node.getAttributes(), buf);
- return new SimpleScalar(buf.toString().trim());
- }
- if (key.equals("@@previous_significant")) {
- Node previousSibling = node.getPreviousSibling();
- while(previousSibling != null && !this.isSignificantNode(previousSibling)) {
- previousSibling = previousSibling.getPreviousSibling();
- }
- if(previousSibling == null) {
- return new NodeListModel(Collections.emptyList(), null);
+ } else if (key.equals("**")) {
+ return new NodeListModel(((Element) node).getElementsByTagName("*"), this);
+ } else if (key.startsWith("@")) {
+ if (key.startsWith("@@")) {
+ if (key.equals(AtAtKey.ATTRIBUTES.getKey())) {
+ return new NodeListModel(node.getAttributes(), this);
+ } else if (key.equals(AtAtKey.START_TAG.getKey())) {
+ NodeOutputter nodeOutputter = new NodeOutputter(node);
+ return new SimpleScalar(nodeOutputter.getOpeningTag((Element) node));
+ } else if (key.equals(AtAtKey.END_TAG.getKey())) {
+ NodeOutputter nodeOutputter = new NodeOutputter(node);
+ return new SimpleScalar(nodeOutputter.getClosingTag((Element) node));
+ } else if (key.equals(AtAtKey.ATTRIBUTES_MARKUP.getKey())) {
+ StringBuilder buf = new StringBuilder();
+ NodeOutputter nu = new NodeOutputter(node);
+ nu.outputContent(node.getAttributes(), buf);
+ return new SimpleScalar(buf.toString().trim());
+ } else if (key.equals(AtAtKey.PREVIOUS_SIGNIFICANT.getKey())) {
+ Node previousSibling = node.getPreviousSibling();
+ while(previousSibling != null && !this.isSignificantNode(previousSibling)) {
+ previousSibling = previousSibling.getPreviousSibling();
+ }
+ if(previousSibling == null) {
+ return new NodeListModel(Collections.emptyList(), null);
+ } else {
+ return wrap(previousSibling);
+ }
+ } else if (key.equals(AtAtKey.NEXT_SIGNIFICANT.getKey())) {
+ Node nextSibling = node.getNextSibling();
+ while(nextSibling != null && !this.isSignificantNode(nextSibling)) {
+ nextSibling = nextSibling.getNextSibling();
+ }
+ if(nextSibling == null) {
+ return new NodeListModel(Collections.emptyList(), null);
+ } else {
+ return wrap(nextSibling);
+ }
} else {
- return wrap(previousSibling);
+ // We don't anything like this that's element-specific; fall back
+ return super.get(key);
}
- }
- if (key.equals("@@next_significant")) {
- Node nextSibling = node.getNextSibling();
- while(nextSibling != null && !this.isSignificantNode(nextSibling)) {
- nextSibling = nextSibling.getNextSibling();
- }
- if(nextSibling == null) {
- return new NodeListModel(Collections.emptyList(), null);
- }
- else {
- return wrap(nextSibling);
- }
- }
- if (StringUtil.isXMLID(key.substring(1))) {
- Attr att = getAttribute(key.substring(1));
- if (att == null) {
- return new NodeListModel(this);
+ } else { // Starts with "@", but not with "@@"
+ if (DomStringUtil.isXMLID(key, 1)) {
+ Attr att = getAttribute(key.substring(1));
+ if (att == null) {
+ return new NodeListModel(this);
+ }
+ return wrap(att);
+ } else if (key.equals("@*")) {
+ return new NodeListModel(node.getAttributes(), this);
+ } else {
+ // We don't anything like this that's element-specific; fall back
+ return super.get(key);
}
- return wrap(att);
}
- }
- if (StringUtil.isXMLID(key)) {
+ } else if (DomStringUtil.isXMLID(key)) {
+ // We interpret key as an element name
NodeListModel result = ((NodeListModel) getChildNodes()).filterByName(key);
- if (result.size() == 1) {
- return result.get(0);
- }
- return result;
+ return result.size() != 1 ? result : result.get(0);
+ } else {
+ // We don't anything like this that's element-specific; fall back
+ return super.get(key);
}
- return super.get(key);
}
public boolean isSignificantNode(Node node) throws TemplateModelException {
@@ -219,6 +219,6 @@ private Attr getAttribute(String qname) {
}
boolean matchesName(String name, Environment env) {
- return StringUtil.matchesName(name, getNodeName(), getNodeNamespace(), env);
+ return DomStringUtil.matchesName(name, getNodeName(), getNodeNamespace(), env);
}
}
@@ -118,21 +118,34 @@ public TemplateModel get(String key) throws TemplateModelException {
NodeModel nm = (NodeModel) get(0);
return nm.get(key);
}
- if (key.startsWith("@@") &&
- (key.equals("@@markup")
- || key.equals("@@nested_markup")
- || key.equals("@@text"))) {
- StringBuilder result = new StringBuilder();
- for (int i = 0; i < size(); i++) {
- NodeModel nm = (NodeModel) get(i);
- TemplateScalarModel textModel = (TemplateScalarModel) nm.get(key);
- result.append(textModel.getAsString());
+ if (key.startsWith("@@")) {
+ if (key.equals(AtAtKey.MARKUP.getKey())
+ || key.equals(AtAtKey.NESTED_MARKUP.getKey())
+ || key.equals(AtAtKey.TEXT.getKey())) {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < size(); i++) {
+ NodeModel nm = (NodeModel) get(i);
+ TemplateScalarModel textModel = (TemplateScalarModel) nm.get(key);
+ result.append(textModel.getAsString());
+ }
+ return new SimpleScalar(result.toString());
+ } else if (key.length() != 2 /* to allow "@@" to fall through */) {
+ // As @@... would cause exception in the XPath engine, we throw a nicer exception now.
+ if (AtAtKey.containsKey(key)) {
+ throw new TemplateModelException(
+ "\"" + key + "\" is only applicable to a single XML node, but it was applied on "
+ + (size() != 0
+ ? size() + " XML nodes (multiple matches)."
+ : "an empty list of XML nodes (no matches)."));
+ } else {
+ throw new TemplateModelException("Unsupported @@ key: " + key);
+ }
}
- return new SimpleScalar(result.toString());
}
- if (StringUtil.isXMLID(key)
- || ((key.startsWith("@") && StringUtil.isXMLID(key.substring(1))))
- || key.equals("*") || key.equals("**") || key.equals("@@") || key.equals("@*")) {
+ if (DomStringUtil.isXMLID(key)
+ || ((key.startsWith("@")
+ && (DomStringUtil.isXMLID(key, 1) || key.equals("@@") || key.equals("@*"))))
+ || key.equals("*") || key.equals("**")) {
NodeListModel result = new NodeListModel(contextNode);
for (int i = 0; i < size(); i++) {
NodeModel nm = (NodeModel) get(i);
@@ -155,12 +168,11 @@ public TemplateModel get(String key) throws TemplateModelException {
if (xps != null) {
Object context = (size() == 0) ? null : rawNodeList();
return xps.executeQuery(context, key);
+ } else {
+ throw new TemplateModelException(
+ "Can't try to resolve the XML query key, because no XPath support is available. "
+ + "This is either malformed or an XPath expression: " + key);
}
- throw new TemplateModelException("Key: '" + key + "' is not legal for a node sequence ("
- + this.getClass().getName() + "). This node sequence contains " + size() + " node(s). "
- + "Some keys are valid only for node sequences of size 1. "
- + "If you use Xalan (instead of Jaxen), XPath expression keys work only with "
- + "node lists of size 1.");
}
private List rawNodeList() throws TemplateModelException {
Oops, something went wrong.

0 comments on commit 55b09e8

Please sign in to comment.