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

com.github.antoniomacri.reactivegwt.proxy.SyncClientSerializationStreamReader Maven / Gradle / Ivy

The newest version!
/*
 * Copyright www.gdevelop.com.
 *
 * 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 com.github.antoniomacri.reactivegwt.proxy;

import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamReader;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.impl.SerializabilityUtil;
import com.google.gwt.user.server.rpc.impl.SerializedInstanceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.*;
import java.util.*;


/**
 * @see com.google.gwt.user.client.rpc.impl.ClientSerializationStreamWriter
 * @see com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader
 * @see com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriter
 * @see com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader
 */
public class SyncClientSerializationStreamReader extends AbstractSerializationStreamReader {
    private static final Logger log = LoggerFactory.getLogger(SyncClientSerializationStreamReader.class);

    /**
     * Used to accumulate elements while deserializing array types. The generic
     * type of the BoundedList will vary from the component type of the array it
     * is intended to create when the array is of a primitive type.
     *
     * @param  The type of object used to hold the data in the buffer
     */
    private static class BoundedList extends LinkedList {

        private static final long serialVersionUID = 1374814408067623454L;
        private final Class componentType;
        private final int expectedSize;

        public BoundedList(Class componentType, int expectedSize) {
            this.componentType = componentType;
            this.expectedSize = expectedSize;
        }

        @Override
        public boolean add(T o) {
            assert size() < getExpectedSize();
            return super.add(o);
        }

        public Class getComponentType() {
            return this.componentType;
        }

        public int getExpectedSize() {
            return this.expectedSize;
        }
    }

