// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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.linecorp.armeria.common.thrift.text; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; import org.apache.thrift.TApplicationException; import org.apache.thrift.TBase; import org.apache.thrift.TException; import org.apache.thrift.TFieldIdEnum; import org.apache.thrift.meta_data.EnumMetaData; import org.apache.thrift.meta_data.FieldMetaData; import org.apache.thrift.meta_data.FieldValueMetaData; import org.apache.thrift.meta_data.ListMetaData; import org.apache.thrift.meta_data.MapMetaData; import org.apache.thrift.meta_data.SetMetaData; import org.apache.thrift.meta_data.StructMetaData; import org.apache.thrift.protocol.TField; import org.apache.thrift.protocol.TType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; import com.linecorp.armeria.internal.thrift.TApplicationExceptions; /** * A struct parsing context. Builds a map from field name to TField. * * @author Alex Roetter */ class StructContext extends PairContext { private static final Logger log = LoggerFactory.getLogger(StructContext.class); // When processing a given thrift struct, we need certain information // for every field in that struct. We store that here, in a map // from fieldName (a string) to a TField object describing that // field. private final Map<String, TField> fieldNameMap; private final Map<String, Class<?>> classMap; /** * Build the name -> TField map. */ StructContext(JsonNode json) { this(json, getCurrentThriftMessageClass()); } StructContext(JsonNode json, Class<?> clazz) { super(json); classMap = new HashMap<>(); fieldNameMap = computeFieldNameMap(clazz); } @Override protected TField getTFieldByName(String name) throws TException { if (!fieldNameMap.containsKey(name)) { throw new TException("Unknown field: " + name); } return fieldNameMap.get(name); } @Override @Nullable protected Class<?> getClassByFieldName(String fieldName) { return classMap.get(fieldName); } /** * I need to know what type thrift message we are processing, * in order to look up fields by their field name. For example, * i I parse a line "count : 7", I need to know we are in a * StatsThriftMessage, or similar, to know that count should be * of type int32, and have a thrift id 1. * * <p>In order to figure this out, I assume that this method was * called (indirectly) by the read() method in a class T which * is a TBase subclass. It is called that way by thrift generated * code. So, I iterate backwards up the call stack, stopping * at the first method call which belongs to a TBase object. * I return the Class for that object. * * <p>One could argue this is someone fragile and error prone. * The alternative is to modify the thrift compiler to generate * code which passes class information into this (and other) * TProtocol objects, and that seems like a lot more work. Given * the low level interface of TProtocol (e.g. methods like readInt(), * rather than readThriftMessage()), it seems likely that a TBase * subclass, which has the knowledge of what fields exist, as well as * their types & relationships, will have to be the caller of * the TProtocol methods. * * <p>Note: this approach does not handle TUnion, because TUnion has its own implementation of * read/write and any TUnion thrift structure does not override its read and write method. * Thus this algorithm fail to get current specific TUnion thrift structure by reading the stack. * To fix this, we can track call stack of nested thrift objects on our own by overriding * TProtocol.writeStructBegin(), rather than relying on the stack trace. */ private static Class<?> getCurrentThriftMessageClass() { StackTraceElement[] frames = Thread.currentThread().getStackTrace(); for (StackTraceElement f : frames) { String className = f.getClassName(); try { Class<?> clazz = Class.forName(className); // Note, we need to check // if the class is abstract, because abstract class does not have metaDataMap // if the class has no-arg constructor, because FieldMetaData.getStructMetaDataMap // calls clazz.newInstance if (isTBase(clazz) && !isAbstract(clazz) && hasNoArgConstructor(clazz)) { return clazz; } if (isTApplicationException(clazz)) { return clazz; } if (isTApplicationExceptions(clazz)) { return TApplicationException.class; } } catch (ClassNotFoundException ex) { log.warn("Can't find class: " + className, ex); } } throw new RuntimeException("Must call (indirectly) from a TBase/TApplicationException object."); } private static boolean isTBase(Class<?> clazz) { return TBase.class.isAssignableFrom(clazz); } private static boolean isTApplicationException(Class<?> clazz) { return TApplicationException.class.isAssignableFrom(clazz); } private static boolean isTApplicationExceptions(Class<?> clazz) { return clazz == TApplicationExceptions.class; } private static boolean isAbstract(Class<?> clazz) { return Modifier.isAbstract(clazz.getModifiers()); } private static boolean hasNoArgConstructor(Class<?> clazz) { Constructor<?>[] allConstructors = clazz.getConstructors(); for (Constructor<?> ctor : allConstructors) { Class<?>[] pType = ctor.getParameterTypes(); if (pType.length == 0) { return true; } } return false; } /** * Compute a new field name map for the current thrift message * we are parsing. */ private Map<String, TField> computeFieldNameMap(Class<?> clazz) { Map<String, TField> map = new HashMap<>(); if (isTBase(clazz)) { // Get the metaDataMap for this Thrift class @SuppressWarnings("unchecked") Map<? extends TFieldIdEnum, FieldMetaData> metaDataMap = FieldMetaData.getStructMetaDataMap((Class<? extends TBase<?, ?>>) clazz); for (Entry<? extends TFieldIdEnum, FieldMetaData> e : metaDataMap.entrySet()) { final String fieldName = e.getKey().getFieldName(); final FieldMetaData metaData = e.getValue(); final FieldValueMetaData elementMetaData; if (metaData.valueMetaData.isContainer()) { if (metaData.valueMetaData instanceof SetMetaData) { elementMetaData = ((SetMetaData) metaData.valueMetaData).elemMetaData; } else if (metaData.valueMetaData instanceof ListMetaData) { elementMetaData = ((ListMetaData) metaData.valueMetaData).elemMetaData; } else if (metaData.valueMetaData instanceof MapMetaData) { elementMetaData = ((MapMetaData) metaData.valueMetaData).valueMetaData; } else { // Unrecognized container type, but let's still continue processing without // special enum support. elementMetaData = metaData.valueMetaData; } } else { elementMetaData = metaData.valueMetaData; } if (elementMetaData instanceof EnumMetaData) { classMap.put(fieldName, ((EnumMetaData) elementMetaData).enumClass); } else if (elementMetaData instanceof StructMetaData) { classMap.put(fieldName, ((StructMetaData) elementMetaData).structClass); } // Workaround a bug in the generated thrift message read() // method by mapping the ENUM type to the INT32 type // The thrift generated parsing code requires that, when expecting // a value of enum, we actually parse a value of type int32. The // generated read() method then looks up the enum value in a map. byte type = TType.ENUM == metaData.valueMetaData.type ? TType.I32 : metaData.valueMetaData.type; map.put(fieldName, new TField(fieldName, type, e.getKey().getThriftFieldId())); } } else { // TApplicationException map.put("message", new TField("message", (byte)11, (short)1)); map.put("type", new TField("type", (byte)8, (short)2)); } return map; } }