All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.awssdk.eventstreamrpc.EventStreamRPCServiceModel Maven / Gradle / Ivy

There is a newer version: 1.23.0
Show newest version
/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

package software.amazon.awssdk.eventstreamrpc;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import software.amazon.awssdk.eventstreamrpc.model.AccessDeniedException;
import software.amazon.awssdk.eventstreamrpc.model.EventStreamJsonMessage;
import software.amazon.awssdk.eventstreamrpc.model.UnsupportedOperationException;
import software.amazon.awssdk.eventstreamrpc.model.ValidationException;
import software.amazon.awssdk.crt.utils.StringUtils;

import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * Implementers of this service model are expected to likely be singletons. There
 * should be little value to having more than one, though between different instances
 * properly constructed for a service, they can be used interchangeably
 */
public abstract class EventStreamRPCServiceModel {
    private static final Gson GSON;

    /**
     * Version header string
     */
    static final String VERSION_HEADER = ":version";

    /**
     * Content type header string
     */
    public static final String CONTENT_TYPE_HEADER = ":content-type";

    /**
     * Content type application text string
     */
    public static final String CONTENT_TYPE_APPLICATION_TEXT = "text/plain";

    /**
     * Content type application json string
     */
    public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";

    /**
     * Service model type header
     */
    public static final String SERVICE_MODEL_TYPE_HEADER = "service-model-type";

    static {
        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeAdapterFactory(new ForceNullsForMapTypeAdapterFactory());
        builder.registerTypeAdapterFactory(OptionalTypeAdapter.FACTORY);
        builder.registerTypeAdapterFactory(EventStreamPostFromJsonTypeAdapter.FACTORY);
        builder.registerTypeAdapter(byte[].class, new Base64BlobSerializerDeserializer());
        builder.registerTypeAdapter(Instant.class, new InstantSerializerDeserializer());
        builder.excludeFieldsWithoutExposeAnnotation();
        GSON = builder.create();
    }

    // Type adapter to automatically call "postFromJson" on all instances of EventStreamJsonMessage we construct
    private static class EventStreamPostFromJsonTypeAdapter extends TypeAdapter {
        public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
            @Override
            public  TypeAdapter create(Gson gson, TypeToken type) {
                if (EventStreamJsonMessage.class.isAssignableFrom(type.getRawType())) {
                    final TypeAdapter delegate = gson.getDelegateAdapter(this, type);
                    return new EventStreamPostFromJsonTypeAdapter(delegate);
                }

                return null;
            }
        };

        private final TypeAdapter adapter;

        public EventStreamPostFromJsonTypeAdapter(TypeAdapter adapter) {
            this.adapter = adapter;
        }

        @Override
        public void write(JsonWriter out, E value) throws IOException {
            adapter.write(out, value);
        }

