/* * Copyright 2013-2014 the original author or authors. * * 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 hr.helix.sqlstream; import groovy.lang.Closure; import groovy.sql.GroovyResultSet; import groovy.sql.GroovyResultSetProxy; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; /** * Wraps the {@code java.sql.ResultSet} and exposes some common collection methods * which are lazily evaluated. Iterates through the {@code ResultSet} only once, * invoking given methods for each row of the result set. The row will be a * {@code GroovyResultSet} which is a {@code ResultSet} that supports accessing the * fields using property style notation and ordinal index values. * * @author Dinko Srkoč * @author Alberto Vilches * @author Adam Sernheim * @since 2013-10-30 */ public class StreamingResultSet { private ResultSet rs; private Fn compute; private List<Object> values; private static class Fn implements Cloneable { private Fn next; public Value call(Value v) { return apply(v); } // override this protected final Value apply(Value v) { return next != null ? next.call(v) : v; } public Fn andThen(Fn that) { if (next == null) next = that; else next.andThen(that); return this; } /** makes deep copy of this Fn object */ @Override protected final Fn clone() { Fn cloned; try { cloned = (Fn) super.clone(); } catch (CloneNotSupportedException e) { return null; } if (next != null) { cloned.next = next.clone(); } return cloned; } } private static class Collect<T> extends Fn { private Closure<T> f; public Collect(Closure<T> f) { this.f = f; } @Override public Value call(Value v) { return apply(new Value(f.call(v.getValue()))); } } private static class CollectMany<T extends Collection> extends Fn { private Closure<T> f; public CollectMany(Closure<T> f) { this.f = f; } @Override public Value call(Value v) { ArrayList<Object> vals = new ArrayList<Object>(); for (Object o : f.call(v.getValue())) { final Value val = apply(new Value(o)); if (val.terminate()) return new TerminateWithValue(new FlatValue(vals)); val.exportTo(vals); } return new FlatValue(vals); } } private static class FindAll extends Fn { private Closure<Boolean> p; public FindAll(Closure<Boolean> p) { this.p = p; } @Override public Value call(Value v) { return p.call(v.getValue()) ? apply(v) : IgnoreValue.INSTANCE; } } private static class FindResults extends Fn { private Closure<?> p; public FindResults(Closure<?> p) { this.p = p; } @Override public Value call(Value v) { Object newVal = p.call(v.getValue()); return newVal == null ? IgnoreValue.INSTANCE : apply(new Value(newVal)); } } private static class Find extends FindAll { public Find(Closure<Boolean> p) { super(p); andThen(new Head()); } } private static class FindResult extends FindResults { public FindResult(Closure<?> p) { super(p); andThen(new Head()); } } private static class Any extends Fn { private Closure<Boolean> p; public Any(Closure<Boolean> p) { this.p = p; } @Override public Value call(Value v) { return p.call(v.getValue()) ? TerminateWithValue.TRUE : IgnoreValue.INSTANCE; } } private static class Every extends Fn { private Closure<Boolean> p; public Every(Closure<Boolean> p) { this.p = p; } @Override public Value call(Value v) { return p.call(v.getValue()) ? IgnoreValue.INSTANCE : TerminateWithValue.FALSE; } } private static class ContainsAll extends Fn { private Collection<?> items; public ContainsAll(Collection<?> items) { this.items = new ArrayList<Object>(items); } @Override public Value call(Value v) { if (items.contains(v.getValue())) { items.remove(v.getValue()); // Every stream item found in the item collection is removed } return items.isEmpty() ? TerminateWithValue.TRUE : IgnoreValue.INSTANCE; } } private static class Contains extends Fn { private Object item; public Contains(Object item) { this.item = item; } @Override public Value call(Value v) { return item == v.getValue() || (item != null && item.equals(v.getValue())) ? TerminateWithValue.TRUE : IgnoreValue.INSTANCE; } } private static class Each extends Fn { private Closure<Object> f; public Each(Closure<Object> f) { this.f = f; } @Override public Value call(Value v) { f.call(v.getValue()); return apply(v); } } private static class Take extends Fn { private int n; public Take(int n) { this.n = n; } @Override public Value call(Value v) { if (n == 0) { return TerminateEmpty.INSTANCE; } else { --n; return apply(v); } } } private static class TakeWhile extends Fn { private Closure<Boolean> p; private boolean done = false; public TakeWhile(Closure<Boolean> p) { this.p = p; } @Override public Value call(Value v) { if (done || (done = !p.call(v.getValue()))) { return TerminateEmpty.INSTANCE; } else { return apply(v); } } } private static class Drop extends Fn { private int n; public Drop(int n) { this.n = n; } @Override public Value call(Value v) { if (n == 0) { return apply(v); } else { --n; return IgnoreValue.INSTANCE; } } } private static class DropWhile extends Fn { private Closure<Boolean> p; private boolean done = false; public DropWhile(Closure<Boolean> p) { this.p = p; } @Override public Value call(Value v) { if (done || (done = !p.call(v.getValue()))) { return apply(v); } else { return IgnoreValue.INSTANCE; } } } private static class Inject<R> extends Fn { private Closure<R> fn; private R accu; public Inject(R zero, Closure<R> fn) { this.accu = zero; this.fn = fn; } @Override public Value call(Value v) { accu = fn.call(accu, v.getValue()); return apply(new SingleValue(accu)); } } private static class Head extends Fn { @Override public Value call(Value v) { return v.ignore() ? v : new TerminateWithValue(v); } } // todo proučiti mogućnost generifikacije Value private static class Value { private Object value; public Object getValue() { return value; } protected Value() {} public Value(Object val) { value = val; } public void exportTo(List<Object> xs) { xs.add(value); } public boolean terminate() { return false; } public boolean ignore() { return false; } } private static class SingleValue extends Value { public SingleValue(Object val) { super(val); } @Override public void exportTo(List<Object> xs) { if (xs.isEmpty()) xs.add(getValue()); else xs.set(0, getValue()); } } private static class FlatValue extends Value { private final Collection<Object> values; public FlatValue(List<Object> vals) { values = vals; } @Override public void exportTo(List<Object> xs) { xs.addAll(values); } } private static class IgnoreValue extends Value { public static final IgnoreValue INSTANCE = new IgnoreValue(); @Override public void exportTo(List<Object> xs) {} @Override public boolean ignore() { return true; } } private static class TerminateEmpty extends IgnoreValue { public static final TerminateEmpty INSTANCE = new TerminateEmpty(); @Override public boolean terminate() { return true; } } private static class TerminateWithValue extends Value { private final Value v; public static final TerminateWithValue TRUE = new TerminateWithValue(new Value(Boolean.TRUE)); public static final TerminateWithValue FALSE = new TerminateWithValue(new Value(Boolean.FALSE)); public TerminateWithValue(Value v) { this.v = v; } @Override public boolean terminate() { return true; } @Override public boolean ignore() { return true; } @Override public void exportTo(List<Object> xs) { v.exportTo(xs); } } private StreamingResultSet(ResultSet rs, Fn fn) { this.rs = rs; this.compute = fn; } /** * Creates a new {@code StreamingResultSet} instance that wraps the {@code ResultSet}. * * @param rs ResultSet that is wrapped by the newly created StreamingResultSet * @return new instance of StreamingResultSet */ public static StreamingResultSet from(ResultSet rs) { return new StreamingResultSet(rs, new Fn()); } /** * Iterates through the stream transforming each element into a new value * using Closure {@code f}. * * @param f the Closure used to transform each element of the stream * @return new {@code StreamingResultSet} instance */ public <T> StreamingResultSet collect(Closure<T> f) { return next(new Collect<T>(f)); } /** * Iterates through the stream transforming each element to a collection and * concatenates (flattens) the resulting collections into a single list. * * @param f the Closure used to transform each element of the stream * @param <T> the collection type that Closure {@code f} returns * @return new {@code StreamingResultSet} instance */ public <T extends Collection> StreamingResultSet collectMany(Closure<T> f) { return next(new CollectMany<T>(f)); } /** * Finds all elements matching the given Closure predicate. * * @param p the Closure that must evaluate to {@code true} for element to be taken * @return new {@code StreamingResultSet} instance */ public StreamingResultSet findAll(Closure<Boolean> p) { return next(new FindAll(p)); } public StreamingResultSet findResults(Closure<?> p) { return next(new FindResults(p)); } /** * Finds the first element matching the given Closure predicate. * The result is equivalent to {@code findAll} operation followed by {@code head}. * * @param p the Closure that must evaluate to {@code true} for element to be taken * @return the first element matching the Closure predicate */ public Object find(Closure<Boolean> p) throws SQLException { if (values != null) return DefaultGroovyMethods.find(values, p); return terminate(new Find(p), null); } public Object findResult(Closure<?> p) throws SQLException { // todo better parameter name if (values != null) return DefaultGroovyMethods.findResult(values, p); return terminate(new FindResult(p), null); } /** * Iterates through the stream passing each element to the given Closure {@code f}. * * @param f the Closure applied to each element found * @return new {@code StreamingResultSet} instance */ public StreamingResultSet each(Closure<Object> f) { return next(new Each(f)); } /** * Takes the first {@code n} elements from the head of the stream. * * @param n the number of elements to take from the stream * @return new {@code StreamingResultSet} instance */ public StreamingResultSet take(int n) { return next(new Take(n)); } /** * Drops the given number of elements from the head of the stream if available. * * @param n the number of elements to drop from the stream * @return new {@code StreamingResultSet} instance */ public StreamingResultSet drop(int n) { return next(new Drop(n)); } /** * Takes the longest prefix of the stream where each element passed to the * given Closure predicate evaluates to {@code true}. * * @param p the Closure that must evaluate to {@code true} to continue taking elements * @return new {@code StreamingResultSet} instance */ public StreamingResultSet takeWhile(Closure<Boolean> p) { return next(new TakeWhile(p)); } /** * Returns a suffix of the stream where elements are dropped from the front while the * given Closure predicate evaluates to {@code true}. * * @param p the predicate that must evaluate to {@code true} to continue dropping elements * @return new {@code StreamingResultSet} instance */ public StreamingResultSet dropWhile(Closure<Boolean> p) { return next(new DropWhile(p)); } public <T> T inject(T zero, Closure<T> fn) throws SQLException { return (T) terminate(new Inject<T>(zero, fn), zero); } /** * Selects the first element of the stream. * <p> * <em>Note</em>: in order to read the first element {@code head()} has to start * processing the result set. {@code StreamingResultSet} can be used again, but, * currently, it will process the result set from the beginning. This means it will * have to return the cursor ({@code ResultSet#beforeFirst()}) before processing. * This will throw {@code SQLException} if the result set type is {@code TYPE_FORWARD_ONLY}. * </p> * <strong>Example</strong> * <pre> * // be careful about result set type if stream is forced more than once * sql.resultSetType = ResultSet.TYPE_SCROLL_INSENSITIVE * * def result = sql.withStream('SELECT * FROM a_table') { StreamingResultSet stream -> * // calls ResultSet#next() to read the first element * def h = stream.head() * * // calls ResultSet#beforeFirst() to start processing from the beginning * def xs = stream.collect { it.col_a }.toList() * h + xs.sum() * } * </pre> * * @return the first element of this stream */ public Object head() throws SQLException { if (values != null) return values.get(0); StreamingResultSet srs = new StreamingResultSet(rs, compute.clone().andThen(new Head())); //next(new Head()); return srs.toList().get(0); } /** * Iterates over the stream and checks whether the predicate is valid for at least one element. * * <p><strong>Example</strong></p> * <pre> * def result = sql.withStream('SELECT * FROM a_table') { StreamingResultSet stream -> * boolean atLeastOneEvenElement = stream.any { * it.col_a % 2 == 0 * } * } * </pre> * * @param p the Closure predicate that must evaluate to {@code true} at least once * for this method to return {@code true} * @return true if any item in the stream matches the Closure predicate * @throws SQLException if database access error occurs */ public boolean any(Closure<Boolean> p) throws SQLException { if (values != null) return DefaultGroovyMethods.any(values, p); return terminateBool(new Any(p), Boolean.FALSE); } /** * Iterates over the stream and check if the predicate is valid for all the elements * (i.e. returns {@code true} for all items in this data structure). * * <p><strong>Example</strong></p> * <pre> * def result = sql.withStream('SELECT * FROM a_table') { StreamingResultSet stream -> * boolean areAllElementsEven = stream.every { * it.col_a % 2 == 0 * } * } * </pre> * * @param p the Closure predicate that must evaluate to {@code true} for each stream element * for this method to return {@code true} * @return true if all items in the stream match the Closure predicate * @throws SQLException if database access error occurs */ public boolean every(Closure<Boolean> p) throws SQLException { if (values != null) return DefaultGroovyMethods.every(values, p); return terminateBool(new Every(p), Boolean.TRUE); } /** * Checks if the stream contains all the elements in the specified array. * * <p><strong>Example</strong></p> * <pre> * def result = sql.withStream('SELECT * FROM a_table') { StreamingResultSet stream -> * boolean areAllElementsEven = stream.collect { * it.col_a * }.containsAll([1,2,10]) * } * </pre> * * @param items array to be checked for containment in this stream. * @return true if the stream contains all the elements. * @throws SQLException if database access error occurs */ public boolean containsAll(Object[] items) throws SQLException { return containsAll(Arrays.asList(items)); } /** * Checks if the stream contains all the elements in the specified collection. * * @param items collection to be checked for containment in this stream. * @return true if the stream contains all the elements. * @throws SQLException if database access error occurs */ public boolean containsAll(Collection<?> items) throws SQLException { if (values != null) return values.containsAll(items); return terminateBool(new ContainsAll(items), Boolean.FALSE); } /** * Checks if the stream contains the given item. * * @param item item to be checked for containment in this stream. * @return true if the stream contains the given item. * @throws SQLException if database access error occurs */ public boolean contains(Object item) throws SQLException { if (values != null) return values.contains(item); return terminateBool(new Contains(item), Boolean.FALSE); } /** * Selects all elements except the head of the stream. * <p> * This is an alias for {@code drop(1)} * </p> * * @return new {@code StreamingResultSet} instance with all the elements of * this stream except the first one */ public StreamingResultSet tail() { return drop(1); } private StreamingResultSet next(Fn that) { return new StreamingResultSet(rs, compute.andThen(that)); } /** * Terminating functions should end with an empty stream or with a single element stream. * This method returns either the value from the stream or the default value if the stream is empty. * * @param that terminating function that is invoked at the end of {@code compute} chain * @param defaultIfEmpty default value to be returned if realized stream is empty * @return the first element of the realized stream or {@code defaultIfEmpty} if the stream is empty * @throws SQLException if database access error occurs */ private Object terminate(Fn that, Object defaultIfEmpty) throws SQLException { StreamingResultSet srs = new StreamingResultSet(rs, compute.clone().andThen(that)); List results = srs.toList(); return results.isEmpty() ? defaultIfEmpty : results.get(0); } /** * Boolean terminating functions should end with an empty stream or with a single element stream * with a boolean value. This method returns either the boolean value from the stream or the default * value if the stream is empty. * * @param that terminating function that is invoked at the end of {@code compute} chain * @param defaultIfEmpty default boolean value to be returned if realized stream is empty * @return the first element of the realized stream or {@code defaultIfEmpty} if the stream is empty * @throws SQLException if database access error occurs */ private Boolean terminateBool(Fn that, Boolean defaultIfEmpty) throws SQLException { return (Boolean) terminate(that, defaultIfEmpty); } /** * Realizes the stream if it is not yet realized. The stream is realized * by iterating over the result set and applying all the given operations. * The resulting entries are collected into a list which can be accessed * by applying {@link StreamingResultSet#toList()} to this {@code StreamingResultSet}. * * @return this {@code StreamingResultSet} that is forced into realization * @throws SQLException if database access error occurs */ public StreamingResultSet force() throws SQLException { if (values != null) return this; // stream is already realized values = new ArrayList<Object>(); GroovyResultSet groovyRS = new GroovyResultSetProxy(rs).getImpl(); // if several instances of this stream are forced then each should start at the beginning // CAUTION: beforeFirst() throws SQLException if ResultSet type is TYPE_FORWARD_ONLY! if (!groovyRS.isBeforeFirst()) groovyRS.beforeFirst(); while (groovyRS.next()) { Value v = compute.call(new Value(groovyRS)); v.exportTo(values); if (v.terminate()) break; } return this; } /** * Returns the list of values computed by the given transformations. * Forces the stream realization if needed. * * @return the list of computed values * @throws SQLException if database access error occurs * @see StreamingResultSet#force() */ public List<Object> toList() throws SQLException { return force().values; } }