]> source.dussan.org Git - vaadin-framework.git/commitdiff
Fix temporal renderers serialization (#10929)
authorIlia Motornyi <elmot@vaadin.com>
Wed, 13 Jun 2018 06:38:45 +0000 (09:38 +0300)
committerTeemu Suo-Anttila <tsuoanttila@users.noreply.github.com>
Wed, 13 Jun 2018 06:38:45 +0000 (09:38 +0300)
server/src/main/java/com/vaadin/data/util/BeanUtil.java
server/src/main/java/com/vaadin/ui/renderers/LocalDateRenderer.java
server/src/main/java/com/vaadin/ui/renderers/LocalDateTimeRenderer.java
server/src/test/java/com/vaadin/data/provider/hierarchical/HierarchyMapperWithDataTest.java
server/src/test/java/com/vaadin/data/provider/hierarchical/Node.java
server/src/test/java/com/vaadin/tests/TestTemporalSerialization.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java

index ac59586d1bc3e40b762b7f9b1dd9590754094239..4600451b4eee16bff654bb9f6c79f141f0343189 100644 (file)
@@ -19,6 +19,8 @@ import java.beans.BeanInfo;
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
 import java.io.Serializable;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -226,6 +228,38 @@ public final class BeanUtil implements Serializable {
         }
     }
 
+    /**
+     * Checks if the object is serializable or not. To be used in assertion checks only,
+     * since the check might be a bit heavyweight.
+     *
+     * @param obj to be checked
+     * @return {@code true}
+     * @throws AssertionError if the object is not serializable
+     */
+    public static boolean checkSerialization(Object obj) {
+        try {
+            ObjectOutputStream dummyObjectOutputStream = new ObjectOutputStream(new OutputStream() {
+                @Override
+                public void write(int b) {
+                }
+
+                @SuppressWarnings("NullableProblems")
+                @Override
+                public void write(byte[] ignored) {
+                }
+
+                @SuppressWarnings("NullableProblems")
+                @Override
+                public void write(byte[] b, int off, int len) {
+                }
+            });
+            dummyObjectOutputStream.writeObject(obj);
+        } catch (Throwable e) {
+            throw new AssertionError("Formatter supplier should be serializable", e);
+        }
+        return true;
+    }
+
     private static class LazyValidationAvailability implements Serializable {
         private static final boolean BEAN_VALIDATION_AVAILABLE = isAvailable();
 
index 155867a695950ba23eba17f4e88c4119f28aabe7..ef962e7d8b2a697d0d6937eac9b6b7a722f60891 100644 (file)
@@ -20,6 +20,8 @@ import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
 import java.util.Locale;
 
+import com.vaadin.data.util.BeanUtil;
+import com.vaadin.server.SerializableSupplier;
 import com.vaadin.shared.ui.grid.renderers.LocalDateRendererState;
 
 import elemental.json.JsonValue;
@@ -32,7 +34,7 @@ import elemental.json.JsonValue;
  */
 public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
 
-    private DateTimeFormatter formatter;
+    private SerializableSupplier<DateTimeFormatter> formatterSupplier;
     private boolean getLocaleFromGrid;
 
     /**
@@ -47,7 +49,7 @@ public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
      *      FormatStyle.LONG</a>
      */
     public LocalDateRenderer() {
-        this(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG), "");
+        this(() -> DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG), "");
         getLocaleFromGrid = true;
     }
 
@@ -133,7 +135,8 @@ public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
             throw new IllegalArgumentException("locale may not be null");
         }
 
-        formatter = DateTimeFormatter.ofPattern(formatPattern, locale);
+        formatterSupplier = () -> DateTimeFormatter.ofPattern(formatPattern,
+                locale);
     }
 
     /**
@@ -142,21 +145,78 @@ public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
      * The renderer is configured to render with the given formatter, with an
      * empty string as its null representation.
      *
+     * <p>
+     * <b>Note</b> the {@code DateTimeFormatter} is not a serializable class, so
+     * using this method in an environment which requires session persistence
+     * may produce {@link java.io.NotSerializableException}.
+     *
      * @param formatter
      *            the formatter to use, not {@code null}
      *
      * @throws IllegalArgumentException
      *             if formatter is null
+     * @deprecated the method is unsafe for serialization, may produce troubles
+     *             in a cluster environment
+     * @see #LocalDateRenderer(SerializableSupplier)
      */
+    @Deprecated
     public LocalDateRenderer(DateTimeFormatter formatter) {
         this(formatter, "");
     }
 
