/*
* Copyright (c) 2002-2012 Alibaba Group Holding Limited.
* All rights reserved.
*
* Licensed 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 com.alibaba.citrus.service.velocity.support;
import static com.alibaba.citrus.util.ArrayUtil.*;
import static com.alibaba.citrus.util.Assert.*;
import static com.alibaba.citrus.util.BasicConstant.*;
import static com.alibaba.citrus.util.CollectionUtil.*;
import static com.alibaba.citrus.util.StringUtil.*;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.alibaba.citrus.service.configuration.ProductionModeAware;
import com.alibaba.citrus.service.velocity.FastCloneable;
import com.alibaba.citrus.service.velocity.VelocityConfiguration;
import com.alibaba.citrus.service.velocity.VelocityPlugin;
import com.alibaba.citrus.util.StringEscapeUtil;
import com.alibaba.citrus.util.ToStringBuilder;
import com.alibaba.citrus.util.ToStringBuilder.MapBuilder;
import org.apache.velocity.app.event.ReferenceInsertionEventHandler;
import org.apache.velocity.context.Context;
import org.apache.velocity.context.InternalContextAdapter;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.TemplateInitException;
import org.apache.velocity.runtime.Renderable;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.directive.Directive;
import org.apache.velocity.runtime.parser.node.Node;
import org.apache.velocity.util.ContextAware;
import org.apache.velocity.util.introspection.Info;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
public class EscapeSupport implements VelocityPlugin, ReferenceInsertionEventHandler, ContextAware, FastCloneable,
ProductionModeAware {
private final static Logger log = LoggerFactory.getLogger(EscapeSupport.class);
private final static String ESCAPE_TYPE_KEY = "_ESCAPE_SUPPORT_TYPE_";
private ResourceLoader loader;
private EscapeType defaultEscape;
private EscapeRule[] escapeRules;
private boolean cacheReferences;
private Map<String, EscapeType> referenceCache = createConcurrentHashMap();
private transient Context context;
public Object createCopy() {
EscapeSupport copy = new EscapeSupport();
copy.loader = loader;
copy.defaultEscape = defaultEscape;
copy.escapeRules = escapeRules;
copy.cacheReferences = cacheReferences;
copy.referenceCache = referenceCache;
return copy;
}
public void setProductionMode(boolean productionMode) {
this.cacheReferences = productionMode;
}
public void setDefaultEscape(String defaultEscapeType) {
this.defaultEscape = EscapeType.getEscapeType(defaultEscapeType);
}
public void setEscapeRules(EscapeRule[] escapeRules) {
this.escapeRules = escapeRules;
}
public void init(VelocityConfiguration configuration) throws Exception {
this.loader = configuration.getResourceLoader();
configuration.getProperties().addProperty("userdirective", Escape.class.getName());
configuration.getProperties().addProperty("userdirective", Noescape.class.getName());
}
public Resource[] getMacros() throws IOException {
String resourceName = "classpath:" + getClass().getPackage().getName().replace('.', '/') + "/escape_macros.vm";
Resource resource = assertNotNull(loader, "loader").getResource(resourceName);
return new Resource[] { resource };
}
public void setContext(Context context) {
this.context = context;
}
public Object referenceInsert(String reference, Object value) {
assertNotNull(context, "context");
if (value == null) {
return null;
}
// 如果当前引用是在#set($v = "$ref")中的,则不进行escape,推迟到最终输出时再escape。
// 同时避免对#define block进行escape。
if (InterpolationUtil.isInInterpolation(context) || value instanceof Renderable) {
return value;
}
EscapeType escapeType = getEscapeType(reference);
if (escapeType == null) {
return value;
}
return escapeType.escape(value);
}
private EscapeType getEscapeType(String reference) {
// 1. 假如明确指定了#escape或#noescape,则使用之。
EscapeType escapeType = (EscapeType) context.get(ESCAPE_TYPE_KEY);
if (escapeType != null) {
log.debug("{} specified for reference {}", escapeType, reference);
return escapeType;
}
// 2. 假如未明确指定,则查找规则
// 3. 假如没有规则,或规则未匹配,则使用默认值
if (cacheReferences) {
escapeType = referenceCache.get(reference);
}
if (escapeType == null) {
escapeType = findEscapeType(reference);
if (cacheReferences) {
referenceCache.put(reference, escapeType);
}
}
return escapeType;
}
private EscapeType findEscapeType(String reference) {
EscapeType escapeType = null;
String normalizedRef = normalizeReference(reference);
if (!isEmptyArray(escapeRules)) {
for (EscapeRule rule : escapeRules) {
Pattern matchedPattern = rule.matches(normalizedRef);
if (matchedPattern != null) {
escapeType = rule.getEscapeType();
if (log.isDebugEnabled()) {
log.debug("{} matched {} for reference {}", new Object[] { escapeType, matchedPattern,
reference });
}
break;
}
}
}
if (escapeType == null) {
escapeType = defaultEscape;
log.debug("{} used by default for reference {}", escapeType, reference);
}
return escapeType;
}
private static boolean renderWithEscape(EscapeType escapeType, InternalContextAdapter context, Writer writer,
Node node) throws IOException, ResourceNotFoundException,
ParseErrorException, MethodInvocationException {
EscapeType savedEscapeType = setEscapeType(escapeType, context);
try {
return node.render(context, writer);
} finally {
setEscapeType(savedEscapeType, context);
}
}
public static EscapeType setEscapeType(EscapeType escapeType, InternalContextAdapter context) {
if (escapeType == null) {
return (EscapeType) context.remove(ESCAPE_TYPE_KEY);
} else {
return (EscapeType) context.localPut(ESCAPE_TYPE_KEY, escapeType);
}
}
private static final Pattern referencePattern = Pattern
.compile("\\s*\\$\\s*\\!?\\s*(\\{\\s*(.*?)\\s*\\}|(.*?))\\s*");
private static String normalizeReference(String reference) {
if (reference == null) {
return EMPTY_STRING;
}
Matcher matcher = referencePattern.matcher(reference);
if (matcher.matches()) {
String form1 = matcher.group(2);
String form2 = matcher.group(3);
if (form1 == null) {
return form2;
} else {
return form1;
}
} else {
return reference;
}
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static enum EscapeType {
NO_ESCAPE("noescape") {
@Override
public Object escape(Object value) {
return value;
}
@Override
protected String escape(String strValue) {
unreachableCode();
return strValue;
}
@Override
public String toString() {
return "#noescape()";
}
},
JAVA("java") {
@Override
protected String escape(String strValue) {
return StringEscapeUtil.escapeJava(strValue);
}
},
JAVA_SCRIPT("javascript") {
@Override
protected String escape(String strValue) {
return StringEscapeUtil.escapeJavaScript(strValue);
}
},
HTML("html") {
@Override
protected String escape(String strValue) {
return StringEscapeUtil.escapeHtml(strValue);
}
},
XML("xml") {
@Override
protected String escape(String strValue) {
return StringEscapeUtil.escapeXml(strValue);
}
},
URL("url") {
@Override
protected String escape(String strValue) {
return StringEscapeUtil.escapeURL(strValue);
}
},
SQL("sql") {
@Override
protected String escape(String strValue) {
return StringEscapeUtil.escapeSql(strValue);
}
};
private static final Map<String, EscapeType> namedTypes = createHashMap();
private static final ArrayList<String> names = createArrayList();
private final String name;
static {
for (EscapeType escapeType : EscapeType.values()) {
namedTypes.put(escapeType.getName(), escapeType);
names.add(escapeType.getName());
}
names.trimToSize();
}
private EscapeType(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Object escape(Object value) {
return escape(value.toString());
}
protected abstract String escape(String strValue);
public static EscapeType getEscapeType(String name) {
name = trimToNull(name);
if (name != null) {
return namedTypes.get(name.toLowerCase());
}
return null;
}
public static List<String> getNames() {
return names;
}
@Override
public String toString() {
return "#escape(\"" + getName() + "\")";
}
}
/** Escape directive。 */
public static class Escape extends Directive {
@Override
public String getName() {
return "escape";
}
@Override
public int getType() {
return BLOCK;
}
@Override
public void init(RuntimeServices rs, InternalContextAdapter context, Node node) throws TemplateInitException {
super.init(rs, context, node);
if (node.jjtGetNumChildren() != 2) {
throw new TemplateInitException("Invalid args for #" + getName()
+ ". Expected 1 and only 1 string arg.", context.getCurrentTemplateName(), node.getColumn(),
node.getLine());
}
}
@Override
public boolean render(InternalContextAdapter context, Writer writer, Node node) throws IOException,
ResourceNotFoundException,
ParseErrorException,
MethodInvocationException {
Node escapeTypeNode = node.jjtGetChild(0);
Object escapeTypeObject = escapeTypeNode.value(context);
String escapeTypeString = escapeTypeObject == null ? null : escapeTypeObject.toString();
EscapeType escapeType = EscapeType.getEscapeType(escapeTypeString);
if (escapeType == null) {
throw new ParseErrorException("Invalid escape type: "
+ escapeTypeObject
+ " at "
+ new Info(escapeTypeNode.getTemplateName(), escapeTypeNode.getColumn(),
escapeTypeNode.getLine()) + ". Available escape types: " + EscapeType.getNames());
}
return renderWithEscape(escapeType, context, writer, node.jjtGetChild(1));
}
}
/** Noescape directive。 */
public static class Noescape extends Directive {
@Override
public String getName() {
return "noescape";
}
@Override
public int getType() {
return BLOCK;
}
@Override
public void init(RuntimeServices rs, InternalContextAdapter context, Node node) throws TemplateInitException {
super.init(rs, context, node);
if (node.jjtGetNumChildren() != 1) {
throw new TemplateInitException("Invalid args for #" + getName() + ". Expected 0 args.",
context.getCurrentTemplateName(), node.getColumn(), node.getLine());
}
}
@Override
public boolean render(InternalContextAdapter context, Writer writer, Node node) throws IOException,
ResourceNotFoundException,
ParseErrorException,
MethodInvocationException {
return renderWithEscape(EscapeType.NO_ESCAPE, context, writer, node.jjtGetChild(0));
}
}
public static class EscapeRule {
private final Pattern pattern;
private final EscapeType escape;
public EscapeRule(String escapeType, String[] patterns) {
this.escape = assertNotNull(EscapeType.getEscapeType(escapeType), "no escapeType specified");
assertTrue(!isEmptyArray(patterns), "no pattern specified");
StringBuilder buf = new StringBuilder();
for (String pattern : patterns) {
pattern = assertNotNull(trimToNull(pattern), "no pattern");
if (buf.length() > 0) {
buf.append("|");
}
buf.append("(").append(pattern).append(")");
}
this.pattern = Pattern.compile(buf.toString(), Pattern.CASE_INSENSITIVE);
}
public Pattern matches(String reference) {
if (pattern.matcher(reference).find()) {
return pattern;
}
return null;
}
public EscapeType getEscapeType() {
return escape;
}
@Override
public String toString() {
MapBuilder mb = new MapBuilder();
mb.append("escape", escape);
mb.append("pattern", pattern);
return new ToStringBuilder().append("EscapeRule").append(mb).toString();
}
}
}