From 61c1c091897c7c0d8aaec837f643fe5a5d644201 Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Thu, 10 Mar 2011 14:19:20 +0000 Subject: [PATCH] #6286 Container filtering improvements: initial version of new filtering API (with simple string filtering implementation only) svn changeset:17707/svn branch:6.6 --- src/com/vaadin/data/Container.java | 129 ++++++++++++++++- .../data/util/AbstractBeanContainer.java | 24 +++- .../data/util/AbstractInMemoryContainer.java | 93 ++++++------- .../vaadin/data/util/IndexedContainer.java | 22 ++- .../SimpleStringFilter.java} | 41 +++--- .../filter/UnsupportedFilterException.java | 32 +++++ .../container/filter/AbstractFilterTest.java | 49 +++++++ .../filter/SimpleStringFilterTest.java | 130 ++++++++++++++++++ 8 files changed, 441 insertions(+), 79 deletions(-) rename src/com/vaadin/data/util/{Filter.java => filter/SimpleStringFilter.java} (66%) create mode 100644 src/com/vaadin/data/util/filter/UnsupportedFilterException.java create mode 100644 tests/src/com/vaadin/tests/server/container/filter/AbstractFilterTest.java create mode 100644 tests/src/com/vaadin/tests/server/container/filter/SimpleStringFilterTest.java diff --git a/src/com/vaadin/data/Container.java b/src/com/vaadin/data/Container.java index 6dfa541c22..68219f2454 100644 --- a/src/com/vaadin/data/Container.java +++ b/src/com/vaadin/data/Container.java @@ -7,6 +7,8 @@ package com.vaadin.data; import java.io.Serializable; import java.util.Collection; +import com.vaadin.data.util.filter.UnsupportedFilterException; + /** *

* A specialized set of identified Items. Basically the Container is a set of @@ -34,7 +36,7 @@ import java.util.Collection; * *

