ProtobufSchemaHandler.java
package space.sunqian.common.third.protobuf;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import com.google.protobuf.ProtocolStringList;
import space.sunqian.annotations.Nonnull;
import space.sunqian.annotations.Nullable;
import space.sunqian.common.Fs;
import space.sunqian.common.base.exception.UnsupportedEnvException;
import space.sunqian.common.base.string.StringKit;
import space.sunqian.common.object.data.ObjectProperty;
import space.sunqian.common.object.data.ObjectPropertyBase;
import space.sunqian.common.object.data.ObjectSchemaParser;
import space.sunqian.common.invoke.Invocable;
import space.sunqian.common.reflect.TypeKit;
import space.sunqian.common.reflect.TypeRef;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* {@link ObjectSchemaParser.Handler} implementation for
* <a href="https://github.com/protocolbuffers/protobuf">Protocol Buffers</a>, can be quickly used through similar
* codes:
* <pre>{@code
* ObjectSchemaParser parser = ObjectSchemaParser
* .defaultParser()
* .withFirstHandler(new ProtobufSchemaHandler());
* }</pre>
* To use this class, the protobuf package {@code com.google.protobuf} must in the runtime environment.
* <p>
* Note:
* <ul>
* <li>
* When {@link ProtocolStringList} is used as a property type, it will be mapped to {@code List<String>}, but
* the type of the instance returned by {@link ObjectProperty#getValue(Object)} is still
* {@link ProtocolStringList};
* </li>
* <li>
* For a Builder, the properties of {@code repeated} and {@code map} types do not have setter methods, but the
* setter (which is am {@link Invocable}) is not null.
* </li>
* </ul>
*
* @author sunqian
*/
public class ProtobufSchemaHandler implements ObjectSchemaParser.Handler {
static final class StringListTypeRef extends TypeRef<List<String>> {
static final @Nonnull StringListTypeRef SINGLETON = new StringListTypeRef();
}
/**
* Constructs a new handler instance. This constructor will check whether the protobuf package is available in the
* current environment.
*
* @throws UnsupportedEnvException if the protobuf package is not available in the current environment.
*/
public ProtobufSchemaHandler() throws UnsupportedEnvException {
Fs.uncheck(() -> Class.forName("com.google.protobuf.Message"), UnsupportedEnvException::new);
}
@Override
public boolean parse(@Nonnull ObjectSchemaParser.Context context) throws Exception {
Class<?> rawType = TypeKit.getRawClass(context.dataType());
if (rawType == null) {
return true;
}
// Check whether it is a protobuf object
boolean isProtobuf = false;
boolean isBuilder = false;
if (Message.class.isAssignableFrom(rawType)) {
isProtobuf = true;
}
if (Message.Builder.class.isAssignableFrom(rawType)) {
isProtobuf = true;
isBuilder = true;
}
if (!isProtobuf) {
return true;
}
Method getDescriptorMethod = rawType.getMethod("getDescriptor");
Descriptors.Descriptor descriptor = (Descriptors.Descriptor) getDescriptorMethod.invoke(null);
for (Descriptors.FieldDescriptor field : descriptor.getFields()) {
ObjectPropertyBase objectPropertyBase = buildProperty(field, rawType, isBuilder);
context.propertyBaseMap().put(objectPropertyBase.name(), objectPropertyBase);
}
return false;
}
private @Nonnull ObjectPropertyBase buildProperty(
@Nonnull Descriptors.FieldDescriptor field,
@Nonnull Class<?> rawClass,
boolean isBuilder
) throws Exception {
String rawName = field.getName();
// map
if (field.isMapField()) {
String name = rawName + "Map";
Method getterMethod = rawClass.getMethod("get" + StringKit.capitalize(name));
Invocable getter = Invocable.of(getterMethod);
Invocable setter = null;
if (isBuilder) {
Method clearMethod = rawClass.getMethod("clear" + StringKit.capitalize(rawName));
Method putAllMethod = rawClass.getMethod("putAll" + StringKit.capitalize(rawName), Map.class);
setter = new MapSetter(clearMethod, putAllMethod);
}
return new PropertyBaseImpl(
rawName,
getterMethod.getGenericReturnType(),
getterMethod,
null,
getter,
setter
);
}
// repeated
if (field.isRepeated()) {
String name = rawName + "List";
Method getterMethod = rawClass.getMethod("get" + StringKit.capitalize(name));
Type type = getterMethod.getGenericReturnType();
if (Objects.equals(type, ProtocolStringList.class)) {
type = StringListTypeRef.SINGLETON.type();
}
Invocable getter = Invocable.of(getterMethod);
Invocable setter = null;
if (isBuilder) {
Method clearMethod = rawClass.getMethod("clear" + StringKit.capitalize(rawName));
Method addAllMethod = rawClass.getMethod("addAll" + StringKit.capitalize(rawName), Iterable.class);
setter = new ListSetter(clearMethod, addAllMethod);
}
return new PropertyBaseImpl(
rawName,
type,
getterMethod,
null,
getter,
setter
);
}
// Simple object
Method getterMethod = rawClass.getMethod("get" + StringKit.capitalize(rawName));
Type type = getterMethod.getGenericReturnType();
Invocable getter = Invocable.of(getterMethod);
Method setterMethod = null;
Invocable setter = null;
if (isBuilder) {
setterMethod = rawClass.getMethod("set" + StringKit.capitalize(rawName), TypeKit.getRawClass(type));
setter = Invocable.of(setterMethod);
}
return new PropertyBaseImpl(
rawName,
type,
getterMethod,
setterMethod,
getter,
setter
);
}
private static final class MapSetter implements Invocable {
private final @Nonnull MethodHandle clearHandle;
private final @Nonnull MethodHandle putAllHandle;
private MapSetter(@Nonnull Method clearMethod, @Nonnull Method putAllMethod) throws Exception {
// setter = (inst, args) -> {
// clearMethod.invoke(inst);
// return putAllMethod.invoke(inst, args);
// };
this.clearHandle = MethodHandles.lookup().unreflect(clearMethod);
this.putAllHandle = MethodHandles.lookup().unreflect(putAllMethod);
}
@Override
public @Nullable Object invokeChecked(
@Nullable Object inst, @Nullable Object @Nonnull ... args
) throws Throwable {
clearHandle.invoke(inst);
putAllHandle.invoke(inst, args[0]);
return null;
}
}
private static final class ListSetter implements Invocable {
private final @Nonnull MethodHandle clearHandle;
private final @Nonnull MethodHandle addAllMethod;
private ListSetter(@Nonnull Method clearMethod, @Nonnull Method addAllMethod) throws Exception {
// setter = (inst, args) -> {
// clearMethod.invoke(inst);
// return addAllMethod.invoke(inst, args);
// };
this.clearHandle = MethodHandles.lookup().unreflect(clearMethod);
this.addAllMethod = MethodHandles.lookup().unreflect(addAllMethod);
}
@Override
public @Nullable Object invokeChecked(
@Nullable Object inst, @Nullable Object @Nonnull ... args
) throws Throwable {
clearHandle.invoke(inst);
addAllMethod.invoke(inst, args[0]);
return null;
}
}
private static final class PropertyBaseImpl implements ObjectPropertyBase {
private final @Nonnull String name;
private final @Nonnull Type type;
private final @Nullable Method getterMethod;
private final @Nullable Method setterMethod;
private final @Nonnull Invocable getter;
private final @Nullable Invocable setter;
private PropertyBaseImpl(
@Nonnull String name,
@Nonnull Type type,
@Nullable Method getterMethod,
@Nullable Method setterMethod,
@Nonnull Invocable getter,
@Nullable Invocable setter
) {
this.name = name;
this.type = type;
this.getterMethod = getterMethod;
this.setterMethod = setterMethod;
this.getter = getter;
this.setter = setter;
}
@Override
public @Nonnull String name() {
return name;
}
@Override
public @Nonnull Type type() {
return type;
}
@Override
public @Nullable Method getterMethod() {
return getterMethod;
}
@Override
public @Nullable Method setterMethod() {
return setterMethod;
}
@Override
public @Nullable Field field() {
return null;
}
@Override
public @Nonnull Invocable getter() {
return getter;
}
@Override
public @Nullable Invocable setter() {
return setter;
}
}
}