        @Override
        public E read(JsonReader in) throws IOException {
            E obj = adapter.read(in);
            if (obj != null) {
                // Call postFromJson to finalize the deserialization. Especially important for unions to have their
                // member get set correctly.
                obj.postFromJson();
            }
            return obj;
        }
    }

    private static class ForceNullsForMapTypeAdapterFactory implements TypeAdapterFactory {

        public final  TypeAdapter create(Gson gson, TypeToken type) {
            if (Map.class.isAssignableFrom(type.getRawType())) {
                final TypeAdapter delegate = gson.getDelegateAdapter(this, type);
                return createCustomTypeAdapter(delegate);
            }

            return null;
        }

        private  TypeAdapter createCustomTypeAdapter(TypeAdapter delegate) {
            return new TypeAdapter() {
                @Override
                public void write(JsonWriter out, T value) throws IOException {
                    final boolean serializeNulls = out.getSerializeNulls();
                    try {
                        out.setSerializeNulls(true);
                        delegate.write(out, value);
                    } finally {
                        out.setSerializeNulls(serializeNulls);
                    }
                }

                @Override
                public T read(JsonReader in) throws IOException {
                    return delegate.read(in);
                }
            };
        }
    }

    private static class OptionalTypeAdapter extends TypeAdapter> {
        public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
            @Override
            public  TypeAdapter create(Gson gson, TypeToken type) {
                Class rawType = (Class) type.getRawType();
                if (rawType != Optional.class) {
                    return null;
                }
                final ParameterizedType parameterizedType = (ParameterizedType) type.getType();
                final Type actualType = parameterizedType.getActualTypeArguments()[0];
                final TypeAdapter adapter = gson.getAdapter(TypeToken.get(actualType));
                return new OptionalTypeAdapter(adapter);
            }
        };

        private final TypeAdapter adapter;

        public OptionalTypeAdapter(TypeAdapter adapter) {
            this.adapter = adapter;
        }

        @Override
        public void write(JsonWriter out, Optional value) throws IOException {
            if (value.isPresent()){
                adapter.write(out, value.get());
            } else if (value != null) {
                out.nullValue();
            } else { }
        }

        @Override
        public Optional read(JsonReader in) throws IOException {
            return Optional.ofNullable(adapter.read(in));
        }
    }

    /**
     * Used to compare two members of a blob shape for equality. Array equals nesting
     * inside of an Optional doesn't work
     *
     * Note: Generated code for equals method of Smithy shapes relies on this
     *
     * @param lhs The first to compare
     * @param rhs The second to compare
     * @return True if both are equal, false otherwise
     */
    public static boolean blobTypeEquals(Optional lhs, Optional rhs) {
        if (lhs.equals(rhs)) {
            //both are same instance, both are same contained array, or both are empty
            return true;
        }
        if (!lhs.isPresent() || !rhs.isPresent()) {
            //if just one or the other is empty at this point
            return false;
        }
        //now we know both are present so compare the arrays
        return Arrays.equals(lhs.get(), rhs.get());
    }

    private static class Base64BlobSerializerDeserializer implements JsonSerializer, JsonDeserializer {
        @Override
        public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
            return StringUtils.base64Decode(json.getAsString().getBytes());
        }

        @Override
        public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
            return new JsonPrimitive(new String(StringUtils.base64Encode(src)));
        }
    }

    private static class InstantSerializerDeserializer implements JsonSerializer, JsonDeserializer {
        @Override
        public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
            double fSecondsEpoch = json.getAsDouble();
            long secondsEpoch = (long)fSecondsEpoch;
            long nanoEpoch = (long)((fSecondsEpoch - secondsEpoch) * 1_000_000_000.);
            return Instant.ofEpochSecond(secondsEpoch, nanoEpoch);
        }

        @Override
        public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) {
            return new JsonPrimitive((double)src.getEpochSecond() + (double)src.getNano() / 1000000000.);
        }
    }

    /**
     * For getting the actual service name
     * @return The name of the service as a string
     */
    public abstract String getServiceName();

    private static final Map> FRAMEWORK_APPLICATION_MODEL_TYPES
            = new HashMap<>();
    static {
        //TODO: find a reliable way to verify all of these are set? reflection cannot scan a package
        FRAMEWORK_APPLICATION_MODEL_TYPES.put(AccessDeniedException.ERROR_CODE, AccessDeniedException.class);
        FRAMEWORK_APPLICATION_MODEL_TYPES.put(UnsupportedOperationException.ERROR_CODE, UnsupportedOperationException.class);
        FRAMEWORK_APPLICATION_MODEL_TYPES.put(ValidationException.ERROR_CODE, ValidationException.class);
    }

    /**
     * Returns the application model class
     * @param applicationModelType The application model
     * @return The class of the given application model
     */
    final public Optional> getApplicationModelClass(final String applicationModelType) {
        final Class clazz = FRAMEWORK_APPLICATION_MODEL_TYPES.get(applicationModelType);
        if (clazz != null) {
            return Optional.of(clazz);
        }
        return getServiceClassType(applicationModelType);
    }

    /**
     * Retreives all operations on the service
     * @return All operations on the service
     */
    public abstract Collection getAllOperations();

    /**
     * Need to override per specific service type so it can look up all associated types and errors
     * possible.
     *
     * @param applicationModelType The application model
     * @return The service class type of the given application model
     */
    protected abstract Optional> getServiceClassType(String applicationModelType);

    /**
     * Retrieves the operation model context for a given operation name on the service
     *
     * This may not be a useful interface as generated code will typically pull a known operation model context
     * Public visibility is useful for testing
     *
     * @param operationName The name of the operation
     * @return The operation context associated with the given operation name
     */
    public abstract OperationModelContext getOperationModelContext(String operationName);

    public byte[] toJson(final EventStreamJsonMessage message) {
        try {
            final byte[] json = message.toPayload(getGson());
            final String stringJson = new String(json, StandardCharsets.UTF_8);
            //this feels like a hack. I'd prefer if java objects with no fields set serialized to being an empty object
            //rather than "null"
            if (null == stringJson || "null".equals(stringJson) || stringJson.isEmpty()) {
                return "{}".getBytes(StandardCharsets.UTF_8);
            }
            return json;
        } catch (Exception e) {
            throw new SerializationException(message, e);
        }
    }

    /**
     * Converts the given EventStreamJsonMessage to a JSON string
     * @param message The message to convert
     * @return A JSON string
     */
    public String toJsonString(final EventStreamJsonMessage message) {
        return new String(toJson(message), StandardCharsets.UTF_8);
    }

    /**
     * Internal getter method can be used by subclasses of specific service models to override default Gson
     * @return Returns GSON context
     */
    protected Gson getGson() {
        return GSON;
    }

    /**
     * In situations where the framework needs to do some JSON processing
     * without a specific service/operation in context
     *
     * @return the static Gson instance capable of processing the basics of EventStreamableJsonMessage
     */
    public static Gson getStaticGson() {
        return GSON;
    }

    /**
     * Creates a EventStreamJsonMessage from the given application model type string and payload.
     * Uses this service's specific model class to create the EventStreamJsonMessage.
     * @param applicationModelType The application model type string
     * @param payload The payload
     * @return A EventStreamMessage
     */
    public EventStreamJsonMessage fromJson(final String applicationModelType, byte[] payload) {
        final Optional> clazz = getApplicationModelClass(applicationModelType);
        if (!clazz.isPresent()) {
            throw new UnmappedDataException(applicationModelType);
        }
        return fromJson(clazz.get(), payload);
    }

    /**
     * Creates a EventStreamJsonMessage of type T from the given application model
     * class and payload.
     * @param  The type to convert the result to
     * @param clazz The class
     * @param payload The payload
     * @return A EventStreamMessage of type T
     */
    public  T fromJson(final Class clazz, byte[] payload) {
        try {
            return getGson().fromJson(new String(payload, StandardCharsets.UTF_8), clazz);
        } catch (Exception e) {
            throw new DeserializationException(payload, e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy