/* * Copyright 2012-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.model; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.glowroot.wire.api.model.TraceOuterClass.Trace; import static com.google.common.base.Preconditions.checkNotNull; public class DetailMapWriter { private static final Logger logger = LoggerFactory.getLogger(DetailMapWriter.class); private static final int MESSAGE_CHAR_LIMIT = Integer.getInteger("glowroot.message.char.limit", 100000); private static final String UNSHADED_GUAVA_OPTIONAL_CLASS_NAME; static { String className = Optional.class.getName(); if (className.startsWith("org.glowroot.agent.shaded")) { className = className.replace("org.glowroot.agent.shaded", "com"); } UNSHADED_GUAVA_OPTIONAL_CLASS_NAME = className; } private DetailMapWriter() {} public static List<Trace.DetailEntry> toProto( Map<String, ?> detail) { return writeMap(detail); } private static List<Trace.DetailEntry> writeMap(Map<?, ?> detail) { List<Trace.DetailEntry> entries = Lists.newArrayListWithCapacity(detail.size()); for (Entry<?, ?> entry : detail.entrySet()) { Object key = entry.getKey(); if (key == null) { // skip invalid data logger.warn("detail map has null key"); continue; } String name; if (key instanceof String) { name = (String) key; } else { name = convertToStringAndTruncate(key); } entries.add(createDetailEntry(name, entry.getValue())); } return entries; } private static Trace.DetailEntry createDetailEntry(String name, @Nullable Object value) { if (value instanceof Map) { return Trace.DetailEntry.newBuilder().setName(name) .addAllChildEntry(writeMap((Map<?, ?>) value)).build(); } else if (value instanceof List) { Trace.DetailEntry.Builder builder = Trace.DetailEntry.newBuilder().setName(name); for (Object v : (List<?>) value) { addValue(builder, v); } return builder.build(); } else { // simple value Trace.DetailEntry.Builder builder = Trace.DetailEntry.newBuilder().setName(name); addValue(builder, value); return builder.build(); } } private static void addValue(Trace.DetailEntry.Builder builder, @Nullable Object possiblyOptionalValue) { Object value = stripOptional(possiblyOptionalValue); if (value == null) { // add nothing (as a corollary, this will strip null/Optional.absent() items from lists) } else if (value instanceof String) { builder.addValueBuilder().setString((String) value).build(); } else if (value instanceof Boolean) { builder.addValueBuilder().setBoolean((Boolean) value).build(); } else if (value instanceof Long) { builder.addValueBuilder().setLong((Long) value).build(); } else if (value instanceof Number) { builder.addValueBuilder().setDouble(((Number) value).doubleValue()).build(); } else { logger.warn("detail map has unexpected value type: {}", value.getClass().getName()); builder.addValueBuilder().setString(convertToStringAndTruncate(value)).build(); } } private static @Nullable Object stripOptional(@Nullable Object value) { if (value == null) { return null; } if (value instanceof Optional) { Optional<?> val = (Optional<?>) value; return val.orNull(); } if (isUnshadedGuavaOptional(value) || isGuavaOptionalInAnotherClassLoader(value)) { // this is just for plugin tests that run against shaded glowroot-core Class<?> optionalClass = value.getClass().getSuperclass(); // just tested that super class is not null in condition checkNotNull(optionalClass); try { Method orNullMethod = optionalClass.getMethod("orNull"); return orNullMethod.invoke(value); } catch (Exception e) { logger.error(e.getMessage(), e); return null; } } return value; } private static boolean isUnshadedGuavaOptional(Object value) { Class<?> superClass = value.getClass().getSuperclass(); return superClass != null && superClass.getName().equals(UNSHADED_GUAVA_OPTIONAL_CLASS_NAME); } private static boolean isGuavaOptionalInAnotherClassLoader(Object value) { Class<?> superClass = value.getClass().getSuperclass(); return superClass != null && superClass.getName().equals(Optional.class.getName()); } // unexpected keys and values are not truncated in org.glowroot.agent.plugin.api.MessageImpl, so // need to be truncated here after converting them to strings private static String convertToStringAndTruncate(Object obj) { String str = obj.toString(); if (str == null) { return ""; } return truncate(str); } private static String truncate(String s) { if (s.length() <= MESSAGE_CHAR_LIMIT) { return s; } else { return s.substring(0, MESSAGE_CHAR_LIMIT) + " [truncated to " + MESSAGE_CHAR_LIMIT + " characters]"; } } }