/*
* Copyright 2013-2017 the original author or authors.
*
* 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 org.glowroot.agent.weaving;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glowroot.common.util.UsedByGeneratedBytecode;
import static com.google.common.base.Preconditions.checkNotNull;
@UsedByGeneratedBytecode
public class MessageTemplate {
private static final Logger logger = LoggerFactory.getLogger(MessageTemplate.class);
private static final Pattern pattern = Pattern.compile("\\{\\{([^}]*)}}");
private final ImmutableList<Part> allParts;
private final ImmutableList<ValuePathPart> thisPathParts;
private final ImmutableList<ArgPathPart> argPathParts;
private final ImmutableList<ValuePathPart> returnPathParts;
@UsedByGeneratedBytecode
public static MessageTemplate create(String template, Method method) {
List<Part> allParts = Lists.newArrayList();
List<ValuePathPart> thisPathParts = Lists.newArrayList();
List<ArgPathPart> argPathParts = Lists.newArrayList();
List<ValuePathPart> returnPathParts = Lists.newArrayList();
Matcher matcher = pattern.matcher(template);
int curr = 0;
while (matcher.find()) {
if (matcher.start() > curr) {
allParts.add(new ConstantPart(template.substring(curr, matcher.start())));
}
String group = checkNotNull(matcher.group(1));
String path = group.trim();
int index = path.indexOf('.');
String base;
String remaining;
if (index == -1) {
base = path;
remaining = "";
} else {
base = path.substring(0, index);
remaining = path.substring(index + 1);
}
if (base.equals("this")) {
ValuePathPart part = new ValuePathPart(PartType.THIS_PATH,
method.getDeclaringClass(), remaining);
allParts.add(part);
thisPathParts.add(part);
} else if (base.matches("[0-9]+")) {
int argNumber = Integer.parseInt(base);
if (argNumber < method.getParameterTypes().length) {
ArgPathPart part = new ArgPathPart(method.getParameterTypes()[argNumber],
remaining, argNumber);
allParts.add(part);
argPathParts.add(part);
} else {
allParts.add(new ConstantPart(
"<requested arg index out of bounds: " + argNumber + ">"));
}
} else if (base.equals("_")) {
ValuePathPart part =
new ValuePathPart(PartType.RETURN_PATH, method.getReturnType(), remaining);
allParts.add(part);
returnPathParts.add(part);
} else if (base.equals("methodName")) {
allParts.add(new Part(PartType.METHOD_NAME));
} else {
logger.warn("invalid template substitution: {}", path);
allParts.add(new ConstantPart("{{" + path + "}}"));
}
curr = matcher.end();
}
if (curr < template.length()) {
allParts.add(new ConstantPart(template.substring(curr)));
}
return new MessageTemplate(allParts, thisPathParts, argPathParts, returnPathParts);
}
private MessageTemplate(List<Part> allParts, List<ValuePathPart> thisPathParts,
List<ArgPathPart> argPathParts, List<ValuePathPart> returnPathParts) {
this.allParts = ImmutableList.copyOf(allParts);
this.thisPathParts = ImmutableList.copyOf(thisPathParts);
this.argPathParts = ImmutableList.copyOf(argPathParts);
this.returnPathParts = ImmutableList.copyOf(returnPathParts);
}
ImmutableList<Part> getAllParts() {
return allParts;
}
ImmutableList<ValuePathPart> getThisPathParts() {
return thisPathParts;
}
ImmutableList<ArgPathPart> getArgPathParts() {
return argPathParts;
}
ImmutableList<ValuePathPart> getReturnPathParts() {
return returnPathParts;
}
enum PartType {
CONSTANT, THIS_PATH, ARG_PATH, RETURN_PATH, METHOD_NAME;
}
static class Part {
private final PartType type;
private Part(PartType type) {
this.type = type;
}
PartType getType() {
return type;
}
}
static class ConstantPart extends Part {
private final String constant;
private ConstantPart(String constant) {
super(PartType.CONSTANT);
this.constant = constant;
}
String getConstant() {
return constant;
}
}
static class ValuePathPart extends Part {
private final PathEvaluator pathEvaluator;
private ValuePathPart(PartType partType, Class<?> valueClass, String propertyPath) {
super(partType);
this.pathEvaluator = new PathEvaluator(valueClass, propertyPath);
}
String evaluatePart(@Nullable Object base) {
if (base == null) {
// this is same as String.valueOf((Object) null);
return "null";
}
try {
return valueOf(pathEvaluator.evaluateOnBase(base));
} catch (InvocationTargetException e) {
logger.debug(e.getMessage(), e);
// InvocationTargetException has the problem of obscuring the original message
// to try to use cause
Throwable t = MoreObjects.firstNonNull(e.getCause(), e);
// using toString() instead of getMessage() in order to capture exception class name
return "<error evaluating: " + t.toString() + ">";
} catch (Exception e) {
logger.warn(e.getMessage(), e);
// using toString() instead of getMessage() in order to capture exception class name
return "<error evaluating: " + e.toString() + ">";
}
}
private String valueOf(@Nullable Object value) {
if (value == null || !value.getClass().isArray()) {
// shortcut the common case
return String.valueOf(value);
}
StringBuilder sb = new StringBuilder();
valueOfArray(value, sb);
return sb.toString();
}
private static void valueOfArray(Object array, StringBuilder sb) {
sb.append('[');
int len = Array.getLength(array);
for (int i = 0; i < len; i++) {
if (i > 0) {
sb.append(", ");
}
valueOf(Array.get(array, i), sb);
}
sb.append(']');
}
private static void valueOf(Object object, StringBuilder sb) {
if (object.getClass().isArray()) {
valueOfArray(object, sb);
} else {
sb.append(String.valueOf(object));
}
}
}
static class ArgPathPart extends ValuePathPart {
private final int argNumber;
private ArgPathPart(Class<?> argClass, String propertyPath, int argNumber) {
super(PartType.ARG_PATH, argClass, propertyPath);
this.argNumber = argNumber;
}
int getArgNumber() {
return argNumber;
}
}
@VisibleForTesting
static class PathEvaluator {
private static final Splitter splitter = Splitter.on('.').omitEmptyStrings();
private final Accessor[] accessors;
private final String /*@Nullable*/[] remainingPath;
PathEvaluator(Class<?> baseClass, String path) {
List<String> parts = Lists.newArrayList(splitter.split(path));
List<Accessor> accessors = Lists.newArrayList();
Class<?> currClass = baseClass;
while (!parts.isEmpty()) {
String currPart = parts.remove(0);
Accessor accessor = Beans.loadPossiblyArrayBasedAccessor(currClass, currPart);
if (accessor == null) {
parts.add(0, currPart);
break;
}
accessors.add(accessor);
currClass = accessor.getValueType();
}
this.accessors = accessors.toArray(new Accessor[accessors.size()]);
if (parts.isEmpty()) {
remainingPath = null;
} else {
remainingPath = parts.toArray(new String[parts.size()]);
}
}
@Nullable
Object evaluateOnBase(Object base) throws Exception {
Object curr = base;
for (Accessor accessor : accessors) {
curr = accessor.evaluate(curr);
if (curr == null) {
return null;
}
}
if (remainingPath != null) {
// too bad, revert to slow Beans
return Beans.value(curr, remainingPath);
}
return curr;
}
}
}