+    /**
+     * Creates a new LocalDateRenderer.
+     * <p>
+     * The renderer is configured to render with the given formatterSupplier.
+     *
+     * @param formatterSupplier
+     *            the formatterSupplier supplier to use, not {@code null}, it
+     *            should not supply {@code null} either
+     * @param nullRepresentation
+     *            the textual representation of the {@code null} value
+     *
+     * @throws IllegalArgumentException
+     *             if formatterSupplier is null
+     */
+    public LocalDateRenderer(
+            SerializableSupplier<DateTimeFormatter> formatterSupplier,
+            String nullRepresentation) {
+        super(LocalDate.class, nullRepresentation);
+
+        if (formatterSupplier == null) {
+            throw new IllegalArgumentException(
+                    "formatterSupplier may not be null");
+        }
+        this.formatterSupplier = formatterSupplier;
+        assert BeanUtil.checkSerialization(formatterSupplier);
+    }
+
+    /**
+     * Creates a new LocalDateRenderer.
+     * <p>
+     * The renderer is configured to render with the given formatterSupplier.
+     *
+     * @param formatterSupplier
+     *            the formatterSupplier supplier to use, not {@code null}, it
+     *            should not supply {@code null} either
+     * @throws IllegalArgumentException
+     *             if formatterSupplier is null
+     */
+    public LocalDateRenderer(
+            SerializableSupplier<DateTimeFormatter> formatterSupplier) {
+        this(formatterSupplier, "");
+    }
+
     /**
      * Creates a new LocalDateRenderer.
      * <p>
      * The renderer is configured to render with the given formatter.
      *
+     * <p>
+     * <b>Note</b> the {@code DateTimeFormatter} is not a serializable class, so
+     * using this method in an environment which requires session persistence
+     * may produce {@link java.io.NotSerializableException}.
+     *
      * @param formatter
      *            the formatter to use, not {@code null}
      * @param nullRepresentation
@@ -164,7 +224,11 @@ public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
      *
      * @throws IllegalArgumentException
      *             if formatter is null
+     * @deprecated the method is unsafe for serialization, may produce troubles
+     *             in acluster environment
+     * @see #LocalDateRenderer(SerializableSupplier, String)
      */
+    @Deprecated
     public LocalDateRenderer(DateTimeFormatter formatter,
             String nullRepresentation) {
         super(LocalDate.class, nullRepresentation);
@@ -173,7 +237,7 @@ public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
             throw new IllegalArgumentException("formatter may not be null");
         }
 
-        this.formatter = formatter;
+        this.formatterSupplier = () -> formatter;
     }
 
     @Override
@@ -188,10 +252,10 @@ public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
                                 + "this renderer should either be attached to a grid "
                                 + "or constructed with locale information");
             }
-            dateString = value
-                    .format(formatter.withLocale(getParentGrid().getLocale()));
+            dateString = value.format(formatterSupplier.get()
+                    .withLocale(getParentGrid().getLocale()));
         } else {
-            dateString = value.format(formatter);
+            dateString = value.format(formatterSupplier.get());
         }
         return encode(dateString, String.class);
     }
@@ -205,4 +269,5 @@ public class LocalDateRenderer extends AbstractRenderer<Object, LocalDate> {
     protected LocalDateRendererState getState(boolean markAsDirty) {
         return (LocalDateRendererState) super.getState(markAsDirty);
     }
+
 }
index c0faae9dd9f9a47418601086447b83d3a5e82831..9dc88b5cbc1e8f4cb31c2d54f8dda958027ae7dd 100644 (file)
@@ -20,9 +20,11 @@ import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
 import java.util.Locale;
 
+import com.vaadin.server.SerializableSupplier;
 import com.vaadin.shared.ui.grid.renderers.LocalDateTimeRendererState;
 
 import elemental.json.JsonValue;
+import static com.vaadin.data.util.BeanUtil.checkSerialization;
 
 /**
  * A renderer for presenting {@code LocalDateTime} objects.
@@ -33,7 +35,7 @@ import elemental.json.JsonValue;
 public class LocalDateTimeRenderer
         extends AbstractRenderer<Object, LocalDateTime> {
 
-    private DateTimeFormatter formatter;
+    private SerializableSupplier<DateTimeFormatter> formatterSupplier;
     private boolean getLocaleFromGrid;
 
     /**
@@ -52,7 +54,7 @@ public class LocalDateTimeRenderer
      *      FormatStyle.SHORT</a>
      */
     public LocalDateTimeRenderer() {
-        this(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG,
+        this(() -> DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG,
                 FormatStyle.SHORT), "");
         getLocaleFromGrid = true;
     }