* Note that though uniquely identified, the Items in a Container are not - * neccessarily {@link Container.Ordered ordered}or {@link Container.Indexed + * necessarily {@link Container.Ordered ordered} or {@link Container.Indexed * indexed}. *

* @@ -581,8 +583,8 @@ public interface Container extends Serializable { *

*

* How filtering is performed when a {@link Hierarchical} container - * implements {@link Filterable} is implementation specific and should be - * documented in the implementing class. + * implements {@link SimpleFilterable} is implementation specific and should + * be documented in the implementing class. *

*

* Adding items (if supported) to a filtered {@link Ordered} or @@ -593,8 +595,10 @@ public interface Container extends Serializable { *

* * @since 5.0 + * @deprecated use {@link Filterable} */ - public interface Filterable extends Container, Serializable { + @Deprecated + public interface SimpleFilterable extends Container, Serializable { /** * Add a filter for given property. @@ -611,17 +615,132 @@ public interface Container extends Serializable { * strings. * @param onlyMatchPrefix * Only match prefixes; no other matches are included. + * + * @deprecated use {@link Filterable#addContainerFilter(Filter)} */ + @Deprecated public void addContainerFilter(Object propertyId, String filterString, boolean ignoreCase, boolean onlyMatchPrefix); /** Remove all filters from all properties. */ public void removeAllContainerFilters(); - /** Remove all filters from given property. */ + /** + * Remove all filters from given property. + * + * @deprecated use {@link Filterable#removeContainerFilter(Filter)} + */ + @Deprecated public void removeContainerFilters(Object propertyId); } + /** + * Filter interface for container filtering. + * + * If a filter does not support in-memory filtering, + * {@link #passesFilter(Item)} should throw + * {@link UnsupportedOperationException}. + * + * Lazy containers must be able to map filters to their internal + * representation (e.g. SQL or JPA 2.0 Criteria). + * + * An {@link UnsupportedFilterException} can be thrown by the container if a + * particular filter is not supported by the container. + * + * An {@link Filter} should implement {@link #equals(Object)} and + * {@link #hashCode()} correctly to avoid duplicate filter registrations + * etc. + * + * @see Filterable + * + * @since 6.6 + */ + public interface Filter extends Serializable { + + /** + * Check if an item passes the filter (in-memory filtering). + * + * @param item + * @return true if the item is accepted by this filter + * @throws UnsupportedOperationException + * if the filter cannot be used for in-memory filtering + */ + public boolean passesFilter(Item item) + throws UnsupportedOperationException; + + /** + * Check if a change in the value of a property can affect the filtering + * result. May always return true, at the cost of performance. + * + * If the filter cannot determine whether it may depend on the property + * or not, should return true. + * + * @param propertyId + * @return true if the filtering result may/does change based on changes + * to the property identified by propertyId + */ + public boolean appliesToProperty(Object propertyId); + + } + + /** + * Interface that is implemented by containers which allow reducing their + * visible contents based on a set of filters. + *

+ * When a set of filters are set, only items that match the filters are + * included in the visible contents of the container. Still new items that + * do not match filters can be added to the container. Multiple filters can + * be added and the container remembers the state of the filters. When + * multiple filters are added, all filters must match for an item to be + * visible in the container. + *

+ *

+ * When an {@link Ordered} or {@link Indexed} container is filtered, all + * operations of these interfaces should only use the filtered contents and + * the filtered indices to the container. + *

+ *

+ * How filtering is performed when a {@link Hierarchical} container + * implements {@link Filterable} is implementation specific and should be + * documented in the implementing class. + *

+ *

+ * Adding items (if supported) to a filtered {@link Ordered} or + * {@link Indexed} container should insert them immediately after the + * indicated visible item. However, the unfiltered position of items added + * at index 0, at index {@link com.vaadin.data.Container#size()} or at an + * undefined position is up to the implementation. + *

+ * + *

+ * This API replaces the old Filterable interface, renamed to + * {@link SimpleFilterable} in Vaadin 6.6. Currently this interface extends + * {@link SimpleFilterable} for backwards compatibility, but might not do so + * in future versions. removeAllContainerFilters() will remain + * a part of the API. + *

+ * + * @since 6.6 + */ + public interface Filterable extends SimpleFilterable, Serializable { + /** + * Adds a filter for the container. + * + * @throws UnsupportedFilterException + * if the filter is not supported by the container + */ + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException; + + /** + * Removes a filter from the container. + * + * This requires that the equals() method considers the filters as + * equivalent (same instance or properly implemented equals() method). + */ + public void removeContainerFilter(Filter filter); + } + /** * Interface implemented by viewer classes capable of using a Container as a * data source. diff --git a/src/com/vaadin/data/util/AbstractBeanContainer.java b/src/com/vaadin/data/util/AbstractBeanContainer.java index 9853b10150..f5bd836485 100644 --- a/src/com/vaadin/data/util/AbstractBeanContainer.java +++ b/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -20,6 +20,8 @@ import com.vaadin.data.Property; import com.vaadin.data.Property.ValueChangeEvent; import com.vaadin.data.Property.ValueChangeListener; import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; /** * An abstract base class for in-memory containers for JavaBeans. @@ -326,8 +328,13 @@ public abstract class AbstractBeanContainer extends */ public void addContainerFilter(Object propertyId, String filterString, boolean ignoreCase, boolean onlyMatchPrefix) { - addFilter(new Filter(propertyId, filterString, ignoreCase, - onlyMatchPrefix)); + try { + addFilter(new SimpleStringFilter(propertyId, filterString, + ignoreCase, onlyMatchPrefix)); + } catch (UnsupportedFilterException e) { + // the filter instance created here is always valid for in-memory + // containers + } } /* @@ -352,7 +359,7 @@ public abstract class AbstractBeanContainer extends * .Object) */ public void removeContainerFilters(Object propertyId) { - Collection removedFilters = super.removeFilters(propertyId); + Collection removedFilters = super.removeFilters(propertyId); if (!removedFilters.isEmpty()) { // stop listening to change events for the property for (Item item : itemIdToItem.values()) { @@ -361,6 +368,15 @@ public abstract class AbstractBeanContainer extends } } + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + addFilter(filter); + } + + public void removeContainerFilter(Filter filter) { + removeFilter(filter); + } + /** * Make this container listen to the given property provided it notifies * when its value changes. @@ -445,7 +461,7 @@ public abstract class AbstractBeanContainer extends // add listeners to be able to update filtering on property // changes - for (ItemFilter filter : getFilters()) { + for (Filter filter : getFilters()) { for (String propertyId : getContainerPropertyIds()) { if (filter.appliesToProperty(propertyId)) { // addValueChangeListener avoids adding duplicates diff --git a/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/src/com/vaadin/data/util/AbstractInMemoryContainer.java index 978aac6d0e..df0e860b24 100644 --- a/src/com/vaadin/data/util/AbstractInMemoryContainer.java +++ b/src/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -1,6 +1,5 @@ package com.vaadin.data.util; -import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -12,6 +11,7 @@ import java.util.Set; import com.vaadin.data.Container; import com.vaadin.data.Container.ItemSetChangeNotifier; import com.vaadin.data.Item; +import com.vaadin.data.util.filter.UnsupportedFilterException; /** * Abstract {@link Container} class that handles common functionality for @@ -49,7 +49,7 @@ import com.vaadin.data.Item; * sort(Object[], boolean[]). * * To implement Filterable, subclasses need to implement the methods - * addContainerFilter() (calling {@link #addFilter(ItemFilter)}), + * addContainerFilter() (calling {@link #addFilter(Filter)}), * removeAllContainerFilters() (calling {@link #removeAllFilters()} * ) and removeContainerFilters(Object) (calling * {@link #removeFilters(Object)}). @@ -70,38 +70,6 @@ public abstract class AbstractInMemoryContainer filters = new HashSet(); + private Set filters = new HashSet(); /** * The item sorter which is used for sorting the container. @@ -395,9 +363,9 @@ public abstract class AbstractInMemoryContainer i = getFilters().iterator(); + final Iterator i = getFilters().iterator(); while (i.hasNext()) { - final ItemFilter f = i.next(); + final Filter f = i.next(); if (!f.passesFilter(item)) { return false; } @@ -406,16 +374,45 @@ public abstract class AbstractInMemoryContainer iterator = getFilters().iterator(); iterator + .hasNext();) { + Filter f = iterator.next(); + if (f.equals(filter)) { + iterator.remove(); + filterAll(); + return; + } + } + } + /** * Remove all container filters for all properties and re-filter the view. * @@ -440,9 +437,9 @@ public abstract class AbstractInMemoryContainer i = getFilters().iterator(); + final Iterator i = getFilters().iterator(); while (i.hasNext()) { - final ItemFilter f = i.next(); + final Filter f = i.next(); if (f.appliesToProperty(propertyId)) { return true; } @@ -461,14 +458,14 @@ public abstract class AbstractInMemoryContainer removed filters */ - protected Collection removeFilters(Object propertyId) { + protected Collection removeFilters(Object propertyId) { if (getFilters().isEmpty() || propertyId == null) { return Collections.emptyList(); } - List removedFilters = new LinkedList(); - for (Iterator iterator = getFilters().iterator(); iterator + List removedFilters = new LinkedList(); + for (Iterator iterator = getFilters().iterator(); iterator .hasNext();) { - ItemFilter f = iterator.next(); + Filter f = iterator.next(); if (f.appliesToProperty(propertyId)) { removedFilters.add(f); iterator.remove(); @@ -860,7 +857,7 @@ public abstract class AbstractInMemoryContainer filters) { + protected void setFilters(Set filters) { this.filters = filters; } @@ -868,9 +865,9 @@ public abstract class AbstractInMemoryContainer + * @return Set */ - protected Set getFilters() { + protected Set getFilters() { return filters; } diff --git a/src/com/vaadin/data/util/IndexedContainer.java b/src/com/vaadin/data/util/IndexedContainer.java index c87417a75f..78df8b2cc2 100644 --- a/src/com/vaadin/data/util/IndexedContainer.java +++ b/src/com/vaadin/data/util/IndexedContainer.java @@ -21,6 +21,8 @@ import java.util.Map; import com.vaadin.data.Container; import com.vaadin.data.Item; import com.vaadin.data.Property; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; /** * An implementation of the {@link Container.Indexed} interface @@ -1014,7 +1016,7 @@ public class IndexedContainer extends nc.types = types != null ? (Hashtable>) types.clone() : null; - nc.setFilters((HashSet) ((HashSet) getFilters()) + nc.setFilters((HashSet) ((HashSet) getFilters()) .clone()); nc.setFilteredItemIds(getFilteredItemIds() == null ? null @@ -1039,8 +1041,13 @@ public class IndexedContainer extends public void addContainerFilter(Object propertyId, String filterString, boolean ignoreCase, boolean onlyMatchPrefix) { - addFilter(new Filter(propertyId, filterString, ignoreCase, - onlyMatchPrefix)); + try { + addFilter(new SimpleStringFilter(propertyId, filterString, + ignoreCase, onlyMatchPrefix)); + } catch (UnsupportedFilterException e) { + // the filter instance created here is always valid for in-memory + // containers + } } public void removeAllContainerFilters() { @@ -1051,4 +1058,13 @@ public class IndexedContainer extends removeFilters(propertyId); } + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + addFilter(filter); + } + + public void removeContainerFilter(Filter filter) { + removeFilter(filter); + } + } \ No newline at end of file diff --git a/src/com/vaadin/data/util/Filter.java b/src/com/vaadin/data/util/filter/SimpleStringFilter.java similarity index 66% rename from src/com/vaadin/data/util/Filter.java rename to src/com/vaadin/data/util/filter/SimpleStringFilter.java index f6f6638250..3fb81c5641 100644 --- a/src/com/vaadin/data/util/Filter.java +++ b/src/com/vaadin/data/util/filter/SimpleStringFilter.java @@ -1,31 +1,35 @@ -/* -@ITMillApache2LicenseForJavaFiles@ - */ -package com.vaadin.data.util; - -import java.io.Serializable; +package com.vaadin.data.util.filter; +import com.vaadin.data.Container.Filter; import com.vaadin.data.Item; import com.vaadin.data.Property; /** - * A default filter that can be used to implement - * {@link com.vaadin.data.Container.Filterable}. + * Simple string filter for matching items that start with or contain a + * specified string. The matching can be case-sensitive or case-insensitive. + * + * This filter also directly supports in-memory filtering. When performing + * in-memory filtering, values of other types are converted using toString(), + * but other (lazy container) implementations do not need to perform such + * conversions and might not support values of different types. + * + * Note that this filter is modeled after the pre-6.6 filtering mechanisms, and + * might not be very efficient e.g. for database filtering. * - * @since 5.4 + * TODO this might still change + * + * @since 6.6 */ -@SuppressWarnings("serial") -public class Filter implements AbstractInMemoryContainer.ItemFilter, - Serializable { +public class SimpleStringFilter implements Filter { + final Object propertyId; final String filterString; final boolean ignoreCase; final boolean onlyMatchPrefix; - Filter(Object propertyId, String filterString, boolean ignoreCase, - boolean onlyMatchPrefix) { + public SimpleStringFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { this.propertyId = propertyId; - ; this.filterString = ignoreCase ? filterString.toLowerCase() : filterString; this.ignoreCase = ignoreCase; @@ -59,10 +63,10 @@ public class Filter implements AbstractInMemoryContainer.ItemFilter, public boolean equals(Object obj) { // Only ones of the objects of the same class can be equal - if (!(obj instanceof Filter)) { + if (!(obj instanceof SimpleStringFilter)) { return false; } - final Filter o = (Filter) obj; + final SimpleStringFilter o = (SimpleStringFilter) obj; // Checks the properties one by one if (propertyId != o.propertyId && o.propertyId != null @@ -88,5 +92,4 @@ public class Filter implements AbstractInMemoryContainer.ItemFilter, return (propertyId != null ? propertyId.hashCode() : 0) ^ (filterString != null ? filterString.hashCode() : 0); } - -} \ No newline at end of file +} diff --git a/src/com/vaadin/data/util/filter/UnsupportedFilterException.java b/src/com/vaadin/data/util/filter/UnsupportedFilterException.java new file mode 100644 index 0000000000..b6b1074024 --- /dev/null +++ b/src/com/vaadin/data/util/filter/UnsupportedFilterException.java @@ -0,0 +1,32 @@ +package com.vaadin.data.util.filter; + +import java.io.Serializable; + +/** + * Exception for cases where a container does not support a specific type of + * filters. + * + * If possible, this should be thrown already when adding a filter to a + * container. If a problem is not detected at that point, an + * {@link UnsupportedOperationException} can be throws when attempting to + * perform filtering. + * + * @since 6.6 + */ +public class UnsupportedFilterException extends RuntimeException implements + Serializable { + public UnsupportedFilterException() { + } + + public UnsupportedFilterException(String message) { + super(message); + } + + public UnsupportedFilterException(Exception cause) { + super(cause); + } + + public UnsupportedFilterException(String message, Exception cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/tests/src/com/vaadin/tests/server/container/filter/AbstractFilterTest.java b/tests/src/com/vaadin/tests/server/container/filter/AbstractFilterTest.java new file mode 100644 index 0000000000..0438ee4dde --- /dev/null +++ b/tests/src/com/vaadin/tests/server/container/filter/AbstractFilterTest.java @@ -0,0 +1,49 @@ +package com.vaadin.tests.server.container.filter; + +import junit.framework.TestCase; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Property; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; + +public abstract class AbstractFilterTest extends + TestCase { + + protected static final String PROPERTY1 = "property1"; + protected static final String PROPERTY2 = "property2"; + + protected static class TestItem extends PropertysetItem { + + public TestItem(T1 value1, T2 value2) { + addItemProperty(PROPERTY1, new ObjectProperty(value1)); + addItemProperty(PROPERTY2, new ObjectProperty(value2)); + } + } + + protected static class NullProperty implements Property { + + public Object getValue() { + return null; + } + + public void setValue(Object newValue) throws ReadOnlyException, + ConversionException { + throw new ReadOnlyException(); + } + + public Class getType() { + return String.class; + } + + public boolean isReadOnly() { + return true; + } + + public void setReadOnly(boolean newStatus) { + // do nothing + } + + } + +} diff --git a/tests/src/com/vaadin/tests/server/container/filter/SimpleStringFilterTest.java b/tests/src/com/vaadin/tests/server/container/filter/SimpleStringFilterTest.java new file mode 100644 index 0000000000..d8415f85a6 --- /dev/null +++ b/tests/src/com/vaadin/tests/server/container/filter/SimpleStringFilterTest.java @@ -0,0 +1,130 @@ +package com.vaadin.tests.server.container.filter; + +import junit.framework.Assert; + +import com.vaadin.data.util.filter.SimpleStringFilter; + +public class SimpleStringFilterTest extends AbstractFilterTest { + + protected static TestItem createTestItem() { + return new TestItem("abcde", "TeSt"); + } + + protected TestItem getTestItem() { + return createTestItem(); + } + + protected SimpleStringFilter f(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + return new SimpleStringFilter(propertyId, filterString, ignoreCase, + onlyMatchPrefix); + } + + protected boolean passes(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + return f(propertyId, filterString, ignoreCase, onlyMatchPrefix) + .passesFilter(getTestItem()); + } + + public void testStartsWithCaseSensitive() { + Assert.assertTrue(passes(PROPERTY1, "ab", false, true)); + Assert.assertTrue(passes(PROPERTY1, "", false, true)); + + Assert.assertFalse(passes(PROPERTY2, "ab", false, true)); + Assert.assertFalse(passes(PROPERTY1, "AB", false, true)); + } + + public void testStartsWithCaseInsensitive() { + Assert.assertTrue(passes(PROPERTY1, "AB", true, true)); + Assert.assertTrue(passes(PROPERTY2, "te", true, true)); + Assert.assertFalse(passes(PROPERTY2, "AB", true, true)); + } + + public void testContainsCaseSensitive() { + Assert.assertTrue(passes(PROPERTY1, "ab", false, false)); + Assert.assertTrue(passes(PROPERTY1, "abcde", false, false)); + Assert.assertTrue(passes(PROPERTY1, "cd", false, false)); + Assert.assertTrue(passes(PROPERTY1, "e", false, false)); + Assert.assertTrue(passes(PROPERTY1, "", false, false)); + + Assert.assertFalse(passes(PROPERTY2, "ab", false, false)); + Assert.assertFalse(passes(PROPERTY1, "es", false, false)); + } + + public void testContainsCaseInsensitive() { + Assert.assertTrue(passes(PROPERTY1, "AB", true, false)); + Assert.assertTrue(passes(PROPERTY1, "aBcDe", true, false)); + Assert.assertTrue(passes(PROPERTY1, "CD", true, false)); + Assert.assertTrue(passes(PROPERTY1, "", true, false)); + + Assert.assertTrue(passes(PROPERTY2, "es", true, false)); + + Assert.assertFalse(passes(PROPERTY2, "ab", true, false)); + } + + public void testAppliesToProperty() { + SimpleStringFilter filter = f(PROPERTY1, "ab", false, true); + Assert.assertTrue(filter.appliesToProperty(PROPERTY1)); + Assert.assertFalse(filter.appliesToProperty(PROPERTY2)); + Assert.assertFalse(filter.appliesToProperty("other")); + } + + public void testEqualsHashCode() { + SimpleStringFilter filter = f(PROPERTY1, "ab", false, true); + + SimpleStringFilter f1 = f(PROPERTY2, "ab", false, true); + SimpleStringFilter f1b = f(PROPERTY2, "ab", false, true); + SimpleStringFilter f2 = f(PROPERTY1, "cd", false, true); + SimpleStringFilter f2b = f(PROPERTY1, "cd", false, true); + SimpleStringFilter f3 = f(PROPERTY1, "ab", true, true); + SimpleStringFilter f3b = f(PROPERTY1, "ab", true, true); + SimpleStringFilter f4 = f(PROPERTY1, "ab", false, false); + SimpleStringFilter f4b = f(PROPERTY1, "ab", false, false); + + // equal but not same instance + Assert.assertEquals(f1, f1b); + Assert.assertEquals(f2, f2b); + Assert.assertEquals(f3, f3b); + Assert.assertEquals(f4, f4b); + + // more than one property differ + Assert.assertFalse(f1.equals(f2)); + Assert.assertFalse(f1.equals(f3)); + Assert.assertFalse(f1.equals(f4)); + Assert.assertFalse(f2.equals(f1)); + Assert.assertFalse(f2.equals(f3)); + Assert.assertFalse(f2.equals(f4)); + Assert.assertFalse(f3.equals(f1)); + Assert.assertFalse(f3.equals(f2)); + Assert.assertFalse(f3.equals(f4)); + Assert.assertFalse(f4.equals(f1)); + Assert.assertFalse(f4.equals(f2)); + Assert.assertFalse(f4.equals(f3)); + + // only one property differs + Assert.assertFalse(filter.equals(f1)); + Assert.assertFalse(filter.equals(f2)); + Assert.assertFalse(filter.equals(f3)); + Assert.assertFalse(filter.equals(f4)); + + Assert.assertFalse(f1.equals(null)); + Assert.assertFalse(f1.equals(new Object())); + + Assert.assertEquals(f1.hashCode(), f1b.hashCode()); + Assert.assertEquals(f2.hashCode(), f2b.hashCode()); + Assert.assertEquals(f3.hashCode(), f3b.hashCode()); + Assert.assertEquals(f4.hashCode(), f4b.hashCode()); + } + + public void testNonExistentProperty() { + Assert.assertFalse(passes("other1", "ab", false, true)); + } + + public void testNullValueForProperty() { + TestItem item = createTestItem(); + item.addItemProperty("other1", new NullProperty()); + + Assert.assertFalse(f("other1", "ab", false, true).passesFilter(item)); + } + +} -- 2.39.5