ProtobufBuilderHandler.java

package space.sunqian.fs.third.protobuf;

import com.google.protobuf.Message;
import space.sunqian.annotation.Nonnull;
import space.sunqian.annotation.Nullable;
import space.sunqian.fs.Fs;
import space.sunqian.fs.base.exception.UnsupportedEnvException;
import space.sunqian.fs.invoke.Invocable;
import space.sunqian.fs.object.ObjectException;
import space.sunqian.fs.object.builder.BuilderOperator;
import space.sunqian.fs.object.builder.BuilderOperatorProvider;

import java.lang.reflect.Method;
import java.lang.reflect.Type;

/**
 * {@link BuilderOperatorProvider.Handler} implementation for
 * <a href="https://github.com/protocolbuffers/protobuf">Protocol Buffers</a>, can be quickly used through similar
 * codes:
 * <pre>{@code
 * BuilderOperatorProvider provider = ...;
 * BuilderOperatorProvider protoProvider = provider
 *     .withFirstHandler(ProtobufBuilderHandler.getInstance());
 * }</pre>
 * To use this class, the protobuf package {@code com.google.protobuf} must in the runtime environment. And in this
 * environment, the {@link BuilderOperatorProvider#defaultProvider()} will automatically load this handler.
 *
 * @author sunqian
 */
public class ProtobufBuilderHandler implements BuilderOperatorProvider.Handler {

    private static final @Nonnull ProtobufBuilderHandler INST = new ProtobufBuilderHandler();

    /**
     * Returns a same one instance of this handler.
     */
    public static @Nonnull ProtobufBuilderHandler getInstance() {
        return INST;
    }

    /**
     * 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 ProtobufBuilderHandler() throws UnsupportedEnvException {
        Fs.uncheck(() -> Class.forName("com.google.protobuf.Message"), UnsupportedEnvException::new);
    }

    @Override
    public @Nullable BuilderOperator newOperator(@Nonnull Type target) throws Exception {
        // Check whether it is a protobuf object
        if (!(target instanceof Class<?>)) {
            return null;
        }
        @SuppressWarnings("PatternVariableCanBeUsed")
        Class<?> rawTarget = (Class<?>) target;
        boolean isProtobuf = false;
        boolean isBuilder = false;
        if (Message.class.isAssignableFrom(rawTarget)) {
            isProtobuf = true;
        }
        if (Message.Builder.class.isAssignableFrom(rawTarget)) {
            isProtobuf = true;
            isBuilder = true;
        }
        if (!isProtobuf) {
            return null;
        }
        if (isBuilder) {
            Method buildMethod = rawTarget.getMethod("build");
            Class<?> messageType = buildMethod.getReturnType();
            Method builderMethod = messageType.getMethod("newBuilder");
            return new ImplForBuilder(builderMethod, target);
        } else {
            Method builderMethod = rawTarget.getMethod("newBuilder");
            Class<?> builderType = builderMethod.getReturnType();
            Method buildMethod = builderType.getMethod("build");
            return new ImplForMessage(builderMethod, buildMethod, builderType);
        }
    }

    private static final class ImplForBuilder implements BuilderOperator {

        private final @Nonnull Invocable builder;
        private final @Nonnull Type builderType;

        private ImplForBuilder(@Nonnull Method builderMethod, @Nonnull Type builderType) {
            this.builder = Invocable.of(builderMethod);
            this.builderType = builderType;
        }

        @Override
        public @Nonnull Type builderType() {
            return builderType;
        }

        @Override
        public @Nonnull Type targetType() {
            return builderType;
        }

        @Override
        public @Nonnull Object createBuilder() throws ObjectException {
            return Fs.uncheck(() -> builder.invoke(null), ObjectException::new);
        }

        @Override
        public @Nonnull Object buildTarget(@Nonnull Object builder) throws ObjectException {
            return builder;
        }
    }

    private static final class ImplForMessage implements BuilderOperator {

        private final @Nonnull Invocable builder;
        private final @Nonnull Invocable build;
        private final @Nonnull Type builderType;
        private final @Nonnull Type messageType;

        private ImplForMessage(
            @Nonnull Method builderMethod, @Nonnull Method buildMethod, @Nonnull Type builderType
        ) {
            this.builder = Invocable.of(builderMethod);
            this.build = Invocable.of(buildMethod);
            this.builderType = builderType;
            this.messageType = buildMethod.getReturnType();
        }

        @Override
        public @Nonnull Type builderType() {
            return builderType;
        }

        @Override
        public @Nonnull Type targetType() {
            return messageType;
        }

        @Override
        public @Nonnull Object createBuilder() throws ObjectException {
            return Fs.uncheck(() -> builder.invoke(null), ObjectException::new);
        }

        @Override
        public @Nonnull Object buildTarget(@Nonnull Object builder) throws ObjectException {
            return Fs.uncheck(() -> build.invoke(builder), ObjectException::new);
        }
    }
}