@@ -63,12 +65,21 @@ public class LocalDateTimeRenderer
      * The renderer is configured to render with the given formatter, with the
      * empty string as its null representation.
      *
+     * <p>
+     * <b>Note</b> the {@code DateTimeFormatter} is not a serializable class, so
+     * using this method in an environment which requires session persistence
+     * may produce {@link java.io.NotSerializableException}.
+     *
      * @param formatter
      *            the formatter to use, not {@code null}
      *
      * @throws IllegalArgumentException
      *             if formatter is null
+     * @deprecated the method is unsafe for serialization, may produce troubles
+     *             in a cluster environment
+     * @see #LocalDateTimeRenderer(SerializableSupplier)
      */
+    @Deprecated
     public LocalDateTimeRenderer(DateTimeFormatter formatter) {
         this(formatter, "");
     }
@@ -78,6 +89,12 @@ public class LocalDateTimeRenderer
      * <p>
      * The renderer is configured to render with the given formatter.
      *
+     * <p>
+     * <b>Note</b> the {@code DateTimeFormatter} is not a serializable class, so
+     * using this method in an environment which requires session persistence
+     * may produce {@link java.io.NotSerializableException}.
+     *
+     *
      * @param formatter
      *            the formatter to use, not {@code null}
      * @param nullRepresentation
@@ -85,7 +102,11 @@ public class LocalDateTimeRenderer
      *
      * @throws IllegalArgumentException
      *             if formatter is null
+     * @deprecated the method is unsafe for serialization, may produce troubles
+     *             in acluster environment
+     * @see #LocalDateTimeRenderer(SerializableSupplier, String)
      */
+    @Deprecated
     public LocalDateTimeRenderer(DateTimeFormatter formatter,
             String nullRepresentation) {
         super(LocalDateTime.class, nullRepresentation);
@@ -94,7 +115,7 @@ public class LocalDateTimeRenderer
             throw new IllegalArgumentException("formatter may not be null");
         }
 
-        this.formatter = formatter;
+        this.formatterSupplier = () -> formatter;
     }
 
     /**
@@ -179,7 +200,51 @@ public class LocalDateTimeRenderer
             throw new IllegalArgumentException("locale may not be null");
         }
 
-        formatter = DateTimeFormatter.ofPattern(formatPattern, locale);
+        formatterSupplier = () -> DateTimeFormatter.ofPattern(formatPattern,
+                locale);
+    }
+
+    /**
+     * Creates a new LocalDateTimeRenderer.
+     * <p>
+     * The renderer is configured to render with the given formatterSupplier.
+     *
+     * @param formatterSupplier
+     *            the formatterSupplier supplier to use, not {@code null}, it
+     *            should not supply {@code null} either
+     * @throws IllegalArgumentException
+     *             if formatterSupplier is null
+     */
+    public LocalDateTimeRenderer(
+            SerializableSupplier<DateTimeFormatter> formatterSupplier) {
+        this(formatterSupplier, "");
+    }
+
+    /**
+     * Creates a new LocalDateTimeRenderer.
+     * <p>
+     * The renderer is configured to render with the given formatterSupplier.
+     *
+     * @param formatterSupplier
+     *            the formatterSupplier supplier to use, not {@code null}, it
+     *            should not supply {@code null} either
+     * @param nullRepresentation
+     *            the textual representation of the {@code null} value
+     *
+     * @throws IllegalArgumentException
+     *             if formatterSupplier is null
+     */
+    public LocalDateTimeRenderer(
+            SerializableSupplier<DateTimeFormatter> formatterSupplier,
+            String nullRepresentation) {
+        super(LocalDateTime.class, nullRepresentation);
+
+        if (formatterSupplier == null) {
+            throw new IllegalArgumentException(
+                    "formatterSupplier may not be null");
+        }
+        this.formatterSupplier = formatterSupplier;
+        assert checkSerialization(formatterSupplier);
     }
 
     @Override
@@ -194,10 +259,10 @@ public class LocalDateTimeRenderer
                                 + "this renderer should either be attached to a grid "
                                 + "or constructed with locale information");
             }
-            dateString = value
-                    .format(formatter.withLocale(getParentGrid().getLocale()));
+            dateString = value.format(formatterSupplier.get()
+                    .withLocale(getParentGrid().getLocale()));
         } else {
-            dateString = value.format(formatter);
+            dateString = value.format(formatterSupplier.get());
         }
         return encode(dateString, String.class);
     }
