import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.vaadin.client.Profiler;
+import com.vaadin.shared.Range;
import com.vaadin.shared.Registration;
-import com.vaadin.shared.ui.grid.Range;
/**
* Base implementation for data sources that fetch data from a remote system.
package com.vaadin.client.data;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
/**
* Determines what data an {@link AbstractRemoteDataSource} should fetch and
package com.vaadin.client.data;
+import java.util.function.Consumer;
+
+import com.vaadin.shared.Range;
import com.vaadin.shared.Registration;
/**
* override of an existing method, we're defining a new method for that
* instead.
*
- * @param rowHandle
+ * @param obj
* the reference object with which to compare
* @return {@code true} if this object is the same as the obj argument;
* {@code false} otherwise.
*
* @param dataChangeHandler
* the data change handler
+ *
+ * @return registration for removing the handler
*/
public Registration addDataChangeHandler(
DataChangeHandler dataChangeHandler);
+ /**
+ * Sets a simple data change handler for a widget without lazy loading.
+ * Refresh method should reset all the data in the widget.
+ *
+ * @param refreshMethod
+ * a method to refresh all data in the widget
+ *
+ * @return registration for removing the handler
+ */
+ public default Registration addDataChangeHandler(
+ Consumer<Range> refreshMethod) {
+ return addDataChangeHandler(
+ new SimpleDataChangeHandler(this, refreshMethod));
+ }
+
/**
* Gets a {@link RowHandle} of a row object in the cache.
*
--- /dev/null
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * 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.vaadin.client.data;
+
+import java.util.function.Consumer;
+
+import com.google.gwt.core.client.Scheduler;
+import com.vaadin.shared.Range;
+
+/**
+ * Helper class for creating a {@link DataChangeHandler} for a Widget that does
+ * not support lazy loading.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public class SimpleDataChangeHandler implements DataChangeHandler {
+
+ /**
+ * Class to request the data source to get the full data set.
+ */
+ private static class DelayedResetScheduler {
+
+ private final DataSource<?> dataSource;
+ private boolean scheduled = false;
+
+ public DelayedResetScheduler(DataSource<?> dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public void schedule() {
+ if (scheduled) {
+ return;
+ }
+ Scheduler.get().scheduleFinally(() -> {
+ dataSource.ensureAvailability(0, dataSource.size());
+ scheduled = false;
+ });
+ scheduled = true;
+ }
+
+ public int getExpectedSize() {
+ return dataSource.size();
+ }
+
+ public boolean isScheduled() {
+ return scheduled;
+ }
+ }
+
+ private final DelayedResetScheduler scheduler;
+ private final Consumer<Range> refreshMethod;
+
+ SimpleDataChangeHandler(DataSource<?> dataSource,
+ Consumer<Range> refreshMethod) {
+ scheduler = new DelayedResetScheduler(dataSource);
+ this.refreshMethod = refreshMethod;
+ }
+
+ @Override
+ public void dataUpdated(int firstRowIndex, int numberOfRows) {
+ scheduler.schedule();
+ }
+
+ @Override
+ public void dataRemoved(int firstRowIndex, int numberOfRows) {
+ scheduler.schedule();
+ }
+
+ @Override
+ public void dataAdded(int firstRowIndex, int numberOfRows) {
+ scheduler.schedule();
+ }
+
+ @Override
+ public void dataAvailable(int firstRowIndex, int numberOfRows) {
+ if (!scheduler.isScheduled() && firstRowIndex == 0
+ && numberOfRows == scheduler.getExpectedSize()) {
+ // All data should now be available.
+ refreshMethod.accept(Range.withLength(firstRowIndex, numberOfRows));
+ } else {
+ scheduler.schedule();
+ }
+ }
+
+ @Override
+ public void resetDataAndSize(int newSize) {
+ scheduler.schedule();
+ }
+}
package com.vaadin.client.widget.escalator;
import com.google.gwt.event.shared.GwtEvent;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
/**
* Event fired when the range of visible rows changes e.g. because of scrolling.
package com.vaadin.client.widget.grid;
import com.google.gwt.event.shared.GwtEvent;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
/**
* Event object describing a change of row availability in DataSource of a Grid.
import com.vaadin.client.widget.grid.events.ScrollEvent;
import com.vaadin.client.widget.grid.events.ScrollHandler;
import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle;
+import com.vaadin.shared.Range;
import com.vaadin.shared.ui.grid.HeightMode;
-import com.vaadin.shared.ui.grid.Range;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.client.widgets.Grid.Editor.State;
import com.vaadin.client.widgets.Grid.StaticSection.StaticCell;
import com.vaadin.client.widgets.Grid.StaticSection.StaticRow;
+import com.vaadin.shared.Range;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.grid.GridConstants;
import com.vaadin.shared.ui.grid.GridConstants.Section;
import com.vaadin.shared.ui.grid.GridStaticCellType;
import com.vaadin.shared.ui.grid.HeightMode;
-import com.vaadin.shared.ui.grid.Range;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.shared.util.SharedUtil;
import org.junit.Test;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
@SuppressWarnings("static-method")
public class PartitioningTest {
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.data.DataSource;
import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.shared.Range;
import com.vaadin.shared.ui.Connect;
-import com.vaadin.shared.ui.grid.Range;
import com.vaadin.v7.client.renderers.ComplexRenderer;
import com.vaadin.v7.client.renderers.Renderer;
import com.vaadin.v7.client.widget.grid.DataAvailableEvent;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.data.AbstractRemoteDataSource;
import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.shared.Range;
import com.vaadin.shared.data.DataProviderRpc;
import com.vaadin.shared.data.DataRequestRpc;
import com.vaadin.shared.ui.Connect;
-import com.vaadin.shared.ui.grid.Range;
import com.vaadin.v7.shared.ui.grid.GridState;
import elemental.json.Json;
package com.vaadin.v7.client.widget.escalator;
import com.google.gwt.event.shared.GwtEvent;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
/**
* Event fired when the range of visible rows changes e.g. because of scrolling.
package com.vaadin.v7.client.widget.grid;
import com.google.gwt.event.shared.GwtEvent;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
/**
* Event object describing a change of row availability in DataSource of a Grid.
import com.vaadin.client.Profiler;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.SubPartAware;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.v7.client.widget.escalator.Cell;
import com.vaadin.v7.client.widget.escalator.ColumnConfiguration;
import com.vaadin.client.ui.dd.DragHandle;
import com.vaadin.client.ui.dd.DragHandle.DragHandleCallback;
import com.vaadin.client.widgets.Overlay;
+import com.vaadin.shared.Range;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.sort.SortDirection;
-import com.vaadin.shared.ui.grid.Range;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.v7.client.renderers.ComplexRenderer;
import com.vaadin.v7.client.renderers.Renderer;
import com.vaadin.server.AbstractExtension;
import com.vaadin.server.ClientConnector;
import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.Range;
import com.vaadin.shared.data.DataProviderRpc;
import com.vaadin.shared.data.DataRequestRpc;
-import com.vaadin.shared.ui.grid.Range;
import com.vaadin.v7.data.Container;
import com.vaadin.v7.data.Container.Indexed;
import com.vaadin.v7.data.Container.Indexed.ItemAddEvent;
import com.vaadin.server.AbstractExtension;
import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.Range;
import com.vaadin.shared.data.DataCommunicatorClientRpc;
import com.vaadin.shared.data.DataCommunicatorConstants;
import com.vaadin.shared.data.DataRequestRpc;
-import com.vaadin.shared.ui.grid.Range;
import elemental.json.Json;
import elemental.json.JsonArray;
--- /dev/null
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * 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.vaadin.shared;
+
+import java.io.Serializable;
+
+/**
+ * An immutable representation of a range, marked by start and end points.
+ * <p>
+ * The range is treated as inclusive at the start, and exclusive at the end.
+ * I.e. the range [0..1[ has the length 1, and represents one integer: 0.
+ * <p>
+ * The range is considered {@link #isEmpty() empty} if the start is the same as
+ * the end.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public final class Range implements Serializable {
+ private final int start;
+ private final int end;
+
+ /**
+ * Creates a range object representing a single integer.
+ *
+ * @param integer
+ * the number to represent as a range
+ * @return the range represented by <code>integer</code>
+ */
+ public static Range withOnly(final int integer) {
+ return new Range(integer, integer + 1);
+ }
+
+ /**
+ * Creates a range between two integers.
+ * <p>
+ * The range start is <em>inclusive</em> and the end is <em>exclusive</em>.
+ * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4,
+ * but not 5.
+ *
+ * @param start
+ * the start of the the range, inclusive
+ * @param end
+ * the end of the range, exclusive
+ * @return a range representing <code>[start..end[</code>
+ * @throws IllegalArgumentException
+ * if <code>start > end</code>
+ */
+ public static Range between(final int start, final int end)
+ throws IllegalArgumentException {
+ return new Range(start, end);
+ }
+
+ /**
+ * Creates a range from a start point, with a given length.
+ *
+ * @param start
+ * the first integer to include in the range
+ * @param length
+ * the length of the resulting range
+ * @return a range starting from <code>start</code>, with
+ * <code>length</code> number of integers following
+ * @throws IllegalArgumentException
+ * if length < 0
+ */
+ public static Range withLength(final int start, final int length)
+ throws IllegalArgumentException {
+ if (length < 0) {
+ /*
+ * The constructor of Range will throw an exception if start >
+ * start+length (i.e. if length is negative). We're throwing the
+ * same exception type, just with a more descriptive message.
+ */
+ throw new IllegalArgumentException("length must not be negative");
+ }
+ return new Range(start, start + length);
+ }
+
+ /**
+ * Creates a new range between two numbers: <code>[start..end[</code>.
+ *
+ * @param start
+ * the start integer, inclusive
+ * @param end
+ * the end integer, exclusive
+ * @throws IllegalArgumentException
+ * if <code>start > end</code>
+ */
+ private Range(final int start, final int end)
+ throws IllegalArgumentException {
+ if (start > end) {
+ throw new IllegalArgumentException(
+ "start must not be greater than end");
+ }
+
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Returns the <em>inclusive</em> start point of this range.
+ *
+ * @return the start point of this range
+ */
+ public int getStart() {
+ return start;
+ }
+
+ /**
+ * Returns the <em>exclusive</em> end point of this range.
+ *
+ * @return the end point of this range
+ */
+ public int getEnd() {
+ return end;
+ }
+
+ /**
+ * The number of integers contained in the range.
+ *
+ * @return the number of integers contained in the range
+ */
+ public int length() {
+ return getEnd() - getStart();
+ }
+
+ /**
+ * Checks whether the range has no elements between the start and end.
+ *
+ * @return <code>true</code> iff the range contains no elements.
+ */
+ public boolean isEmpty() {
+ return getStart() >= getEnd();
+ }
+
+ /**
+ * Checks whether this range and another range are at least partially
+ * covering the same values.
+ *
+ * @param other
+ * the other range to check against
+ * @return <code>true</code> if this and <code>other</code> intersect
+ */
+ public boolean intersects(final Range other) {
+ return getStart() < other.getEnd() && other.getStart() < getEnd();
+ }
+
+ /**
+ * Checks whether an integer is found within this range.
+ *
+ * @param integer
+ * an integer to test for presence in this range
+ * @return <code>true</code> iff <code>integer</code> is in this range
+ */
+ public boolean contains(final int integer) {
+ return getStart() <= integer && integer < getEnd();
+ }
+
+ /**
+ * Checks whether this range is a subset of another range.
+ *
+ * @return <code>true</code> iff <code>other</code> completely wraps this
+ * range
+ */
+ public boolean isSubsetOf(final Range other) {
+ if (isEmpty() && other.isEmpty()) {
+ return true;
+ }
+
+ return other.getStart() <= getStart() && getEnd() <= other.getEnd();
+ }
+
+ /**
+ * Overlay this range with another one, and partition the ranges according
+ * to how they position relative to each other.
+ * <p>
+ * The three partitions are returned as a three-element Range array:
+ * <ul>
+ * <li>Elements in this range that occur before elements in
+ * <code>other</code>.
+ * <li>Elements that are shared between the two ranges.
+ * <li>Elements in this range that occur after elements in
+ * <code>other</code>.
+ * </ul>
+ *
+ * @param other
+ * the other range to act as delimiters.
+ * @return a three-element Range array of partitions depicting the elements
+ * before (index 0), shared/inside (index 1) and after (index 2).
+ */
+ public Range[] partitionWith(final Range other) {
+ final Range[] splitBefore = splitAt(other.getStart());
+ final Range rangeBefore = splitBefore[0];
+ final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd());
+ final Range rangeInside = splitAfter[0];
+ final Range rangeAfter = splitAfter[1];
+ return new Range[] { rangeBefore, rangeInside, rangeAfter };
+ }
+
+ /**
+ * Get a range that is based on this one, but offset by a number.
+ *
+ * @param offset
+ * the number to offset by
+ * @return a copy of this range, offset by <code>offset</code>
+ */
+ public Range offsetBy(final int offset) {
+ if (offset == 0) {
+ return this;
+ } else {
+ return new Range(start + offset, end + offset);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd()
+ + "[" + (isEmpty() ? " (empty)" : "");
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + end;
+ result = prime * result + start;
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Range other = (Range) obj;
+ if (end != other.end) {
+ return false;
+ }
+ if (start != other.start) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether this range starts before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range starts before the
+ * <code>other</code>
+ */
+ public boolean startsBefore(final Range other) {
+ return getStart() < other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range ends before the
+ * <code>other</code>
+ */
+ public boolean endsBefore(final Range other) {
+ return getEnd() <= other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range ends after the
+ * <code>other</code>
+ */
+ public boolean endsAfter(final Range other) {
+ return getEnd() > other.getEnd();
+ }
+
+ /**
+ * Checks whether this range starts after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range starts after the
+ * <code>other</code>
+ */
+ public boolean startsAfter(final Range other) {
+ return getStart() >= other.getEnd();
+ }
+
+ /**
+ * Split the range into two at a certain integer.
+ * <p>
+ * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code>
+ *
+ * @param integer
+ * the integer at which to split the range into two
+ * @return an array of two ranges, with <code>[start..integer[</code> in the
+ * first element, and <code>[integer..end[</code> in the second
+ * element.
+ * <p>
+ * If {@code integer} is less than {@code start}, [empty,
+ * {@code this} ] is returned. if <code>integer</code> is equal to
+ * or greater than {@code end}, [{@code this}, empty] is returned
+ * instead.
+ */
+ public Range[] splitAt(final int integer) {
+ if (integer < start) {
+ return new Range[] { Range.withLength(start, 0), this };
+ } else if (integer >= end) {
+ return new Range[] { this, Range.withLength(end, 0) };
+ } else {
+ return new Range[] { new Range(start, integer),
+ new Range(integer, end) };
+ }
+ }
+
+ /**
+ * Split the range into two after a certain number of integers into the
+ * range.
+ * <p>
+ * Calling this method is equivalent to calling
+ * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code>
+ * <p>
+ * <em>Example:</em>
+ * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code>
+ *
+ * @param length
+ * the length at which to split this range into two
+ * @return an array of two ranges, having the <code>length</code>-first
+ * elements of this range, and the second range having the rest. If
+ * <code>length</code> ≤ 0, the first element will be empty, and
+ * the second element will be this range. If <code>length</code>
+ * ≥ {@link #length()}, the first element will be this range,
+ * and the second element will be empty.
+ */
+ public Range[] splitAtFromStart(final int length) {
+ return splitAt(getStart() + length);
+ }
+
+ /**
+ * Combines two ranges to create a range containing all values in both
+ * ranges, provided there are no gaps between the ranges.
+ *
+ * @param other
+ * the range to combine with this range
+ *
+ * @return the combined range
+ *
+ * @throws IllegalArgumentException
+ * if the two ranges aren't connected
+ */
+ public Range combineWith(Range other) throws IllegalArgumentException {
+ if (getStart() > other.getEnd() || other.getStart() > getEnd()) {
+ throw new IllegalArgumentException(
+ "There is a gap between " + this + " and " + other);
+ }
+
+ return Range.between(Math.min(getStart(), other.getStart()),
+ Math.max(getEnd(), other.getEnd()));
+ }
+
+ /**
+ * Creates a range that is expanded the given amounts in both ends.
+ *
+ * @param startDelta
+ * the amount to expand by in the beginning of the range
+ * @param endDelta
+ * the amount to expand by in the end of the range
+ *
+ * @return an expanded range
+ *
+ * @throws IllegalArgumentException
+ * if the new range would have <code>start > end</code>
+ */
+ public Range expand(int startDelta, int endDelta)
+ throws IllegalArgumentException {
+ return Range.between(getStart() - startDelta, getEnd() + endDelta);
+ }
+
+ /**
+ * Limits this range to be within the bounds of the provided range.
+ * <p>
+ * This is basically an optimized way of calculating
+ * <code>{@link #partitionWith(Range)}[1]</code> without the overhead of
+ * defining the parts that do not overlap.
+ * <p>
+ * If the two ranges do not intersect, an empty range is returned. There are
+ * no guarantees about the position of that range.
+ *
+ * @param bounds
+ * the bounds that the returned range should be limited to
+ * @return a bounded range
+ */
+ public Range restrictTo(Range bounds) {
+ boolean startWithin = bounds.contains(getStart());
+ boolean endWithin = bounds.contains(getEnd());
+ boolean boundsWithin = getStart() < bounds.getStart()
+ && getEnd() >= bounds.getEnd();
+
+ if (startWithin) {
+ if (endWithin) {
+ return this;
+ } else {
+ return Range.between(getStart(), bounds.getEnd());
+ }
+ } else {
+ if (endWithin) {
+ return Range.between(bounds.getStart(), getEnd());
+ } else if (boundsWithin) {
+ return bounds;
+ } else {
+ return Range.withLength(getStart(), 0);
+ }
+ }
+ }
+}
+++ /dev/null
-/*
- * Copyright 2000-2016 Vaadin Ltd.
- *
- * 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.vaadin.shared.ui.grid;
-
-import java.io.Serializable;
-
-/**
- * An immutable representation of a range, marked by start and end points.
- * <p>
- * The range is treated as inclusive at the start, and exclusive at the end.
- * I.e. the range [0..1[ has the length 1, and represents one integer: 0.
- * <p>
- * The range is considered {@link #isEmpty() empty} if the start is the same as
- * the end.
- *
- * @since 7.4
- * @author Vaadin Ltd
- */
-public final class Range implements Serializable {
- private final int start;
- private final int end;
-
- /**
- * Creates a range object representing a single integer.
- *
- * @param integer
- * the number to represent as a range
- * @return the range represented by <code>integer</code>
- */
- public static Range withOnly(final int integer) {
- return new Range(integer, integer + 1);
- }
-
- /**
- * Creates a range between two integers.
- * <p>
- * The range start is <em>inclusive</em> and the end is <em>exclusive</em>.
- * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4,
- * but not 5.
- *
- * @param start
- * the start of the the range, inclusive
- * @param end
- * the end of the range, exclusive
- * @return a range representing <code>[start..end[</code>
- * @throws IllegalArgumentException
- * if <code>start > end</code>
- */
- public static Range between(final int start, final int end)
- throws IllegalArgumentException {
- return new Range(start, end);
- }
-
- /**
- * Creates a range from a start point, with a given length.
- *
- * @param start
- * the first integer to include in the range
- * @param length
- * the length of the resulting range
- * @return a range starting from <code>start</code>, with
- * <code>length</code> number of integers following
- * @throws IllegalArgumentException
- * if length < 0
- */
- public static Range withLength(final int start, final int length)
- throws IllegalArgumentException {
- if (length < 0) {
- /*
- * The constructor of Range will throw an exception if start >
- * start+length (i.e. if length is negative). We're throwing the
- * same exception type, just with a more descriptive message.
- */
- throw new IllegalArgumentException("length must not be negative");
- }
- return new Range(start, start + length);
- }
-
- /**
- * Creates a new range between two numbers: <code>[start..end[</code>.
- *
- * @param start
- * the start integer, inclusive
- * @param end
- * the end integer, exclusive
- * @throws IllegalArgumentException
- * if <code>start > end</code>
- */
- private Range(final int start, final int end)
- throws IllegalArgumentException {
- if (start > end) {
- throw new IllegalArgumentException(
- "start must not be greater than end");
- }
-
- this.start = start;
- this.end = end;
- }
-
- /**
- * Returns the <em>inclusive</em> start point of this range.
- *
- * @return the start point of this range
- */
- public int getStart() {
- return start;
- }
-
- /**
- * Returns the <em>exclusive</em> end point of this range.
- *
- * @return the end point of this range
- */
- public int getEnd() {
- return end;
- }
-
- /**
- * The number of integers contained in the range.
- *
- * @return the number of integers contained in the range
- */
- public int length() {
- return getEnd() - getStart();
- }
-
- /**
- * Checks whether the range has no elements between the start and end.
- *
- * @return <code>true</code> iff the range contains no elements.
- */
- public boolean isEmpty() {
- return getStart() >= getEnd();
- }
-
- /**
- * Checks whether this range and another range are at least partially
- * covering the same values.
- *
- * @param other
- * the other range to check against
- * @return <code>true</code> if this and <code>other</code> intersect
- */
- public boolean intersects(final Range other) {
- return getStart() < other.getEnd() && other.getStart() < getEnd();
- }
-
- /**
- * Checks whether an integer is found within this range.
- *
- * @param integer
- * an integer to test for presence in this range
- * @return <code>true</code> iff <code>integer</code> is in this range
- */
- public boolean contains(final int integer) {
- return getStart() <= integer && integer < getEnd();
- }
-
- /**
- * Checks whether this range is a subset of another range.
- *
- * @return <code>true</code> iff <code>other</code> completely wraps this
- * range
- */
- public boolean isSubsetOf(final Range other) {
- if (isEmpty() && other.isEmpty()) {
- return true;
- }
-
- return other.getStart() <= getStart() && getEnd() <= other.getEnd();
- }
-
- /**
- * Overlay this range with another one, and partition the ranges according
- * to how they position relative to each other.
- * <p>
- * The three partitions are returned as a three-element Range array:
- * <ul>
- * <li>Elements in this range that occur before elements in
- * <code>other</code>.
- * <li>Elements that are shared between the two ranges.
- * <li>Elements in this range that occur after elements in
- * <code>other</code>.
- * </ul>
- *
- * @param other
- * the other range to act as delimiters.
- * @return a three-element Range array of partitions depicting the elements
- * before (index 0), shared/inside (index 1) and after (index 2).
- */
- public Range[] partitionWith(final Range other) {
- final Range[] splitBefore = splitAt(other.getStart());
- final Range rangeBefore = splitBefore[0];
- final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd());
- final Range rangeInside = splitAfter[0];
- final Range rangeAfter = splitAfter[1];
- return new Range[] { rangeBefore, rangeInside, rangeAfter };
- }
-
- /**
- * Get a range that is based on this one, but offset by a number.
- *
- * @param offset
- * the number to offset by
- * @return a copy of this range, offset by <code>offset</code>
- */
- public Range offsetBy(final int offset) {
- if (offset == 0) {
- return this;
- } else {
- return new Range(start + offset, end + offset);
- }
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd()
- + "[" + (isEmpty() ? " (empty)" : "");
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + end;
- result = prime * result + start;
- return result;
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- final Range other = (Range) obj;
- if (end != other.end) {
- return false;
- }
- if (start != other.start) {
- return false;
- }
- return true;
- }
-
- /**
- * Checks whether this range starts before the start of another range.
- *
- * @param other
- * the other range to compare against
- * @return <code>true</code> iff this range starts before the
- * <code>other</code>
- */
- public boolean startsBefore(final Range other) {
- return getStart() < other.getStart();
- }
-
- /**
- * Checks whether this range ends before the start of another range.
- *
- * @param other
- * the other range to compare against
- * @return <code>true</code> iff this range ends before the
- * <code>other</code>
- */
- public boolean endsBefore(final Range other) {
- return getEnd() <= other.getStart();
- }
-
- /**
- * Checks whether this range ends after the end of another range.
- *
- * @param other
- * the other range to compare against
- * @return <code>true</code> iff this range ends after the
- * <code>other</code>
- */
- public boolean endsAfter(final Range other) {
- return getEnd() > other.getEnd();
- }
-
- /**
- * Checks whether this range starts after the end of another range.
- *
- * @param other
- * the other range to compare against
- * @return <code>true</code> iff this range starts after the
- * <code>other</code>
- */
- public boolean startsAfter(final Range other) {
- return getStart() >= other.getEnd();
- }
-
- /**
- * Split the range into two at a certain integer.
- * <p>
- * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code>
- *
- * @param integer
- * the integer at which to split the range into two
- * @return an array of two ranges, with <code>[start..integer[</code> in the
- * first element, and <code>[integer..end[</code> in the second
- * element.
- * <p>
- * If {@code integer} is less than {@code start}, [empty,
- * {@code this} ] is returned. if <code>integer</code> is equal to
- * or greater than {@code end}, [{@code this}, empty] is returned
- * instead.
- */
- public Range[] splitAt(final int integer) {
- if (integer < start) {
- return new Range[] { Range.withLength(start, 0), this };
- } else if (integer >= end) {
- return new Range[] { this, Range.withLength(end, 0) };
- } else {
- return new Range[] { new Range(start, integer),
- new Range(integer, end) };
- }
- }
-
- /**
- * Split the range into two after a certain number of integers into the
- * range.
- * <p>
- * Calling this method is equivalent to calling
- * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code>
- * <p>
- * <em>Example:</em>
- * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code>
- *
- * @param length
- * the length at which to split this range into two
- * @return an array of two ranges, having the <code>length</code>-first
- * elements of this range, and the second range having the rest. If
- * <code>length</code> ≤ 0, the first element will be empty, and
- * the second element will be this range. If <code>length</code>
- * ≥ {@link #length()}, the first element will be this range,
- * and the second element will be empty.
- */
- public Range[] splitAtFromStart(final int length) {
- return splitAt(getStart() + length);
- }
-
- /**
- * Combines two ranges to create a range containing all values in both
- * ranges, provided there are no gaps between the ranges.
- *
- * @param other
- * the range to combine with this range
- *
- * @return the combined range
- *
- * @throws IllegalArgumentException
- * if the two ranges aren't connected
- */
- public Range combineWith(Range other) throws IllegalArgumentException {
- if (getStart() > other.getEnd() || other.getStart() > getEnd()) {
- throw new IllegalArgumentException(
- "There is a gap between " + this + " and " + other);
- }
-
- return Range.between(Math.min(getStart(), other.getStart()),
- Math.max(getEnd(), other.getEnd()));
- }
-
- /**
- * Creates a range that is expanded the given amounts in both ends.
- *
- * @param startDelta
- * the amount to expand by in the beginning of the range
- * @param endDelta
- * the amount to expand by in the end of the range
- *
- * @return an expanded range
- *
- * @throws IllegalArgumentException
- * if the new range would have <code>start > end</code>
- */
- public Range expand(int startDelta, int endDelta)
- throws IllegalArgumentException {
- return Range.between(getStart() - startDelta, getEnd() + endDelta);
- }
-
- /**
- * Limits this range to be within the bounds of the provided range.
- * <p>
- * This is basically an optimized way of calculating
- * <code>{@link #partitionWith(Range)}[1]</code> without the overhead of
- * defining the parts that do not overlap.
- * <p>
- * If the two ranges do not intersect, an empty range is returned. There are
- * no guarantees about the position of that range.
- *
- * @param bounds
- * the bounds that the returned range should be limited to
- * @return a bounded range
- */
- public Range restrictTo(Range bounds) {
- boolean startWithin = bounds.contains(getStart());
- boolean endWithin = bounds.contains(getEnd());
- boolean boundsWithin = getStart() < bounds.getStart()
- && getEnd() >= bounds.getEnd();
-
- if (startWithin) {
- if (endWithin) {
- return this;
- } else {
- return Range.between(getStart(), bounds.getEnd());
- }
- } else {
- if (endWithin) {
- return Range.between(bounds.getStart(), getEnd());
- } else if (boundsWithin) {
- return bounds;
- } else {
- return Range.withLength(getStart(), 0);
- }
- }
- }
-}
import org.junit.Test;
+import com.vaadin.shared.Range;
+
@SuppressWarnings("static-method")
public class RangeTest {
--- /dev/null
+package com.vaadin.tests.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.data.DataCommunicator;
+import com.vaadin.server.data.ListDataSource;
+import com.vaadin.server.data.Query;
+import com.vaadin.shared.data.DataCommunicatorConstants;
+import com.vaadin.shared.data.selection.SelectionModel;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.ui.AbstractListing;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.HorizontalLayout;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class DummyData extends AbstractTestUIWithLog {
+
+ /**
+ * DataSource that keeps track on how often the data is requested.
+ */
+ private class LoggingDataSource extends ListDataSource<String> {
+ private int count = 0;
+
+ private LoggingDataSource(Collection<String> collection) {
+ super(collection);
+ }
+
+ @Override
+ public Stream<String> apply(Query query) {
+ log("Backend request #" + (count++));
+ return super.apply(query);
+ }
+ }
+
+ /**
+ * Simplified server only selection model. Selection state passed in data,
+ * shown as bold text.
+ */
+ private static class DummySelectionModel implements SelectionModel<String> {
+ private String selected;
+ private DataCommunicator<String> communicator;
+
+ @Override
+ public Set<String> getSelectedItems() {
+ if (selected != null) {
+ return Collections.singleton(selected);
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ public void select(String item) {
+ if (selected != null) {
+ communicator.refresh(selected);
+ }
+ selected = item;
+ if (selected != null) {
+ communicator.refresh(selected);
+ }
+ }
+
+ @Override
+ public void deselect(String item) {
+ if (item == selected) {
+ select(null);
+ }
+ }
+
+ private void setCommunicator(DataCommunicator<String> dataComm) {
+ communicator = dataComm;
+ }
+ }
+
+ public static class DummyComponent
+ extends AbstractListing<String, DummySelectionModel> {
+
+ private DummyComponent() {
+ setSelectionModel(new DummySelectionModel());
+ addDataGenerator((str, json) -> {
+ json.put(DataCommunicatorConstants.DATA, str);
+ if (isSelected(str)) {
+ json.put(DataCommunicatorConstants.SELECTED, true);
+ }
+ });
+ getSelectionModel().setCommunicator(getDataCommunicator());
+ }
+ }
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ DummyComponent dummy = new DummyComponent();
+ List<String> items = new ArrayList<>();
+ for (int i = 0; i < 300; ++i) {
+ items.add("Foo " + i);
+ }
+ dummy.setDataSource(new LoggingDataSource(items));
+ dummy.select("Foo 200");
+
+ HorizontalLayout controls = new HorizontalLayout();
+ addComponent(controls);
+ controls.addComponent(new Button("Select Foo 20", e -> {
+ dummy.select("Foo " + 20);
+ }));
+ controls.addComponent(new Button("Reset data source", e -> {
+ dummy.setDataSource(new LoggingDataSource(items));
+ }));
+ addComponent(dummy);
+ }
+}
--- /dev/null
+package com.vaadin.tests.widgetset.client.data;
+
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.vaadin.client.connectors.AbstractListingConnector;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.VLabel;
+import com.vaadin.shared.data.DataCommunicatorConstants;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.tests.data.DummyData.DummyComponent;
+
+import elemental.json.JsonObject;
+
+@Connect(DummyComponent.class)
+public class DummyComponentConnector extends AbstractListingConnector {
+
+ @Override
+ public FlowPanel getWidget() {
+ return (FlowPanel) super.getWidget();
+ }
+
+ @Override
+ public void setDataSource(DataSource<JsonObject> dataSource) {
+ super.setDataSource(dataSource);
+
+ dataSource.addDataChangeHandler(range -> {
+ assert range.getStart() == 0 && range.getEnd() == dataSource
+ .size() : "Widget only supports full updates.";
+ getWidget().clear();
+ for (int i = range.getStart(); i < range.getEnd(); ++i) {
+ VLabel label = new VLabel();
+ getWidget().add(label);
+ JsonObject row = dataSource.getRow(i);
+ String text = row.getString(DataCommunicatorConstants.DATA);
+ if (row.hasKey(DataCommunicatorConstants.SELECTED)
+ && row.getBoolean(DataCommunicatorConstants.SELECTED)) {
+ text = "<b>" + text + "</b>";
+ label.addStyleName("selected");
+ }
+ label.setHTML(text);
+ }
+ });
+ }
+}
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
import com.vaadin.testbench.By;
import com.vaadin.testbench.ElementQuery;
import com.vaadin.testbench.TestBenchElement;
import org.openqa.selenium.WebElement;
import com.vaadin.client.WidgetUtil;
-import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.Range;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.NotificationElement;
import com.vaadin.testbench.parallel.BrowserUtil;
--- /dev/null
+package com.vaadin.tests.data;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.tests.tb3.SingleBrowserTest;
+
+public class DummyDataTest extends SingleBrowserTest {
+
+ @Before
+ public void setUp() {
+ setDebug(true);
+ openTestURL();
+ }
+
+ @Test
+ public void testCorrectRowSelectedOnInit() {
+ List<WebElement> selected = findElements(By.className("selected"));
+ assertTrue("Only one should be selected", 1 == selected.size());
+ assertEquals("Wrong item selected", "Foo 200",
+ selected.get(0).getText());
+ }
+
+ @Test
+ public void testServerSelectionUpdatesSelected() {
+ $(ButtonElement.class).first().click();
+ List<WebElement> selected = findElements(By.className("selected"));
+ assertTrue("Only one should be selected", 1 == selected.size());
+ assertEquals("Wrong item selected", "Foo 20",
+ selected.get(0).getText());
+ }
+
+ @Test
+ public void testDataUpdateDoesNotCauseBackEndRequest() {
+ assertEquals("Unexpected backend requests", "2. Backend request #1",
+ getLogRow(0));
+ assertEquals("Unexpected backend requests", "1. Backend request #0",
+ getLogRow(1));
+ // Select a row on the server-side, triggers an update
+ $(ButtonElement.class).first().click();
+ assertEquals("No requests should have happened",
+ "2. Backend request #1", getLogRow(0));
+ }
+
+ @Test
+ public void testDataSourceChangeOnlyOneRequest() {
+ // Change to a new logging data source
+ $(ButtonElement.class).get(1).click();
+ assertEquals("DataSource change should only cause 1 request",
+ "3. Backend request #0", getLogRow(0));
+ }
+}