]> source.dussan.org Git - vaadin-framework.git/commitdiff
Make data providers statefull with regards to default sort orders (#8247) pr8264/r2
authorLeif Åstrand <legioth@gmail.com>
Tue, 17 Jan 2017 14:22:55 +0000 (16:22 +0200)
committerDenis <denis@vaadin.com>
Tue, 17 Jan 2017 14:22:55 +0000 (16:22 +0200)
* Make data providers statefull with regards to default sort orders

This is one of many steps towards #8245

server/src/main/java/com/vaadin/data/provider/BackEndDataProvider.java
server/src/main/java/com/vaadin/data/provider/ListDataProvider.java
server/src/test/java/com/vaadin/data/provider/BackendDataProviderTest.java
server/src/test/java/com/vaadin/data/provider/DataProviderTestBase.java
server/src/test/java/com/vaadin/data/provider/ListDataProviderTest.java
server/src/test/java/com/vaadin/data/provider/StrBean.java

index 5f789e8a5591603db6d8216db491263236ce04c2..3606a99ade0fa28c68e32b525da11384740180a3 100644 (file)
 package com.vaadin.data.provider;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import com.vaadin.server.SerializableFunction;
 import com.vaadin.server.SerializableToIntFunction;
-import com.vaadin.shared.Registration;
 
 /**
  * A {@link DataProvider} for any back end.
@@ -34,6 +36,8 @@ import com.vaadin.shared.Registration;
  */
 public class BackEndDataProvider<T, F> extends AbstractDataProvider<T, F> {
 
+    private List<SortOrder<String>> sortOrders = new ArrayList<>();
+
     private final SerializableFunction<Query<T, F>, Stream<T>> request;
     private final SerializableToIntFunction<Query<T, F>> sizeCallback;
 
@@ -57,46 +61,71 @@ public class BackEndDataProvider<T, F> extends AbstractDataProvider<T, F> {
 
     @Override
     public Stream<T> fetch(Query<T, F> query) {
-        return request.apply(query);
+        return request.apply(mixInSortOrders(query));
     }
 
     @Override
     public int size(Query<T, F> query) {
-        return sizeCallback.applyAsInt(query);
+        return sizeCallback.applyAsInt(mixInSortOrders(query));
+    }
+
+    private Query<T, F> mixInSortOrders(Query<T, F> query) {
+        Set<String> sortedPropertyNames = query.getSortOrders().stream()
+                .map(SortOrder::getSorted).collect(Collectors.toSet());
+
+        List<SortOrder<String>> combinedSortOrders = Stream
+                .concat(query.getSortOrders().stream(),
+                        sortOrders.stream()
+                                .filter(order -> !sortedPropertyNames
+                                        .contains(order.getSorted())))
+                .collect(Collectors.toList());
+
+        return new Query<>(query.getOffset(), query.getLimit(),
+                combinedSortOrders, query.getInMemorySorting(),
+                query.getFilter().orElse(null));
     }
 
     /**
-     * Sets a default sorting order to the data provider.
+     * Sets a list of sort orders to use as the default sorting for this data
+     * provider. This overrides the sorting set by any other method that
+     * manipulates the default sorting of this data provider.
+     * <p>
+     * The default sorting is used if the query defines no sorting. The default
+     * sorting is also used to determine the ordering of items that are
+     * considered equal by the sorting defined in the query.
+     *
+     * @see #setSortOrder(SortOrder)
      *
      * @param sortOrders
-     *            a list of sorting information containing field ids and
-     *            directions
-     * @return new data provider with modified sorting
+     *            a list of sort orders to set, not <code>null</code>
      */
-    @SuppressWarnings("serial")
-    public BackEndDataProvider<T, F> sortingBy(
-            List<SortOrder<String>> sortOrders) {
-        BackEndDataProvider<T, F> parent = this;
-        return new BackEndDataProvider<T, F>(query -> {
-            List<SortOrder<String>> queryOrder = new ArrayList<>(
-                    query.getSortOrders());
-            queryOrder.addAll(sortOrders);
-            return parent.fetch(new Query<>(query.getOffset(), query.getLimit(),
-                    queryOrder, query.getInMemorySorting(),
-                    query.getFilter().orElse(null)));
-        }, sizeCallback) {
-
-            @Override
-            public Registration addDataProviderListener(
-                    DataProviderListener listener) {
-                return parent.addDataProviderListener(listener);
-            }
+    public void setSortOrders(List<SortOrder<String>> sortOrders) {
+        this.sortOrders = Objects.requireNonNull(sortOrders,
+                "Sort orders cannot be null");
+        refreshAll();
+    }
 
-            @Override
-            public void refreshAll() {
-                parent.refreshAll();
-            }
-        };
+    /**
+     * Sets a single sort order to use as the default sorting for this data
+     * provider. This overrides the sorting set by any other method that
+     * manipulates the default sorting of this data provider.
+     * <p>
+     * The default sorting is used if the query defines no sorting. The default
+     * sorting is also used to determine the ordering of items that are
+     * considered equal by the sorting defined in the query.
+     *
+     * @see #setSortOrders(List)
+     *
+     * @param sortOrder
+     *            a sort order to set, or <code>null</code> to clear any
+     *            previously set sort orders
+     */
+    public void setSortOrder(SortOrder<String> sortOrder) {
+        if (sortOrder == null) {
+            setSortOrders(Collections.emptyList());
+        } else {
+            setSortOrders(Collections.singletonList(sortOrder));
+        }
     }
 
     @Override
index c6860c26cc86b7a85050860e2bcb95fef14ceb8a..e1c8e8867c148df1f05cc0b75340c04f21668ab3 100644 (file)
@@ -19,11 +19,12 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.function.Function;
 import java.util.stream.Stream;
 
+import com.vaadin.data.ValueProvider;
+import com.vaadin.server.SerializableComparator;
 import com.vaadin.server.SerializablePredicate;
-import com.vaadin.shared.Registration;
+import com.vaadin.shared.data.sort.SortDirection;
 
 /**
  * {@link DataProvider} wrapper for {@link Collection}s. This class does not
@@ -36,7 +37,7 @@ public class ListDataProvider<T>
         extends AbstractDataProvider<T, SerializablePredicate<T>>
         implements AppendableFilterDataProvider<T, SerializablePredicate<T>> {
 
-    private Comparator<T> sortOrder = null;
+    private SerializableComparator<T> sortOrder = null;
     private final Collection<T> backend;
 
     /**
@@ -55,26 +56,6 @@ public class ListDataProvider<T>
         sortOrder = null;
     }
 
-    /**
-     * Chaining constructor for making modified {@link ListDataProvider}s. This
-     * Constructor is used internally for making sorted and filtered variants of
-     * a base data provider with actual data.
-     * <p>
-     * No protective copy is made of the list, and changes in the provided
-     * backing Collection will be visible via this data provider. The caller
-     * should copy the list if necessary.
-     *
-     * @param items
-     *            the backend data from the original list data provider
-     * @param sortOrder
-     *            a {@link Comparator} providing the needed sorting order
-     *
-     */
-    protected ListDataProvider(Collection<T> items, Comparator<T> sortOrder) {
-        this(items);
-        this.sortOrder = sortOrder;
-    }
-
     @Override
     public Stream<T> fetch(Query<T, SerializablePredicate<T>> query) {
         Stream<T> stream = backend.stream()
@@ -92,64 +73,140 @@ public class ListDataProvider<T>
         return stream.skip(query.getOffset()).limit(query.getLimit());
     }
 
+    @Override
+    public boolean isInMemory() {
+        return true;
+    }
+
+    @Override
+    public int size(Query<T, SerializablePredicate<T>> query) {
+        return (int) backend.stream()
+                .filter(t -> query.getFilter().orElse(p -> true).test(t))
+                .count();
+    }
+
     /**
-     * Creates a new list data provider based on this list data provider with
-     * the given sort order.
+     * Sets the comparator to use as the default sorting for this data provider.
+     * This overrides the sorting set by any other method that manipulates the
+     * default sorting of this data provider.
      * <p>
-     * <b>NOTE</b>: this data provider is not modified in any way.
+     * The default sorting is used if the query defines no sorting. The default
+     * sorting is also used to determine the ordering of items that are
+     * considered equal by the sorting defined in the query.
+     *
+     * @see #setSortOrder(ValueProvider, SortDirection)
+     * @see #addSortComparator(SerializableComparator)
      *
      * @param sortOrder
-     *            a {@link Comparator} providing the needed sorting order
-     * @return new data provider with modified sorting
+     *            a comparator to use, or <code>null</code> to clear any
+     *            previously set sort order
      */
-    @SuppressWarnings("serial")
-    public ListDataProvider<T> sortingBy(Comparator<T> sortOrder) {
-        ListDataProvider<T> parent = this;
-        return new ListDataProvider<T>(backend, sortOrder) {
-
-            @Override
-            public Registration addDataProviderListener(
-                    DataProviderListener listener) {
-                return parent.addDataProviderListener(listener);
-            }
-
-            @Override
-            public void refreshAll() {
-                parent.refreshAll();
-            }
-        };
+    public void setSortComparator(SerializableComparator<T> sortOrder) {
+        this.sortOrder = sortOrder;
+        refreshAll();
     }
 
     /**
-     * Creates a new list data provider based on this list data provider with
-     * the given sort order.
+     * Sets the property and direction to use as the default sorting for this
+     * data provider. This overrides the sorting set by any other method that
+     * manipulates the default sorting of this data provider.
      * <p>
-     * <b>NOTE</b>: this data provider is not modified in any way.
+     * The default sorting is used if the query defines no sorting. The default
+     * sorting is also used to determine the ordering of items that are
+     * considered equal by the sorting defined in the query.
+     *
+     * @see #setSortComparator(SerializableComparator)
+     * @see #addSortOrder(ValueProvider, SortDirection)
+     *
+     * @param valueProvider
+     *            the value provider that defines the property do sort by, not
+     *            <code>null</code>
+     * @param sortDirection
+     *            the sort direction to use, not <code>null</code>
+     */
+    public <V extends Comparable<? super V>> void setSortOrder(
+            ValueProvider<T, V> valueProvider, SortDirection sortDirection) {
+        setSortComparator(propertyComparator(valueProvider, sortDirection));
+    }
+
+    /**
+     * Adds a comparator to the default sorting for this data provider. If no
+     * default sorting has been defined, then the provided comparator will be
+     * used as the default sorting. If a default sorting has been defined, then
+     * the provided comparator will be used to determine the ordering of items
+     * that are considered equal by the previously defined default sorting.
      * <p>
-     * This method is a short-hand for
-     * {@code sortingBy(Comparator.comparing(sortOrder))}.
+     * The default sorting is used if the query defines no sorting. The default
+     * sorting is also used to determine the ordering of items that are
+     * considered equal by the sorting defined in the query.
+     *
+     * @see #setSortComparator(SerializableComparator)
+     * @see #addSortOrder(ValueProvider, SortDirection)
      *
      * @param sortOrder
-     *            function to sort by, not {@code null}
-     * @param <U>
-     *            the type of the Comparable sort key
-     * @return new data provider with modified sorting
+     *            a comparator to add, not <code>null</code>
      */
-    public <U extends Comparable<? super U>> ListDataProvider<T> sortingBy(
-            Function<T, U> sortOrder) {
-        return sortingBy(Comparator.comparing(sortOrder));
+    public void addSortComparator(SerializableComparator<T> sortOrder) {
+        Objects.requireNonNull(sortOrder, "Sort order to add cannot be null");
+
+        SerializableComparator<T> originalComparator = this.sortOrder;
+        if (originalComparator == null) {
+            setSortComparator(sortOrder);
+        } else {
+            setSortComparator((a, b) -> {
+                int result = originalComparator.compare(a, b);
+                if (result == 0) {
+                    result = sortOrder.compare(a, b);
+                }
+                return result;
+            });
+        }
     }
 
-    @Override
-    public boolean isInMemory() {
-        return true;
+    /**
+     * Adds a property and direction to the default sorting for this data
+     * provider. If no default sorting has been defined, then the provided sort
+     * order will be used as the default sorting. If a default sorting has been
+     * defined, then the provided sort order will be used to determine the
+     * ordering of items that are considered equal by the previously defined
+     * default sorting.
+     * <p>
+     * The default sorting is used if the query defines no sorting. The default
+     * sorting is also used to determine the ordering of items that are
+     * considered equal by the sorting defined in the query.
+     *
+     * @see #setSortOrder(ValueProvider, SortDirection)
+     * @see #addSortComparator(SerializableComparator)
+     *
+     * @param valueProvider
+     *            the value provider that defines the property do sort by, not
+     *            <code>null</code>
+     * @param sortDirection
+     *            the sort direction to use, not <code>null</code>
+     */
+    public <V extends Comparable<? super V>> void addSortOrder(
+            ValueProvider<T, V> valueProvider, SortDirection sortDirection) {
+        addSortComparator(propertyComparator(valueProvider, sortDirection));
     }
 
-    @Override
-    public int size(Query<T, SerializablePredicate<T>> query) {
-        return (int) backend.stream()
-                .filter(t -> query.getFilter().orElse(p -> true).test(t))
-                .count();
+    private static <V extends Comparable<? super V>, T> SerializableComparator<T> propertyComparator(
+            ValueProvider<T, V> valueProvider, SortDirection sortDirection) {
+        Objects.requireNonNull(valueProvider, "Value provider cannot be null");
+        Objects.requireNonNull(sortDirection, "Sort direction cannot be null");
+
+        Comparator<V> comparator = getNaturalSortComparator(sortDirection);
+
+        return (a, b) -> comparator.compare(valueProvider.apply(a),
+                valueProvider.apply(b));
+    }
+
+    private static <V extends Comparable<? super V>> Comparator<V> getNaturalSortComparator(
+            SortDirection sortDirection) {
+        Comparator<V> comparator = Comparator.naturalOrder();
+        if (sortDirection == SortDirection.DESCENDING) {
+            comparator = comparator.reversed();
+        }
+        return comparator;
     }
 
     @Override
index 585abc230a7f10b8268ee8bec84e81fde7682c4c..55048611655e2573d11c28d597ddaa540a44f9f5 100644 (file)
@@ -53,8 +53,8 @@ public class BackendDataProviderTest extends
     }
 
     @Override
-    protected BackEndDataProvider<StrBean, SerializablePredicate<StrBean>> sortingBy(
-            List<SortOrder<String>> sortOrder, Comparator<StrBean> comp) {
-        return getDataProvider().sortingBy(sortOrder);
+    protected void setSortOrder(List<SortOrder<String>> sortOrder,
+            Comparator<StrBean> comp) {
+        getDataProvider().setSortOrders(sortOrder);
     }
 }
index da909af29a71032698ac123a630f0abaa36a5026..ccbb0b0c1a1db2f4e32295cbe801fe349159a3b9 100644 (file)
@@ -49,7 +49,7 @@ public abstract class DataProviderTestBase<D extends DataProvider<StrBean, Seria
         return dataProvider;
     }
 
-    protected abstract D sortingBy(List<SortOrder<String>> sortOrder,
+    protected abstract void setSortOrder(List<SortOrder<String>> sortOrder,
             Comparator<StrBean> comp);
 
     private Query<StrBean, SerializablePredicate<StrBean>> createQuery(
@@ -110,11 +110,12 @@ public abstract class DataProviderTestBase<D extends DataProvider<StrBean, Seria
     public void testDefaultSortWithSpecifiedPostSort() {
         Comparator<StrBean> comp = Comparator.comparing(StrBean::getValue)
                 .thenComparing(Comparator.comparing(StrBean::getId).reversed());
-        List<StrBean> list = sortingBy(
-                Sort.asc("value").thenDesc("id").build(), comp)
-                        .fetch(createQuery(Sort.asc("randomNumber").build(),
-                                Comparator.comparing(StrBean::getRandomNumber)))
-                        .collect(Collectors.toList());
+        setSortOrder(Sort.asc("value").thenDesc("id").build(), comp);
+
+        List<StrBean> list = dataProvider
+                .fetch(createQuery(Sort.asc("randomNumber").build(),
+                        Comparator.comparing(StrBean::getRandomNumber)))
+                .collect(Collectors.toList());
 
         Assert.assertEquals("Sorted data and original data sizes don't match",
                 data.size(), list.size());
@@ -141,9 +142,11 @@ public abstract class DataProviderTestBase<D extends DataProvider<StrBean, Seria
 
     @Test
     public void testDefaultSortWithFunction() {
-        List<StrBean> list = sortingBy(Sort.asc("value").build(),
-                Comparator.comparing(StrBean::getValue)).fetch(new Query<>())
-                        .collect(Collectors.toList());
+        setSortOrder(Sort.asc("value").build(),
+                Comparator.comparing(StrBean::getValue));
+
+        List<StrBean> list = dataProvider.fetch(new Query<>())
+                .collect(Collectors.toList());
 
         Assert.assertEquals("Sorted data and original data sizes don't match",
                 data.size(), list.size());
@@ -157,112 +160,6 @@ public abstract class DataProviderTestBase<D extends DataProvider<StrBean, Seria
         }
     }
 
-    @Test
-    public void refreshAll_changeBeanInstance() {
-        StrBean bean = new StrBean("foo", -1, hashCode());
-        int size = dataProvider.size(new Query<>());
-
-        data.set(0, bean);
-        dataProvider.refreshAll();
-
-        List<StrBean> list = dataProvider.fetch(new Query<>())
-                .collect(Collectors.toList());
-        StrBean first = list.get(0);
-        Assert.assertEquals(bean.getValue(), first.getValue());
-        Assert.assertEquals(bean.getRandomNumber(), first.getRandomNumber());
-        Assert.assertEquals(bean.getId(), first.getId());
-
-        Assert.assertEquals(size, dataProvider.size(new Query<>()));
-    }
-
-    @Test
-    public void refreshAll_updateBean() {
-        int size = dataProvider.size(new Query<>());
-
-        StrBean bean = data.get(0);
-        bean.setValue("foo");
-        dataProvider.refreshAll();
-
-        List<StrBean> list = dataProvider.fetch(new Query<>())
-                .collect(Collectors.toList());
-        StrBean first = list.get(0);
-        Assert.assertEquals("foo", first.getValue());
-
-        Assert.assertEquals(size, dataProvider.size(new Query<>()));
-    }
-
-    @Test
-    public void refreshAll_sortingBy_changeBeanInstance() {
-        StrBean bean = new StrBean("foo", -1, hashCode());
-        int size = dataProvider.size(new Query<>());
-
-        data.set(0, bean);
-
-        D dSource = sortingBy(Sort.asc("id").build(),
-                Comparator.comparing(StrBean::getId));
-        dSource.refreshAll();
-
-        List<StrBean> list = dSource.fetch(new Query<>())
-                .collect(Collectors.toList());
-        StrBean first = list.get(0);
-        Assert.assertEquals(bean.getValue(), first.getValue());
-        Assert.assertEquals(bean.getRandomNumber(), first.getRandomNumber());
-        Assert.assertEquals(bean.getId(), first.getId());
-
-        Assert.assertEquals(size, dataProvider.size(new Query<>()));
-    }
-
-    @Test
-    public void refreshAll_addBeanInstance() {
-        StrBean bean = new StrBean("foo", -1, hashCode());
-
-        int size = dataProvider.size(new Query<>());
-
-        data.add(0, bean);
-        dataProvider.refreshAll();
-
-        List<StrBean> list = dataProvider.fetch(new Query<>())
-                .collect(Collectors.toList());
-        StrBean first = list.get(0);
-        Assert.assertEquals(bean.getValue(), first.getValue());
-        Assert.assertEquals(bean.getRandomNumber(), first.getRandomNumber());
-        Assert.assertEquals(bean.getId(), first.getId());
-
-        Assert.assertEquals(size + 1, dataProvider.size(new Query<>()));
-    }
-
-    @Test
-    public void refreshAll_removeBeanInstance() {
-        int size = dataProvider.size(new Query<>());
-
-        data.remove(0);
-        dataProvider.refreshAll();
-
-        Assert.assertEquals(size - 1, dataProvider.size(new Query<>()));
-    }
-
-    @Test
-    public void refreshAll_fromParentToSortedBy() {
-        D sortedDataProvider = sortingBy(Sort.asc("randomNumber").build(),
-                Comparator.comparing(StrBean::getRandomNumber));
-
-        CountingListener listener = new CountingListener();
-        sortedDataProvider.addDataProviderListener(listener);
-
-        Assert.assertEquals("Listener was not called prematurely", 0,
-                listener.getCounter());
-
-        dataProvider.refreshAll();
-
-        Assert.assertEquals("Listener was not called correctly", 1,
-                listener.getCounter());
-
-        sortedDataProvider.refreshAll();
-
-        Assert.assertEquals("Listener was not called correctly", 2,
-                listener.getCounter());
-    }
-
     @Test
     public void filteringListDataProvider_convertFilter() {
         DataProvider<StrBean, String> strFilterDataProvider = dataProvider
index 148b5f98f860eb2360c5e3624411442e2aebcfab..ad7c3b1f466ccbd95eedfa4cde472bbcb1f98090 100644 (file)
@@ -2,11 +2,14 @@ package com.vaadin.data.provider;
 
 import java.util.Comparator;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.junit.Assert;
 import org.junit.Test;
 
+import com.vaadin.server.SerializableComparator;
 import com.vaadin.server.SerializablePredicate;
+import com.vaadin.shared.data.sort.SortDirection;
 
 public class ListDataProviderTest
         extends DataProviderTestBase<ListDataProvider<StrBean>> {
@@ -75,10 +78,53 @@ public class ListDataProviderTest
                         .size(new Query<>("Zyx")));
     }
 
+    @Test
+    public void setSortByProperty_ascending() {
+        ListDataProvider<StrBean> dataProvider = getDataProvider();
+
+        dataProvider.setSortOrder(StrBean::getId, SortDirection.ASCENDING);
+
+        int[] threeFirstIds = dataProvider.fetch(new Query<>())
+                .mapToInt(StrBean::getId).limit(3).toArray();
+
+        Assert.assertArrayEquals(new int[] { 0, 1, 2 }, threeFirstIds);
+    }
+
+    @Test
+    public void setSortByProperty_descending() {
+        ListDataProvider<StrBean> dataProvider = getDataProvider();
+
+        dataProvider.setSortOrder(StrBean::getId, SortDirection.DESCENDING);
+
+        int[] threeFirstIds = dataProvider.fetch(new Query<>())
+                .mapToInt(StrBean::getId).limit(3).toArray();
+
+        Assert.assertArrayEquals(new int[] { 98, 97, 96 }, threeFirstIds);
+    }
+
+    @Test
+    public void testMultipleSortOrder_firstAddedWins() {
+        ListDataProvider<StrBean> dataProvider = getDataProvider();
+
+        dataProvider.addSortOrder(StrBean::getValue, SortDirection.DESCENDING);
+        dataProvider.addSortOrder(StrBean::getId, SortDirection.DESCENDING);
+
+        List<StrBean> threeFirstItems = dataProvider.fetch(new Query<>())
+                .limit(3).collect(Collectors.toList());
+
+        // First one is Xyz
+        Assert.assertEquals(new StrBean("Xyz", 10, 100),
+                threeFirstItems.get(0));
+        // The following are Foos ordered by id
+        Assert.assertEquals(new StrBean("Foo", 93, 2), threeFirstItems.get(1));
+        Assert.assertEquals(new StrBean("Foo", 91, 2), threeFirstItems.get(2));
+    }
+
     @Override
-    protected ListDataProvider<StrBean> sortingBy(
-            List<SortOrder<String>> sortOrder, Comparator<StrBean> comp) {
-        return getDataProvider().sortingBy(comp);
+    protected void setSortOrder(List<SortOrder<String>> sortOrder,
+            Comparator<StrBean> comp) {
+        SerializableComparator<StrBean> serializableComp = comp::compare;
+        getDataProvider().setSortComparator(serializableComp);
     }
 
 }
index 29c188b82daeec3ca7b5f4c00d5874acf50cb68f..b596ccdde569ceee0c0894360a55141547855af5 100644 (file)
@@ -3,6 +3,7 @@ package com.vaadin.data.provider;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Random;
 
 class StrBean implements Serializable {
@@ -46,6 +47,24 @@ class StrBean implements Serializable {
         return data;
     }
 
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof StrBean) {
+            StrBean that = (StrBean) obj;
+            return that.id == this.id && that.randomNumber == this.randomNumber
+                    && Objects.equals(this.value, that.value);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, randomNumber, value);
+    }
+
     @Override
     public String toString() {
         return "{ " + value + ", " + randomNumber + ", " + id + " }";