index 81ee908a52bb22ca34f570005a1068fb9320bf0b..e056b99c21f74e4363dde9b2158dea964e288899 100644 (file)
@@ -227,15 +227,16 @@ public class HierarchyMapperWithDataTest {
 
     static List<Node> generateTestData(int rootCount, int parentCount,
             int leafCount) {
+        int nodeCounter = 0;
         List<Node> nodes = new ArrayList<>();
         for (int i = 0; i < rootCount; ++i) {
-            Node root = new Node();
+            Node root = new Node(nodeCounter++);
             nodes.add(root);
             for (int j = 0; j < parentCount; ++j) {
-                Node parent = new Node(root);
+                Node parent = new Node(root, nodeCounter++);
                 nodes.add(parent);
                 for (int k = 0; k < leafCount; ++k) {
-                    nodes.add(new Node(parent));
+                    nodes.add(new Node(parent, nodeCounter++));
                 }
             }
         }
index 95e961df52ea13e9ba3b12c7a74bb5dbfd4f7f63..1be5bfa0aaedc45cf6d1e345e87fa4ebfa05a2d6 100644 (file)
@@ -4,18 +4,16 @@ import java.io.Serializable;
 
 public class Node implements Serializable {
 
-    private static int counter = 0;
-
     private final Node parent;
     private final int number;
 
-    public Node() {
-        this(null);
+    public Node(int number) {
+        this(null, number);
     }
 
-    public Node(Node parent) {
+    public Node(Node parent, int number) {
         this.parent = parent;
-        this.number = counter++;
+        this.number = number;
     }
 
     public Node getParent() {
diff --git a/server/src/test/java/com/vaadin/tests/TestTemporalSerialization.java b/server/src/test/java/com/vaadin/tests/TestTemporalSerialization.java
new file mode 100644 (file)
index 0000000..51e4933
--- /dev/null
@@ -0,0 +1,155 @@
+package com.vaadin.tests;
+
+import javax.swing.text.DateFormatter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Locale;
+
+import org.apache.commons.lang.SerializationUtils;
+import org.junit.Test;
+
+import com.vaadin.server.SerializableSupplier;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.renderers.LocalDateRenderer;
+import com.vaadin.ui.renderers.LocalDateTimeRenderer;
+
+import static java.lang.reflect.Modifier.isPublic;
+import static java.lang.reflect.Modifier.isStatic;
+import static junit.framework.TestCase.assertNotNull;
+
+public class TestTemporalSerialization {
+
+    @Test
+    public void smokeTestRendererSerialization()
+            throws IOException, ClassNotFoundException {
+        Grid<Object> grid = new Grid<>();
+        grid.addColumn(
+                o -> new Date(o.hashCode()).toInstant()
+                        .atZone(ZoneId.systemDefault()).toLocalDate(),
+                new LocalDateRenderer());
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        ObjectOutputStream objectOutputStream = new ObjectOutputStream(
+                outputStream);
+        objectOutputStream.writeObject(grid);
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(
+                outputStream.toByteArray());
+        Grid readGrid = (Grid) new ObjectInputStream(inputStream).readObject();
+        assertNotNull(readGrid.getColumns().get(0));
+    }
+
+    @Test
+    public void testLocalDateRenderer() throws IllegalAccessException,
+            InstantiationException, InvocationTargetException {
+        testSerialization(LocalDateRenderer.class);
+    }
+
+    @Test
+    public void testLocalDateTimeRenderer() throws IllegalAccessException,
+            InstantiationException, InvocationTargetException {
+        testSerialization(LocalDateTimeRenderer.class);
+    }
+
+    @Test(expected = AssertionError.class)
+    public void testAssertionFail() {
+        new LocalDateRenderer(new NonSerializableThing());
+    }
+
+    private static class NonSerializableThing
+            implements SerializableSupplier<DateTimeFormatter> {
+        public NonSerializableThing() {
+        }
+
+        private DateTimeFormatter useless = DateTimeFormatter.ofPattern("Y");
+
+        @Override
+        public DateTimeFormatter get() {
+            return useless;
+        }
+    }
+
+    private void testSerialization(Class<?> rendererClass)
+            throws IllegalAccessException, InvocationTargetException,
+            InstantiationException {
+        for (Constructor<?> constructor : rendererClass.getConstructors()) {
+            if (!isPublic(constructor.getModifiers()))
+                continue;
+            Object[] params = simulateParams(constructor);
+            if (params != null) {
+                Object o = constructor.newInstance(params);
+                checkSerialization(constructor, o);
+            }
+        }
+        for (Method method : rendererClass.getMethods()) {
+            if (!isPublic(method.getModifiers())
+                    || !isStatic(method.getModifiers())) {
+                continue;
+            }
+            if (!method.getReturnType().isAssignableFrom(rendererClass)) {
+                continue;
+            }
+            Object[] params = simulateParams(method);
+            if (params != null) {
+                Object o = method.invoke(simulateParams(method));
+                checkSerialization(method, o);
+            }
+        }
+    }
+
+    private Object[] simulateParams(Executable executable) {
+        Parameter[] parameters = executable.getParameters();
+        Object[] args = new Object[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            Parameter parameter = parameters[i];
+            Class<?> type = parameter.getType();
+            if (type.isAssignableFrom(String.class)) {
+                args[i] = "";
+            } else if (type.isAssignableFrom(Locale.class)) {
+                args[i] = Locale.US;
+            } else if (type.isAssignableFrom(SerializableSupplier.class)) {
+                Type genericType = ((ParameterizedType) parameter
+                        .getParameterizedType()).getActualTypeArguments()[0];
+                args[i] = (SerializableSupplier) () -> {
+                    try {
+                        return ((Class) genericType).newInstance();
+                    } catch (Exception e) {
+                        throw new AssertionError(e);
+                    }
+                };
+            } else if (type.isAssignableFrom(DateFormatter.class)
+                    || type.isAssignableFrom(DateTimeFormatter.class)) {
+                assertNotNull(
+                        "Non-deprecated code has non-serializable parameter: "
+                                + executable.toGenericString(),
+                        executable.getAnnotation(Deprecated.class));
+                return null;
+            } else {
+                throw new IllegalArgumentException(
+                        "Unsupported parameter type: " + type.getName());
+            }
+        }
+        return args;
+    }
+
+    private void checkSerialization(Executable method, Object o) {
+        try {
+            byte[] serialize = SerializationUtils.serialize((Serializable) o);
+            SerializationUtils.deserialize(serialize);
+        } catch (Throwable e) {
+            throw new AssertionError(method.toGenericString(), e);
+        }
+    }
+}
index 819f0530c106cfb3a5a821cb85ac4dd302c75e6b..4bc0dd2729aaa260d7297e1c77be18f94cab337f 100644 (file)
@@ -69,8 +69,9 @@ public class ClassesSerializableTest {
             "com\\.vaadin\\.server\\.communication\\.PushHandler.*", // PushHandler
             "com\\.vaadin\\.server\\.communication\\.DateSerializer", //
             "com\\.vaadin\\.server\\.communication\\.JSONSerializer", //
+            "com\\.vaadin\\.ui\\.declarative\\.DesignContext", //
             // and its inner classes do not need to be serializable
-            "com\\.vaadin\\.util\\.SerializerHelper", // fully static
+            "com\\.vaadin\\.v7\\.util\\.SerializerHelper", // fully static
             // class level filtering, also affecting nested classes and
             // interfaces
             "com\\.vaadin\\.server\\.LegacyCommunicationManager.*", //
@@ -81,10 +82,11 @@ public class ClassesSerializableTest {
             "com\\.vaadin\\.data\\.provider\\.HierarchyMapper\\$TreeLevelQuery",
             "com\\.vaadin\\.data\\.util\\.ReflectTools.*", //
             "com\\.vaadin\\.data\\.util\\.JsonUtil.*", //
-            "com\\.vaadin\\.data\\.util.BeanItemContainerGenerator.*",
-            "com\\.vaadin\\.data\\.util\\.sqlcontainer\\.connection\\.MockInitialContextFactory",
-            "com\\.vaadin\\.data\\.util\\.sqlcontainer\\.DataGenerator",
-            "com\\.vaadin\\.data\\.util\\.sqlcontainer\\.FreeformQueryUtil",
+            "com\\.vaadin\\.v7\\.data\\.util.BeanItemContainerGenerator.*",
+            "com\\.vaadin\\.v7\\.data\\.util\\.sqlcontainer\\.connection\\.MockInitialContextFactory",
+            "com\\.vaadin\\.v7\\.data\\.util\\.sqlcontainer\\.DataGenerator",
+            "com\\.vaadin\\.v7\\.data\\.util\\.sqlcontainer\\.FreeformQueryUtil",
+            "com\\.vaadin\\.data\\.util\\.BeanUtil.*",
             // the JSR-303 constraint interpolation context
             "com\\.vaadin\\.data\\.validator\\.BeanValidator\\$1", //
             "com\\.vaadin\\.sass.*", //
@@ -142,7 +144,7 @@ public class ClassesSerializableTest {
                 continue;
             }
 
-            if (Component.class.isAssignableFrom(cls) && !cls.isInterface()
+            if (!cls.isInterface()
                     && !Modifier.isAbstract(cls.getModifiers())) {
                 serializeAndDeserialize(cls);
             }