| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.el.stream; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| import jakarta.el.ELException; |
| import jakarta.el.LambdaExpression; |
| |
| import org.apache.el.lang.ELArithmetic; |
| import org.apache.el.lang.ELSupport; |
| import org.apache.el.util.MessageFactory; |
| |
| /** |
| * A stream of elements supporting sequential operations such as filter, map, |
| * reduce, and various terminal operations. |
| */ |
| public class Stream { |
| |
| private final Iterator<Object> iterator; |
| |
| |
| /** |
| * Constructs a new Stream from the given iterator. |
| * |
| * @param iterator the iterator providing the stream elements |
| */ |
| public Stream(Iterator<Object> iterator) { |
| this.iterator = iterator; |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the elements that match the given predicate. |
| * |
| * @param le the predicate to apply to each element |
| * @return the new filtered stream |
| */ |
| public Stream filter(final LambdaExpression le) { |
| Iterator<Object> downStream = new OpIterator() { |
| @Override |
| protected void findNext() { |
| while (iterator.hasNext()) { |
| Object obj = iterator.next(); |
| if (ELSupport.coerceToBoolean(null, le.invoke(obj), true).booleanValue()) { |
| next = obj; |
| foundNext = true; |
| break; |
| } |
| } |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the results of applying the given function |
| * to the elements of this stream. |
| * |
| * @param le the function to apply to each element |
| * @return the new mapped stream |
| */ |
| public Stream map(final LambdaExpression le) { |
| Iterator<Object> downStream = new OpIterator() { |
| @Override |
| protected void findNext() { |
| if (iterator.hasNext()) { |
| Object obj = iterator.next(); |
| next = le.invoke(obj); |
| foundNext = true; |
| } |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the results of replacing each element of |
| * this stream with the contents of a stream produced by applying the provided |
| * function to each element. |
| * |
| * @param le the function to apply to each element which produces a new stream |
| * @return the new flattened stream |
| */ |
| public Stream flatMap(final LambdaExpression le) { |
| Iterator<Object> downStream = new OpIterator() { |
| |
| private Iterator<?> inner; |
| |
| @Override |
| protected void findNext() { |
| while (iterator.hasNext() || (inner != null && inner.hasNext())) { |
| if (inner == null || !inner.hasNext()) { |
| Object obj = le.invoke(iterator.next()); |
| if (obj instanceof Stream) { |
| inner = ((Stream) obj).iterator; |
| } |
| } |
| |
| if (inner != null && inner.hasNext()) { |
| next = inner.next(); |
| foundNext = true; |
| break; |
| } |
| } |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the distinct elements of this stream. |
| * |
| * @return the new stream with distinct elements |
| */ |
| public Stream distinct() { |
| Iterator<Object> downStream = new OpIterator() { |
| |
| private final Set<Object> values = new HashSet<>(); |
| |
| @Override |
| protected void findNext() { |
| while (iterator.hasNext()) { |
| Object obj = iterator.next(); |
| if (values.add(obj)) { |
| next = obj; |
| foundNext = true; |
| break; |
| } |
| } |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the elements of this stream, sorted |
| * according to natural order. |
| * |
| * @return the new sorted stream |
| */ |
| public Stream sorted() { |
| Iterator<Object> downStream = new OpIterator() { |
| |
| private Iterator<Object> sorted = null; |
| |
| @Override |
| protected void findNext() { |
| if (sorted == null) { |
| sort(); |
| } |
| if (sorted.hasNext()) { |
| next = sorted.next(); |
| foundNext = true; |
| } |
| } |
| |
| @SuppressWarnings({ "rawtypes", "unchecked" }) |
| private void sort() { |
| List list = new ArrayList<>(); |
| while (iterator.hasNext()) { |
| list.add(iterator.next()); |
| } |
| Collections.sort(list); |
| sorted = list.iterator(); |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the elements of this stream, sorted |
| * according to the order induced by the provided comparator. |
| * |
| * @param le the comparator to determine sort order |
| * @return the new sorted stream |
| */ |
| public Stream sorted(final LambdaExpression le) { |
| Iterator<Object> downStream = new OpIterator() { |
| |
| private Iterator<Object> sorted = null; |
| |
| @Override |
| protected void findNext() { |
| if (sorted == null) { |
| sort(le); |
| } |
| if (sorted.hasNext()) { |
| next = sorted.next(); |
| foundNext = true; |
| } |
| } |
| |
| @SuppressWarnings({ "rawtypes", "unchecked" }) |
| private void sort(LambdaExpression le) { |
| List list = new ArrayList<>(); |
| Comparator<Object> c = new LambdaExpressionComparator(le); |
| while (iterator.hasNext()) { |
| list.add(iterator.next()); |
| } |
| list.sort(c); |
| sorted = list.iterator(); |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Performs an action for each element of this stream. |
| * |
| * @param le the action to perform for each element |
| * @return null |
| */ |
| public Object forEach(final LambdaExpression le) { |
| while (iterator.hasNext()) { |
| le.invoke(iterator.next()); |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the elements of this stream, additionally |
| * performing the provided action on each element as elements are consumed. |
| * |
| * @param le the action to perform on each element |
| * @return the new stream |
| */ |
| public Stream peek(final LambdaExpression le) { |
| Iterator<Object> downStream = new OpIterator() { |
| @Override |
| protected void findNext() { |
| if (iterator.hasNext()) { |
| Object obj = iterator.next(); |
| le.invoke(obj); |
| next = obj; |
| foundNext = true; |
| } |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Returns the underlying iterator for this stream. |
| * |
| * @return the underlying iterator |
| */ |
| public Iterator<?> iterator() { |
| return iterator; |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the first {@code count} elements of this stream. |
| * |
| * @param count the number of elements to limit to |
| * @return the new limited stream |
| */ |
| public Stream limit(final Number count) { |
| return substream(Integer.valueOf(0), count); |
| } |
| |
| |
| /** |
| * Returns a stream consisting of the elements of this stream from the |
| * specified start position to the end. |
| * |
| * @param start the start position (inclusive) |
| * @return the new substream |
| */ |
| public Stream substream(final Number start) { |
| return substream(start, Integer.valueOf(Integer.MAX_VALUE)); |
| } |
| |
| /** |
| * Returns a stream consisting of the elements of this stream from the |
| * specified start position up to, but not including, the specified end position. |
| * |
| * @param start the start position (inclusive) |
| * @param end the end position (exclusive) |
| * @return the new substream |
| */ |
| public Stream substream(final Number start, final Number end) { |
| |
| Iterator<Object> downStream = new OpIterator() { |
| |
| private final int startPos = start.intValue(); |
| private final int endPos = end.intValue(); |
| private int itemCount = 0; |
| |
| @Override |
| protected void findNext() { |
| while (itemCount < startPos && iterator.hasNext()) { |
| iterator.next(); |
| itemCount++; |
| } |
| if (itemCount < endPos && iterator.hasNext()) { |
| itemCount++; |
| next = iterator.next(); |
| foundNext = true; |
| } |
| } |
| }; |
| return new Stream(downStream); |
| } |
| |
| |
| /** |
| * Returns a list containing all the elements of this stream. |
| * |
| * @return the list of elements |
| */ |
| public List<Object> toList() { |
| List<Object> result = new ArrayList<>(); |
| while (iterator.hasNext()) { |
| result.add(iterator.next()); |
| } |
| return result; |
| } |
| |
| |
| /** |
| * Returns an array containing all the elements of this stream. |
| * |
| * @return the array of elements |
| */ |
| public Object[] toArray() { |
| List<Object> result = new ArrayList<>(); |
| while (iterator.hasNext()) { |
| result.add(iterator.next()); |
| } |
| return result.toArray(new Object[0]); |
| } |
| |
| |
| /** |
| * Performs a reduction on the elements of this stream, using the provided |
| * accumulator function. The first element is used as the initial value. |
| * |
| * @param le the accumulator function |
| * @return an Optional describing the reduced value, or empty if this stream is empty |
| */ |
| public Optional reduce(LambdaExpression le) { |
| Object seed = null; |
| |
| if (iterator.hasNext()) { |
| seed = iterator.next(); |
| } else { |
| return Optional.EMPTY; |
| } |
| |
| return new Optional(reduce(seed, le)); |
| } |
| |
| |
| /** |
| * Performs a reduction on the elements of this stream, using the provided |
| * initial value and accumulator function. |
| * |
| * @param seed the initial value for the reduction |
| * @param le the accumulator function |
| * @return the result of the reduction |
| */ |
| public Object reduce(Object seed, LambdaExpression le) { |
| Object result = seed; |
| |
| while (iterator.hasNext()) { |
| result = le.invoke(result, iterator.next()); |
| } |
| |
| return result; |
| } |
| |
| |
| /** |
| * Returns the maximum element of this stream according to natural order. |
| * |
| * @return an Optional describing the maximum element, or empty if this stream is empty |
| */ |
| public Optional max() { |
| return compare(true); |
| } |
| |
| |
| /** |
| * Returns the maximum element of this stream according to the provided comparator. |
| * |
| * @param le the comparator to determine the maximum |
| * @return an Optional describing the maximum element, or empty if this stream is empty |
| */ |
| public Optional max(LambdaExpression le) { |
| return compare(true, le); |
| } |
| |
| |
| /** |
| * Returns the minimum element of this stream according to natural order. |
| * |
| * @return an Optional describing the minimum element, or empty if this stream is empty |
| */ |
| public Optional min() { |
| return compare(false); |
| } |
| |
| |
| /** |
| * Returns the minimum element of this stream according to the provided comparator. |
| * |
| * @param le the comparator to determine the minimum |
| * @return an Optional describing the minimum element, or empty if this stream is empty |
| */ |
| public Optional min(LambdaExpression le) { |
| return compare(false, le); |
| } |
| |
| |
| /** |
| * Calculates the average of all numeric elements in the stream. |
| * |
| * @return an Optional containing the average, or empty if the stream is empty |
| */ |
| public Optional average() { |
| long count = 0; |
| Number sum = Long.valueOf(0); |
| |
| while (iterator.hasNext()) { |
| count++; |
| sum = ELArithmetic.add(sum, iterator.next()); |
| } |
| |
| if (count == 0) { |
| return Optional.EMPTY; |
| } else { |
| return new Optional(ELArithmetic.divide(sum, Long.valueOf(count))); |
| } |
| } |
| |
| |
| /** |
| * Returns the sum of all numeric elements in the stream. |
| * |
| * @return the sum of all elements |
| */ |
| public Number sum() { |
| Number sum = Long.valueOf(0); |
| |
| while (iterator.hasNext()) { |
| sum = ELArithmetic.add(sum, iterator.next()); |
| } |
| |
| return sum; |
| } |
| |
| |
| /** |
| * Counts the number of elements in the stream. |
| * |
| * @return the number of elements |
| */ |
| public Long count() { |
| long count = 0; |
| |
| while (iterator.hasNext()) { |
| iterator.next(); |
| count++; |
| } |
| |
| return Long.valueOf(count); |
| } |
| |
| |
| /** |
| * Checks if any element in the stream matches the given predicate. |
| * |
| * @param le the predicate lambda expression |
| * @return an Optional containing true if any element matches, or empty if the stream is empty |
| */ |
| public Optional anyMatch(LambdaExpression le) { |
| if (!iterator.hasNext()) { |
| return Optional.EMPTY; |
| } |
| |
| Boolean match = Boolean.FALSE; |
| |
| while (!match.booleanValue() && iterator.hasNext()) { |
| match = (Boolean) le.invoke(iterator.next()); |
| } |
| |
| return new Optional(match); |
| } |
| |
| |
| /** |
| * Checks if all elements in the stream match the given predicate. |
| * |
| * @param le the predicate lambda expression |
| * @return an Optional containing true if all elements match, or empty if the stream is empty |
| */ |
| public Optional allMatch(LambdaExpression le) { |
| if (!iterator.hasNext()) { |
| return Optional.EMPTY; |
| } |
| |
| Boolean match = Boolean.TRUE; |
| |
| while (match.booleanValue() && iterator.hasNext()) { |
| match = (Boolean) le.invoke(iterator.next()); |
| } |
| |
| return new Optional(match); |
| } |
| |
| |
| /** |
| * Checks if no elements of this stream match the given predicate. |
| * |
| * @param le the predicate lambda expression |
| * @return an Optional containing true if no elements match, or empty if the stream is empty |
| */ |
| public Optional noneMatch(LambdaExpression le) { |
| if (!iterator.hasNext()) { |
| return Optional.EMPTY; |
| } |
| |
| Boolean match = Boolean.FALSE; |
| |
| while (!match.booleanValue() && iterator.hasNext()) { |
| match = (Boolean) le.invoke(iterator.next()); |
| } |
| |
| return new Optional(Boolean.valueOf(!match.booleanValue())); |
| } |
| |
| |
| /** |
| * Returns an Optional describing the first element of this stream, |
| * or an empty Optional if the stream is empty. |
| * |
| * @return an Optional describing the first element of this stream |
| */ |
| public Optional findFirst() { |
| if (iterator.hasNext()) { |
| return new Optional(iterator.next()); |
| } else { |
| return Optional.EMPTY; |
| } |
| } |
| |
| |
| @SuppressWarnings({ "rawtypes", "unchecked" }) |
| private Optional compare(boolean isMax) { |
| Comparable result = null; |
| |
| if (iterator.hasNext()) { |
| Object obj = iterator.next(); |
| if ((obj instanceof Comparable)) { |
| result = (Comparable) obj; |
| } else { |
| throw new ELException(MessageFactory.get("stream.compare.notComparable")); |
| } |
| } |
| |
| while (iterator.hasNext()) { |
| Object obj = iterator.next(); |
| if ((obj instanceof Comparable)) { |
| if (isMax && ((Comparable) obj).compareTo(result) > 0) { |
| result = (Comparable) obj; |
| } else if (!isMax && ((Comparable) obj).compareTo(result) < 0) { |
| result = (Comparable) obj; |
| } |
| } else { |
| throw new ELException(MessageFactory.get("stream.compare.notComparable")); |
| } |
| } |
| |
| if (result == null) { |
| return Optional.EMPTY; |
| } else { |
| return new Optional(result); |
| } |
| } |
| |
| |
| private Optional compare(boolean isMax, LambdaExpression le) { |
| Object result = null; |
| |
| if (iterator.hasNext()) { |
| result = iterator.next(); |
| } |
| |
| while (iterator.hasNext()) { |
| Object obj = iterator.next(); |
| if (isMax && ELSupport.coerceToNumber(null, le.invoke(obj, result), Integer.class).intValue() > 0) { |
| result = obj; |
| } else if (!isMax && ELSupport.coerceToNumber(null, le.invoke(obj, result), Integer.class).intValue() < 0) { |
| result = obj; |
| } |
| } |
| |
| if (result == null) { |
| return Optional.EMPTY; |
| } else { |
| return new Optional(result); |
| } |
| } |
| |
| |
| private record LambdaExpressionComparator(LambdaExpression le) implements Comparator<Object> { |
| @Override |
| public int compare(Object o1, Object o2) { |
| return ELSupport.coerceToNumber(null, le.invoke(o1, o2), Integer.class).intValue(); |
| } |
| } |
| |
| |
| private abstract static class OpIterator implements Iterator<Object> { |
| protected boolean foundNext = false; |
| protected Object next; |
| |
| @Override |
| public boolean hasNext() { |
| if (foundNext) { |
| return true; |
| } |
| findNext(); |
| return foundNext; |
| } |
| |
| @Override |
| public Object next() { |
| if (foundNext) { |
| foundNext = false; |
| return next; |
| } |
| findNext(); |
| if (foundNext) { |
| foundNext = false; |
| return next; |
| } else { |
| throw new NoSuchElementException(); |
| } |
| } |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected abstract void findNext(); |
| } |
| } |