    /**
     * Enumeration used to provided typed instance readers.
     */
    private enum ValueReader {
        BOOLEAN {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readBoolean();
            }
        },
        BYTE {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readByte();
            }
        },
        CHAR {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readChar();
            }
        },
        DOUBLE {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readDouble();
            }
        },
        FLOAT {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readFloat();
            }
        },
        INT {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readInt();
            }
        },
        LONG {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readLong();
            }
        },
        OBJECT {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) throws SerializationException {
                return stream.readObject();
            }
        },
        SHORT {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readShort();
            }
        },
        STRING {
            @Override
            Object readValue(SyncClientSerializationStreamReader stream) {
                return stream.readString();
            }
        };

        abstract Object readValue(SyncClientSerializationStreamReader stream)
                throws SerializationException;
    }

    /**
     * Enumeration used to provided typed instance readers for vectors.
     */
    private enum VectorReader {
        BOOLEAN_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readBoolean();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setBoolean(array, index, (Boolean) value);
            }
        },
        BYTE_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readByte();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setByte(array, index, (Byte) value);
            }
        },
        CHAR_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readChar();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setChar(array, index, (Character) value);
            }
        },
        DOUBLE_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readDouble();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setDouble(array, index, (Double) value);
            }
        },
        FLOAT_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readFloat();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setFloat(array, index, (Float) value);
            }
        },
        INT_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readInt();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setInt(array, index, (Integer) value);
            }
        },
        LONG_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readLong();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setLong(array, index, (Long) value);
            }
        },
        OBJECT_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream)
                    throws SerializationException {
                return stream.readObject();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.set(array, index, value);
            }
        },
        SHORT_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readShort();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.setShort(array, index, (Short) value);
            }
        },
        STRING_VECTOR {
            @Override
            protected Object readSingleValue(SyncClientSerializationStreamReader stream) {
                return stream.readString();
            }

            @Override
            protected void setSingleValue(Object array, int index, Object value) {
                Array.set(array, index, value);
            }
        };

        Object read(SyncClientSerializationStreamReader stream,
                    BoundedList instance) throws SerializationException {
            for (int i = 0, n = instance.getExpectedSize(); i < n; ++i) {
                instance.add(readSingleValue(stream));
            }

            return toArray(instance.getComponentType(), instance);
        }

        protected abstract Object readSingleValue(SyncClientSerializationStreamReader stream)
                throws SerializationException;

        protected abstract void setSingleValue(Object array, int index,
                                               Object value);

        /**
         * Convert a BoundedList to an array of the correct type. This
         * implementation consumes the BoundedList.
         */
        protected Object toArray(Class componentType,
                                 BoundedList buffer) throws SerializationException {
            if (buffer.getExpectedSize() != buffer.size()) {
                throw new SerializationException(
                        "Inconsistent number of elements received. Received "
                        + buffer.size() + " but expecting "
                        + buffer.getExpectedSize());
            }

            Object arr = Array.newInstance(componentType, buffer.size());

            for (int i = 0, n = buffer.size(); i < n; i++) {
                setSingleValue(arr, i, buffer.removeFirst());
            }

            return arr;
        }
    }

    private static final char JS_ESCAPE_CHAR = '\\';

    /**
     * Map of {@link Class} objects to {@link ValueReader}s.
     */
    private static final Map, ValueReader> CLASS_TO_VALUE_READER = new IdentityHashMap<>();

    /**
     * Map of {@link Class} objects to {@link VectorReader}s.
     */
    private static final Map, VectorReader> CLASS_TO_VECTOR_READER = new IdentityHashMap<>();

    static {
        CLASS_TO_VECTOR_READER.put(boolean[].class,
                SyncClientSerializationStreamReader.VectorReader.BOOLEAN_VECTOR);
        CLASS_TO_VECTOR_READER.put(byte[].class,
                SyncClientSerializationStreamReader.VectorReader.BYTE_VECTOR);
        CLASS_TO_VECTOR_READER.put(char[].class,
                SyncClientSerializationStreamReader.VectorReader.CHAR_VECTOR);
        CLASS_TO_VECTOR_READER.put(double[].class,
                SyncClientSerializationStreamReader.VectorReader.DOUBLE_VECTOR);
        CLASS_TO_VECTOR_READER.put(float[].class,
                SyncClientSerializationStreamReader.VectorReader.FLOAT_VECTOR);
        CLASS_TO_VECTOR_READER.put(int[].class,
                SyncClientSerializationStreamReader.VectorReader.INT_VECTOR);
        CLASS_TO_VECTOR_READER.put(long[].class,
                SyncClientSerializationStreamReader.VectorReader.LONG_VECTOR);
        CLASS_TO_VECTOR_READER.put(Object[].class,
                SyncClientSerializationStreamReader.VectorReader.OBJECT_VECTOR);
        CLASS_TO_VECTOR_READER.put(short[].class,
                SyncClientSerializationStreamReader.VectorReader.SHORT_VECTOR);
        CLASS_TO_VECTOR_READER.put(String[].class,
                SyncClientSerializationStreamReader.VectorReader.STRING_VECTOR);

        CLASS_TO_VALUE_READER.put(boolean.class,
                SyncClientSerializationStreamReader.ValueReader.BOOLEAN);
        CLASS_TO_VALUE_READER.put(byte.class,
                SyncClientSerializationStreamReader.ValueReader.BYTE);
        CLASS_TO_VALUE_READER.put(char.class,
                SyncClientSerializationStreamReader.ValueReader.CHAR);
        CLASS_TO_VALUE_READER.put(double.class,
                SyncClientSerializationStreamReader.ValueReader.DOUBLE);
        CLASS_TO_VALUE_READER.put(float.class,
                SyncClientSerializationStreamReader.ValueReader.FLOAT);
        CLASS_TO_VALUE_READER.put(int.class,
                SyncClientSerializationStreamReader.ValueReader.INT);
        CLASS_TO_VALUE_READER.put(long.class,
                SyncClientSerializationStreamReader.ValueReader.LONG);
        CLASS_TO_VALUE_READER.put(Object.class,
                SyncClientSerializationStreamReader.ValueReader.OBJECT);
        CLASS_TO_VALUE_READER.put(short.class,
                SyncClientSerializationStreamReader.ValueReader.SHORT);
        CLASS_TO_VALUE_READER.put(String.class,
                SyncClientSerializationStreamReader.ValueReader.STRING);
    }

    private final List results = new ArrayList<>();

    private int index;

    private final List stringTable = new ArrayList<>();

    private final SerializationPolicy serializationPolicy;

    private static final String PRELUDE = "].concat([";

    private static final String POSTLUDE1 = "],[";

    private static final String POSTLUDE = "])";

    public SyncClientSerializationStreamReader(SerializationPolicy serializationPolicy) {
        this.serializationPolicy = serializationPolicy;
    }

    private void buildStringTable() {
        String raw = this.results.get(--this.index);
        byte b1;
        byte b2;
        byte b3;
        byte b4;

        boolean startNewString = true;
        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < raw.length(); i++) {
            char ch = raw.charAt(i);
            if (startNewString) {
                assert ch == '\"';
                startNewString = false;
                continue;
            }
            if (ch == '\"') { // end-of-string
                this.stringTable.add(buffer.toString());

                buffer.setLength(0);
                startNewString = true;

                if (i != raw.length() - 1) {
                    assert raw.charAt(i + 1) == ',';
                    i++;
                }
                continue;
            }
            if (ch == JS_ESCAPE_CHAR) {
                i++;
                ch = raw.charAt(i);
                switch (ch) {
                    case '0' -> // \0
                            buffer.append('\u0000');
                    case 'b' -> // \b
                            buffer.append('\b');
                    case 't' -> // \t
                            buffer.append('\t');
                    case 'n' -> // \n
                            buffer.append('\n');
                    case 'f' -> // \f
                            buffer.append('\f');
                    case 'r' -> // \r
                            buffer.append('\r');
                    case '\"' -> // \"
                            buffer.append('\"');
                    case '\\' -> // \\
                            buffer.append('\\');
                    case 'x' -> { // \\xNN
                        b1 = hex2byte(raw.charAt(++i));
                        b2 = hex2byte(raw.charAt(++i));
                        ch = (char) (b1 * 16 + b2);
                        buffer.append(ch);
                    }
                    case 'u' -> { // \\uNNNN
                        b1 = hex2byte(raw.charAt(++i));
                        b2 = hex2byte(raw.charAt(++i));
                        b3 = hex2byte(raw.charAt(++i));
                        b4 = hex2byte(raw.charAt(++i));
                        ch = (char) (b1 * 16 * 16 * 16 + b2 * 16 * 16 + b3 * 16 + b4);
                        buffer.append(ch);
                    }
                    default -> {
                        // TODO:
                        System.out.println("???");
                        throw new RuntimeException("Unhandled JS Escape Char Type");
                    }
                }
            } else {
                buffer.append(ch);
            }
        }
    }

    private String deconcat(String encoded) {
        int start = encoded.indexOf(PRELUDE);
        if (start > 0) {
            StringBuilder ret = new StringBuilder(encoded.length() - PRELUDE.length());
            ret.append(encoded, 0, start);

            start += PRELUDE.length();
            int end = encoded.indexOf(POSTLUDE1, start);
            while (end > 0) {
                ret.append(",");
                ret.append(encoded, start, end);

                start = end + POSTLUDE1.length();
                end = encoded.indexOf(POSTLUDE1, start);
            }

            end = encoded.indexOf(POSTLUDE, start);
            if (end > 0) {
                ret.append(",");
                ret.append(encoded, start, end + 1);
                return ret.toString();
            }
        }

        return encoded;
    }

    @Override
    protected Object deserialize(String typeSignature) throws SerializationException {
        Object instance;
        SerializedInstanceReference serializedInstRef = SerializabilityUtil.decodeSerializedInstanceReference(typeSignature);

        try {
            // Class instanceClass =
            // Class.forName(serializedInstRef.getName(),
            // false, null);
            Class instanceClass = ClassLoading.loadClass(serializedInstRef.getName());

            assert this.serializationPolicy != null;

            try {
                this.serializationPolicy.validateDeserialize(instanceClass);
            } catch (SerializationException e) {
                log.warn("Failed validateDeserialize", e);
            }

            // TODO validateTypeVersions(instanceClass, serializedInstRef);

            Class customSerializer = SerializabilityUtil.hasCustomFieldSerializer(instanceClass);

            int index = reserveDecodedObjectIndex();

            instance = instantiate(customSerializer, instanceClass);

            rememberDecodedObject(index, instance);

            Object replacement = deserializeImpl(customSerializer, instanceClass, instance);

            // It's possible that deserializing an object requires the original
            // proxy
            // object to be replaced.
            if (instance != replacement) {
                rememberDecodedObject(index, replacement);
                instance = replacement;
            }

            return instance;

        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException |
                 InvocationTargetException | NoSuchMethodException e) {
            throw new SerializationException(e);
        }
    }

    /**
     * Deserialize an instance that is an array. Will default to deserializing
     * as an Object vector if the instance is not a primitive vector.
     */
    @SuppressWarnings("unchecked")
    private Object deserializeArray(Class instanceClass, Object instance) throws SerializationException {
        assert instanceClass.isArray();

        BoundedList buffer = (BoundedList) instance;
        VectorReader instanceReader = CLASS_TO_VECTOR_READER.get(instanceClass);
        return Objects.requireNonNullElse(instanceReader, VectorReader.OBJECT_VECTOR).read(this, buffer);
    }

    private void deserializeClass(Class instanceClass, Object instance)
            throws SerializationException, IllegalAccessException,
            NoSuchMethodException, InvocationTargetException,
            ClassNotFoundException {
        // Patch for Issue 36
        // if there are server fields ignore them
        if (this.serializationPolicy.getClientFieldNamesForEnhancedClass(instanceClass) != null) {
            int encodedPosition = readInt();
            log.info("For class={} received encodedField={}", instanceClass, getString(encodedPosition));
        }
        Field[] serializableFields = SerializabilityUtil.applyFieldSerializationPolicy(instanceClass, this.serializationPolicy);
        for (Field declField : serializableFields) {
            assert declField != null;

            Object value = deserializeValue(declField.getType());

            boolean isAccessible = declField.isAccessible();
            boolean needsAccessOverride = !isAccessible && !Modifier.isPublic(declField.getModifiers());
            if (needsAccessOverride) {
                // Override access restrictions
                declField.setAccessible(true);
            }

            declField.set(instance, value);
        }

        Class superClass = instanceClass.getSuperclass();
        if (this.serializationPolicy.shouldDeserializeFields(superClass)) {
            deserializeImpl(SerializabilityUtil.hasCustomFieldSerializer(superClass), superClass, instance);
        }
    }

    private Object deserializeImpl(Class customSerializer,
                                   Class instanceClass, Object instance)
            throws NoSuchMethodException, IllegalArgumentException,
            IllegalAccessException, InvocationTargetException,
            SerializationException, ClassNotFoundException {

        if (customSerializer != null) {
            deserializeWithCustomFieldDeserializer(customSerializer, instanceClass, instance);
        } else if (instanceClass.isArray()) {
            instance = deserializeArray(instanceClass, instance);
        } else if (instanceClass.isEnum()) {
            // Enums are deserialized when they are instantiated
        } else if (Exception.class.isAssignableFrom(instanceClass)) {
            // Exceptions are deserialized when they are instantiated
        } else {
            deserializeClass(instanceClass, instance);
        }

        return instance;
    }

    public Object deserializeValue(Class type) throws SerializationException {
        ValueReader valueReader = CLASS_TO_VALUE_READER.get(type);
        // Arrays of primitive or reference types need to go through readObject.
        return Objects.requireNonNullElse(valueReader, ValueReader.OBJECT).readValue(this);
    }

    private void deserializeWithCustomFieldDeserializer(
            Class customSerializer, Class instanceClass, Object instance)
            throws NoSuchMethodException, IllegalAccessException,
            InvocationTargetException {
        assert !instanceClass.isArray();

        for (Method method : customSerializer.getMethods()) {
            if ("deserialize".equals(method.getName())) {
                method.invoke(null, this, instance);
                return;
            }
        }
        throw new NoSuchMethodException("deserialize");
    }

    @Override
    protected String getString(int index) {
        if (index == 0) {
            return null;
        }
        // index is 1-based
        assert index > 0;
        assert index <= this.stringTable.size();

        // index is 1-based
        return this.stringTable.get(index - 1);
    }

    private byte hex2byte(char ch) {
        if (ch >= '0' && ch <= '9') {
            return (byte) (ch - '0');
        }
        if (ch >= 'A' && ch <= 'F') {
            return (byte) (ch - 'A' + 10);
        }
        if (ch >= 'a' && ch <= 'f') {
            return (byte) (ch - 'a' + 10);
        }

        return -1;
    }

    private Object instantiate(Class customSerializer, Class instanceClass)
            throws InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException,
            NoSuchMethodException {
        if (customSerializer != null) {
            for (Method method : customSerializer.getMethods()) {
                if ("instantiate".equals(method.getName())) {
                    return method.invoke(null, this);
                }
            }
            // Ok to not have one.
        }

        if (instanceClass.isArray()) {
            int length = readInt();
            // We don't pre-allocate the array; this prevents an allocation
            // attack
            return new BoundedList<>(instanceClass.getComponentType(), length);
        } else if (instanceClass.isEnum()) {
            Enum[] enumConstants = (Enum[]) instanceClass.getEnumConstants();
            int ordinal = readInt();
            assert ordinal >= 0 && ordinal < enumConstants.length;
            return enumConstants[ordinal];
        } else if (Exception.class.isAssignableFrom(instanceClass)) {
            // See SerializabilityUtil.fieldQualifiesForSerialization()
            String message = readString();
            return new Exception(message);
        } else {
            Constructor constructor = instanceClass.getDeclaredConstructor();
            constructor.setAccessible(true);
            return constructor.newInstance();
        }
    }

    /**
     * Parse response from GWT RPC example:
     * [3,23456,0,2,0,0,0,1,1,["dab.rpp.client.Person/1455343364"
     * ,"My dad name","GWT User"],0,5]
     */
    private void parse(String encoded) {
        // encoded = encoded.substring(1, encoded.length()-1);
        if (encoded.endsWith("]")) {
            encoded = encoded.substring(1, encoded.length() - 1);
        } else {
            encoded = encoded.substring(1);
        }
        StringBuilder token = new StringBuilder();
        for (int i = 0; i < encoded.length(); i++) {
            char ch = encoded.charAt(i);
            if (ch == ',') {
                this.results.add(token.toString());
                token = new StringBuilder();
                continue;
            }
            if (ch == '[') {
                int pos = encoded.lastIndexOf(']');
                if (pos < 0) {
                    // TODO: throw exeption
                    throw new RuntimeException("Unhandled mismatch in encoded response: " + encoded);
                }
                this.results.add(encoded.substring(i + 1, pos));
                i = pos + 1;
                continue;
            }
            token.append(ch);
        }
        if (!token.isEmpty()) {
            this.results.add(token.toString());
        }
    }

    @Override
    public void prepareToRead(String encoded) throws SerializationException {
        encoded = deconcat(encoded);
        parse(encoded);
        this.index = this.results.size();
        super.prepareToRead(encoded);

        if (getVersion() < SERIALIZATION_STREAM_MIN_VERSION
            || getVersion() > SERIALIZATION_STREAM_VERSION) {
            throw new IncompatibleRemoteServiceException(
                    "Expecting version between "
                    + SERIALIZATION_STREAM_MIN_VERSION + " and "
                    + SERIALIZATION_STREAM_VERSION
                    + " from server, got " + getVersion() + ".");
        }

        buildStringTable();
    }

    @Override
    public boolean readBoolean() {
        return !this.results.get(--this.index).equals("0");
    }

    @Override
    public byte readByte() {
        return Byte.parseByte(this.results.get(--this.index));
    }

    @Override
    public char readChar() {
        return (char) Integer.parseInt(this.results.get(--this.index));
    }

    @Override
    public double readDouble() {
        return Double.parseDouble(this.results.get(--this.index));
    }

    @Override
    public float readFloat() {
        return Float.parseFloat(this.results.get(--this.index));
    }

    @Override
    public int readInt() {
        try {
            int val = Integer.parseInt(this.results.get(--this.index));
            return val;
        } catch (NumberFormatException nfe) {
            return 0;
        }
    }

    @Override
    public long readLong() {
        if (getVersion() == SERIALIZATION_STREAM_MIN_VERSION) {
            return (long) readDouble() + (long) readDouble();
        } else {
            String s = this.results.get(--this.index);
            // remove quotes
            if (s.length() > 1) {
                s = s.substring(1, s.length() - 1);
            }
            return Utils.longFromBase64(s);
        }
    }

    @Override
    public short readShort() {
        return Short.parseShort(this.results.get(--this.index));
    }

    @Override
    public String readString() {
        return getString(readInt());
    }
}