Skip to content

Commit

Permalink
Better error messages when someone tries to get an invalid @@... subv…
Browse files Browse the repository at this point in the history
…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
ddekany committed Jan 10, 2017
1 parent 28ac1ab commit 55b09e8
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 142 deletions.
3 changes: 2 additions & 1 deletion src/main/java/freemarker/core/BuiltInsForNodes.java
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/freemarker/ext/dom/AtAtKey.java
@@ -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;
}

}
2 changes: 1 addition & 1 deletion src/main/java/freemarker/ext/dom/DocumentModel.java
Expand Up @@ -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;
Expand Down
93 changes: 93 additions & 0 deletions src/main/java/freemarker/ext/dom/DomStringUtil.java
@@ -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);
}

}
120 changes: 60 additions & 60 deletions src/main/java/freemarker/ext/dom/ElementModel.java
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
48 changes: 30 additions & 18 deletions src/main/java/freemarker/ext/dom/NodeListModel.java
Expand Up @@ -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);
Expand All @@ -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 {
Expand Down

0 comments on commit 55b09e8

Please sign in to comment.