8072722: add stream support to Scanner
authorsmarks
Wed, 16 Sep 2015 16:24:35 -0700
changeset 32650 174fd1116b9d
parent 32649 2ee9017c7597
child 32651 25b6082a4eb0
8072722: add stream support to Scanner Reviewed-by: psandoz, chegar, sherman
jdk/src/java.base/share/classes/java/util/Scanner.java
jdk/test/java/util/Scanner/ScanTest.java
jdk/test/java/util/Scanner/ScannerStreamTest.java
--- a/jdk/src/java.base/share/classes/java/util/Scanner.java	Tue Sep 15 21:56:04 2015 -0700
+++ b/jdk/src/java.base/share/classes/java/util/Scanner.java	Wed Sep 16 16:24:35 2015 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2015, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,16 +25,18 @@
 
 package java.util;
 
-import java.nio.file.Path;
-import java.nio.file.Files;
-import java.util.regex.*;
 import java.io.*;
 import java.math.*;
 import java.nio.*;
 import java.nio.channels.*;
 import java.nio.charset.*;
+import java.nio.file.Path;
+import java.nio.file.Files;
 import java.text.*;
-import java.util.Locale;
+import java.util.function.Consumer;
+import java.util.regex.*;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 import sun.misc.LRUCache;
 
@@ -96,22 +98,25 @@
  * }</pre></blockquote>
  *
  * <p>The <a name="default-delimiter">default whitespace delimiter</a> used
- * by a scanner is as recognized by {@link java.lang.Character}.{@link
- * java.lang.Character#isWhitespace(char) isWhitespace}. The {@link #reset}
+ * by a scanner is as recognized by {@link Character#isWhitespace(char)
+ * Character.isWhitespace()}. The {@link #reset reset()}
  * method will reset the value of the scanner's delimiter to the default
  * whitespace delimiter regardless of whether it was previously changed.
  *
  * <p>A scanning operation may block waiting for input.
  *
  * <p>The {@link #next} and {@link #hasNext} methods and their
- * primitive-type companion methods (such as {@link #nextInt} and
+ * companion methods (such as {@link #nextInt} and
  * {@link #hasNextInt}) first skip any input that matches the delimiter
- * pattern, and then attempt to return the next token. Both {@code hasNext}
- * and {@code next} methods may block waiting for further input.  Whether a
- * {@code hasNext} method blocks has no connection to whether or not its
- * associated {@code next} method will block.
+ * pattern, and then attempt to return the next token. Both {@code hasNext()}
+ * and {@code next()} methods may block waiting for further input.  Whether a
+ * {@code hasNext()} method blocks has no connection to whether or not its
+ * associated {@code next()} method will block. The {@link #tokens} method
+ * may also block waiting for input.
  *
- * <p> The {@link #findInLine}, {@link #findWithinHorizon}, and {@link #skip}
+ * <p>The {@link #findInLine findInLine()},
+ * {@link #findWithinHorizon findWithinHorizon()},
+ * {@link #skip skip()}, and {@link #findAll findAll()}
  * methods operate independently of the delimiter pattern. These methods will
  * attempt to match the specified pattern with no regard to delimiters in the
  * input and thus can be used in special circumstances where delimiters are
@@ -129,7 +134,7 @@
  *
  * <p> A scanner can read text from any object which implements the {@link
  * java.lang.Readable} interface.  If an invocation of the underlying
- * readable's {@link java.lang.Readable#read} method throws an {@link
+ * readable's {@link java.lang.Readable#read read()} method throws an {@link
  * java.io.IOException} then the scanner assumes that the end of the input
  * has been reached.  The most recent {@code IOException} thrown by the
  * underlying readable can be retrieved via the {@link #ioException} method.
@@ -156,7 +161,7 @@
  * <a name="initial-locale">initial locale </a>is the value returned by the {@link
  * java.util.Locale#getDefault(Locale.Category)
  * Locale.getDefault(Locale.Category.FORMAT)} method; it may be changed via the {@link
- * #useLocale} method. The {@link #reset} method will reset the value of the
+ * #useLocale useLocale()} method. The {@link #reset} method will reset the value of the
  * scanner's locale to the initial locale regardless of whether it was
  * previously changed.
  *
@@ -374,6 +379,11 @@
     // A holder of the last IOException encountered
     private IOException lastException;
 
+    // Number of times this scanner's state has been modified.
+    // Generally incremented on most public APIs and checked
+    // within spliterator implementations.
+    int modCount;
+
     // A pattern for java whitespace
     private static Pattern WHITESPACE_PATTERN = Pattern.compile(
                                                 "\\p{javaWhitespace}+");
@@ -995,8 +1005,9 @@
     }
 
     // Finds the specified pattern in the buffer up to horizon.
-    // Returns a match for the specified input pattern.
-    private String findPatternInBuffer(Pattern pattern, int horizon) {
+    // Returns true if the specified input pattern was matched,
+    // and leaves the matcher field with the current match state.
+    private boolean findPatternInBuffer(Pattern pattern, int horizon) {
         matchValid = false;
         matcher.usePattern(pattern);
         int bufferLimit = buf.limit();
@@ -1014,7 +1025,7 @@
                 if (searchLimit != horizonLimit) {
                      // Hit an artificial end; try to extend the match
                     needInput = true;
-                    return null;
+                    return false;
                 }
                 // The match could go away depending on what is next
                 if ((searchLimit == horizonLimit) && matcher.requireEnd()) {
@@ -1022,27 +1033,28 @@
                     // that it is at the horizon and the end of input is
                     // required for the match.
                     needInput = true;
-                    return null;
+                    return false;
                 }
             }
             // Did not hit end, or hit real end, or hit horizon
             position = matcher.end();
-            return matcher.group();
+            return true;
         }
 
         if (sourceClosed)
-            return null;
+            return false;
 
         // If there is no specified horizon, or if we have not searched
         // to the specified horizon yet, get more input
         if ((horizon == 0) || (searchLimit != horizonLimit))
             needInput = true;
-        return null;
+        return false;
     }
 
-    // Returns a match for the specified input pattern anchored at
-    // the current position
-    private String matchPatternInBuffer(Pattern pattern) {
+    // Attempts to match a pattern anchored at the current position.
+    // Returns true if the specified input pattern was matched,
+    // and leaves the matcher field with the current match state.
+    private boolean matchPatternInBuffer(Pattern pattern) {
         matchValid = false;
         matcher.usePattern(pattern);
         matcher.region(position, buf.limit());
@@ -1050,18 +1062,18 @@
             if (matcher.hitEnd() && (!sourceClosed)) {
                 // Get more input and try again
                 needInput = true;
-                return null;
+                return false;
             }
             position = matcher.end();
-            return matcher.group();
+            return true;
         }
 
         if (sourceClosed)
-            return null;
+            return false;
 
         // Read more to find pattern
         needInput = true;
-        return null;
+        return false;
     }
 
     // Throws if the scanner is closed
@@ -1128,6 +1140,7 @@
      * @return this scanner
      */
     public Scanner useDelimiter(Pattern pattern) {
+        modCount++;
         delimPattern = pattern;
         return this;
     }
@@ -1147,6 +1160,7 @@
      * @return this scanner
      */
     public Scanner useDelimiter(String pattern) {
+        modCount++;
         delimPattern = patternCache.forName(pattern);
         return this;
     }
@@ -1181,6 +1195,7 @@
         if (locale.equals(this.locale))
             return this;
 
+        modCount++;
         this.locale = locale;
         DecimalFormat df =
             (DecimalFormat)NumberFormat.getNumberInstance(locale);
@@ -1236,8 +1251,8 @@
      * number matching regular expressions; see
      * <a href= "#localized-numbers">localized numbers</a> above.
      *
-     * <p>If the radix is less than {@code Character.MIN_RADIX}
-     * or greater than {@code Character.MAX_RADIX}, then an
+     * <p>If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX}
+     * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an
      * {@code IllegalArgumentException} is thrown.
      *
      * <p>Invoking the {@link #reset} method will set the scanner's radix to
@@ -1253,6 +1268,7 @@
 
         if (this.defaultRadix == radix)
             return this;
+        modCount++;
         this.defaultRadix = radix;
         // Force rebuilding and recompilation of radix dependent patterns
         integerPattern = null;
@@ -1275,15 +1291,15 @@
      * if no match has been performed, or if the last match was
      * not successful.
      *
-     * <p>The various {@code next}methods of {@code Scanner}
+     * <p>The various {@code next} methods of {@code Scanner}
      * make a match result available if they complete without throwing an
      * exception. For instance, after an invocation of the {@link #nextInt}
      * method that returned an int, this method returns a
      * {@code MatchResult} for the search of the
      * <a href="#Integer-regex"><i>Integer</i></a> regular expression
-     * defined above. Similarly the {@link #findInLine},
-     * {@link #findWithinHorizon}, and {@link #skip} methods will make a
-     * match available if they succeed.
+     * defined above. Similarly the {@link #findInLine findInLine()},
+     * {@link #findWithinHorizon findWithinHorizon()}, and {@link #skip skip()}
+     * methods will make a match available if they succeed.
      *
      * @return a match result for the last match operation
      * @throws IllegalStateException  If no match result is available
@@ -1333,6 +1349,7 @@
     public boolean hasNext() {
         ensureOpen();
         saveState();
+        modCount++;
         while (!sourceClosed) {
             if (hasTokenInBuffer())
                 return revertState(true);
@@ -1357,6 +1374,7 @@
     public String next() {
         ensureOpen();
         clearCaches();
+        modCount++;
 
         while (true) {
             String token = getCompleteTokenInBuffer(null);
@@ -1435,6 +1453,7 @@
             throw new NullPointerException();
         hasNextPattern = null;
         saveState();
+        modCount++;
 
         while (true) {
             if (getCompleteTokenInBuffer(pattern) != null) {
@@ -1466,6 +1485,7 @@
         if (pattern == null)
             throw new NullPointerException();
 
+        modCount++;
         // Did we already find this pattern?
         if (hasNextPattern == pattern)
             return getCachedResult();
@@ -1497,6 +1517,7 @@
     public boolean hasNextLine() {
         saveState();
 
+        modCount++;
         String result = findWithinHorizon(linePattern(), 0);
         if (result != null) {
             MatchResult mr = this.match();
@@ -1531,6 +1552,7 @@
      * @throws IllegalStateException if this scanner is closed
      */
     public String nextLine() {
+        modCount++;
         if (hasNextPattern == linePattern())
             return getCachedResult();
         clearCaches();
@@ -1589,12 +1611,12 @@
         if (pattern == null)
             throw new NullPointerException();
         clearCaches();
+        modCount++;
         // Expand buffer to include the next newline or end of input
         int endPosition = 0;
         saveState();
         while (true) {
-            String token = findPatternInBuffer(separatorPattern(), 0);
-            if (token != null) {
+            if (findPatternInBuffer(separatorPattern(), 0)) {
                 endPosition = matcher.start();
                 break; // up to next newline
             }
@@ -1623,7 +1645,7 @@
      * <p>An invocation of this method of the form
      * {@code findWithinHorizon(pattern)} behaves in exactly the same way as
      * the invocation
-     * {@code findWithinHorizon(Pattern.compile(pattern, horizon))}.
+     * {@code findWithinHorizon(Pattern.compile(pattern), horizon)}.
      *
      * @param pattern a string specifying the pattern to search for
      * @param horizon the search horizon
@@ -1673,13 +1695,13 @@
         if (horizon < 0)
             throw new IllegalArgumentException("horizon < 0");
         clearCaches();
+        modCount++;
 
         // Search for the pattern
         while (true) {
-            String token = findPatternInBuffer(pattern, horizon);
-            if (token != null) {
+            if (findPatternInBuffer(pattern, horizon)) {
                 matchValid = true;
-                return token;
+                return matcher.group();
             }
             if (needInput)
                 readInput();
@@ -1717,11 +1739,11 @@
         if (pattern == null)
             throw new NullPointerException();
         clearCaches();
+        modCount++;
 
         // Search for the pattern
         while (true) {
-            String token = matchPatternInBuffer(pattern);
-            if (token != null) {
+            if (matchPatternInBuffer(pattern)) {
                 matchValid = true;
                 position = matcher.end();
                 return this;
@@ -1932,7 +1954,7 @@
      *
      * <p> An invocation of this method of the form
      * {@code nextShort()} behaves in exactly the same way as the
-     * invocation {@code nextShort(radix)}, where {@code radix}
+     * invocation {@link #nextShort(int) nextShort(radix)}, where {@code radix}
      * is the default radix of this scanner.
      *
      * @return the {@code short} scanned from the input
@@ -2590,8 +2612,10 @@
      * Resets this scanner.
      *
      * <p> Resetting a scanner discards all of its explicit state
-     * information which may have been changed by invocations of {@link
-     * #useDelimiter}, {@link #useLocale}, or {@link #useRadix}.
+     * information which may have been changed by invocations of
+     * {@link #useDelimiter useDelimiter()},
+     * {@link #useLocale useLocale()}, or
+     * {@link #useRadix useRadix()}.
      *
      * <p> An invocation of this method of the form
      * {@code scanner.reset()} behaves in exactly the same way as the
@@ -2612,6 +2636,206 @@
         useLocale(Locale.getDefault(Locale.Category.FORMAT));
         useRadix(10);
         clearCaches();
+        modCount++;
         return this;
     }
+
+    /**
+     * Returns a stream of delimiter-separated tokens from this scanner. The
+     * stream contains the same tokens that would be returned, starting from
+     * this scanner's current state, by calling the {@link #next} method
+     * repeatedly until the {@link #hasNext} method returns false.
+     *
+     * <p>The resulting stream is sequential and ordered. All stream elements are
+     * non-null.
+     *
+     * <p>Scanning starts upon initiation of the terminal stream operation, using the
+     * current state of this scanner. Subsequent calls to any methods on this scanner
+     * other than {@link #close} and {@link #ioException} may return undefined results
+     * or may cause undefined effects on the returned stream. The returned stream's source
+     * {@code Spliterator} is <em>fail-fast</em> and will, on a best-effort basis, throw a
+     * {@link java.util.ConcurrentModificationException} if any such calls are detected
+     * during stream pipeline execution.
+     *
+     * <p>After stream pipeline execution completes, this scanner is left in an indeterminate
+     * state and cannot be reused.
+     *
+     * <p>If this scanner contains a resource that must be released, this scanner
+     * should be closed, either by calling its {@link #close} method, or by
+     * closing the returned stream. Closing the stream will close the underlying scanner.
+     * {@code IllegalStateException} is thrown if the scanner has been closed when this
+     * method is called, or if this scanner is closed during stream pipeline execution.
+     *
+     * <p>This method might block waiting for more input.
+     *
+     * @apiNote
+     * For example, the following code will create a list of
+     * comma-delimited tokens from a string:
+     *
+     * <pre>{@code
+     * List<String> result = new Scanner("abc,def,,ghi")
+     *     .useDelimiter(",")
+     *     .tokens()
+     *     .collect(Collectors.toList());
+     * }</pre>
+     *
+     * <p>The resulting list would contain {@code "abc"}, {@code "def"},
+     * the empty string, and {@code "ghi"}.
+     *
+     * @return a sequential stream of token strings
+     * @throws IllegalStateException if this scanner is closed
+     * @since 1.9
+     */
+    public Stream<String> tokens() {
+        ensureOpen();
+        Stream<String> stream = StreamSupport.stream(new TokenSpliterator(), false);
+        return stream.onClose(this::close);
+    }
+
+    class TokenSpliterator extends Spliterators.AbstractSpliterator<String> {
+        int expectedCount = -1;
+
+        TokenSpliterator() {
+            super(Long.MAX_VALUE,
+                  Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED);
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super String> cons) {
+            if (expectedCount >= 0 && expectedCount != modCount) {
+                throw new ConcurrentModificationException();
+            }
+
+            if (hasNext()) {
+                String token = next();
+                expectedCount = modCount;
+                cons.accept(token);
+                if (expectedCount != modCount) {
+                    throw new ConcurrentModificationException();
+                }
+                return true;
+            } else {
+                expectedCount = modCount;
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Returns a stream of match results from this scanner. The stream
+     * contains the same results in the same order that would be returned by
+     * calling {@code findWithinHorizon(pattern, 0)} and then {@link #match}
+     * successively as long as {@link #findWithinHorizon findWithinHorizon()}
+     * finds matches.
+     *
+     * <p>The resulting stream is sequential and ordered. All stream elements are
+     * non-null.
+     *
+     * <p>Scanning starts upon initiation of the terminal stream operation, using the
+     * current state of this scanner. Subsequent calls to any methods on this scanner
+     * other than {@link #close} and {@link #ioException} may return undefined results
+     * or may cause undefined effects on the returned stream. The returned stream's source
+     * {@code Spliterator} is <em>fail-fast</em> and will, on a best-effort basis, throw a
+     * {@link java.util.ConcurrentModificationException} if any such calls are detected
+     * during stream pipeline execution.
+     *
+     * <p>After stream pipeline execution completes, this scanner is left in an indeterminate
+     * state and cannot be reused.
+     *
+     * <p>If this scanner contains a resource that must be released, this scanner
+     * should be closed, either by calling its {@link #close} method, or by
+     * closing the returned stream. Closing the stream will close the underlying scanner.
+     * {@code IllegalStateException} is thrown if the scanner has been closed when this
+     * method is called, or if this scanner is closed during stream pipeline execution.
+     *
+     * <p>As with the {@link #findWithinHorizon findWithinHorizon()} methods, this method
+     * might block waiting for additional input, and it might buffer an unbounded amount of
+     * input searching for a match.
+     *
+     * @apiNote
+     * For example, the following code will read a file and return a list
+     * of all sequences of characters consisting of seven or more Latin capital
+     * letters:
+     *
+     * <pre>{@code
+     * try (Scanner sc = new Scanner(Paths.get("input.txt"))) {
+     *     Pattern pat = Pattern.compile("[A-Z]{7,}");
+     *     List<String> capWords = sc.findAll(pat)
+     *                               .map(MatchResult::group)
+     *                               .collect(Collectors.toList());
+     * }
+     * }</pre>
+     *
+     * @param pattern the pattern to be matched
+     * @return a sequential stream of match results
+     * @throws NullPointerException if pattern is null
+     * @throws IllegalStateException if this scanner is closed
+     * @since 1.9
+     */
+    public Stream<MatchResult> findAll(Pattern pattern) {
+        Objects.requireNonNull(pattern);
+        ensureOpen();
+        Stream<MatchResult> stream = StreamSupport.stream(new FindSpliterator(pattern), false);
+        return stream.onClose(this::close);
+    }
+
+    /**
+     * Returns a stream of match results that match the provided pattern string.
+     * The effect is equivalent to the following code:
+     *
+     * <pre>{@code
+     *     scanner.findAll(Pattern.compile(patString))
+     * }</pre>
+     *
+     * @param patString the pattern string
+     * @return a sequential stream of match results
+     * @throws NullPointerException if patString is null
+     * @throws IllegalStateException if this scanner is closed
+     * @throws PatternSyntaxException if the regular expression's syntax is invalid
+     * @since 1.9
+     * @see java.util.regex.Pattern
+     */
+    public Stream<MatchResult> findAll(String patString) {
+        Objects.requireNonNull(patString);
+        ensureOpen();
+        return findAll(patternCache.forName(patString));
+    }
+
+    class FindSpliterator extends Spliterators.AbstractSpliterator<MatchResult> {
+        final Pattern pattern;
+        int expectedCount = -1;
+
+        FindSpliterator(Pattern pattern) {
+            super(Long.MAX_VALUE,
+                  Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED);
+            this.pattern = pattern;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super MatchResult> cons) {
+            ensureOpen();
+            if (expectedCount >= 0) {
+                if (expectedCount != modCount) {
+                    throw new ConcurrentModificationException();
+                }
+            } else {
+                expectedCount = modCount;
+            }
+
+            while (true) {
+                // assert expectedCount == modCount
+                if (findPatternInBuffer(pattern, 0)) { // doesn't increment modCount
+                    cons.accept(matcher.toMatchResult());
+                    if (expectedCount != modCount) {
+                        throw new ConcurrentModificationException();
+                    }
+                    return true;
+                }
+                if (needInput)
+                    readInput(); // doesn't increment modCount
+                else
+                    return false; // reached end of input
+            }
+        }
+    }
 }
--- a/jdk/test/java/util/Scanner/ScanTest.java	Tue Sep 15 21:56:04 2015 -0700
+++ b/jdk/test/java/util/Scanner/ScanTest.java	Wed Sep 16 16:24:35 2015 -0700
@@ -24,25 +24,30 @@
 /**
  * @test
  * @bug 4313885 4926319 4927634 5032610 5032622 5049968 5059533 6223711 6277261 6269946 6288823
+ *      8072722
  * @summary Basic tests of java.util.Scanner methods
  * @key randomness
  * @run main/othervm ScanTest
  */
 
-import java.util.*;
+import java.io.*;
+import java.math.*;
+import java.nio.*;
 import java.text.*;
-import java.io.*;
-import java.nio.*;
+import java.util.*;
+import java.util.function.Consumer;
 import java.util.regex.*;
-import java.math.*;
+import java.util.stream.*;
 
 public class ScanTest {
 
     private static boolean failure = false;
     private static int failCount = 0;
     private static int NUM_SOURCE_TYPES = 2;
+    private static File inputFile = new File(System.getProperty("test.src", "."), "input.txt");
 
     public static void main(String[] args) throws Exception {
+
         Locale reservedLocale = Locale.getDefault();
         String lang = reservedLocale.getLanguage();
         try {
@@ -70,8 +75,10 @@
             cacheTest2();
             nonASCIITest();
             resetTest();
+            streamCloseTest();
+            streamComodTest();
 
-            for (int j=0; j<NUM_SOURCE_TYPES; j++) {
+            for (int j = 0; j < NUM_SOURCE_TYPES; j++) {
                 hasNextTest(j);
                 nextTest(j);
                 hasNextPatternTest(j);
@@ -115,149 +122,147 @@
     }
 
     public static void useCase1() throws Exception {
-        File f = new File(System.getProperty("test.src", "."), "input.txt");
-        Scanner sc = new Scanner(f);
-        sc.findWithinHorizon("usage case 1", 0);
-        String[] names = new String[4];
-        for (int i=0; i<4; i++) {
-            while(sc.hasNextFloat())
-                sc.nextFloat();
-            names[i] = sc.next();
-            sc.nextLine();
+        try (Scanner sc = new Scanner(inputFile)) {
+            sc.findWithinHorizon("usage case 1", 0);
+            String[] names = new String[4];
+            for (int i=0; i<4; i++) {
+                while (sc.hasNextFloat())
+                    sc.nextFloat();
+                names[i] = sc.next();
+                sc.nextLine();
+            }
+            if (!names[0].equals("Frank"))
+                failCount++;
+            if (!names[1].equals("Joe"))
+                failCount++;
+            if (!names[2].equals("Mary"))
+                failCount++;
+            if (!names[3].equals("Michelle"))
+                failCount++;
         }
-        if (!names[0].equals("Frank"))
-            failCount++;
-        if (!names[1].equals("Joe"))
-            failCount++;
-        if (!names[2].equals("Mary"))
-            failCount++;
-        if (!names[3].equals("Michelle"))
-            failCount++;
-        sc.close();
         report("Use case 1");
     }
 
     public static void useCase2() throws Exception {
-        File f = new File(System.getProperty("test.src", "."), "input.txt");
-        Scanner sc = new Scanner(f).useDelimiter("-");
-        String testDataTag = sc.findWithinHorizon("usage case 2\n", 0);
-        if (!testDataTag.equals("usage case 2\n"))
-            failCount++;
-        if (!sc.next().equals("cat"))
-            failCount++;
-        if (sc.nextInt() != 9)
-            failCount++;
-        if (!sc.next().equals("dog"))
-            failCount++;
-        if (sc.nextInt() != 6)
-            failCount++;
-        if (!sc.next().equals("pig"))
-            failCount++;
-        if (sc.nextInt() != 2)
-            failCount++;
-        if (!sc.next().equals(""))
-            failCount++;
-        if (sc.nextInt() != 5)
-            failCount++;
-        sc.close();
+        try (Scanner sc = new Scanner(inputFile).useDelimiter("-")) {
+            String testDataTag = sc.findWithinHorizon("usage case 2\n", 0);
+            if (!testDataTag.equals("usage case 2\n"))
+                failCount++;
+            if (!sc.next().equals("cat"))
+                failCount++;
+            if (sc.nextInt() != 9)
+                failCount++;
+            if (!sc.next().equals("dog"))
+                failCount++;
+            if (sc.nextInt() != 6)
+                failCount++;
+            if (!sc.next().equals("pig"))
+                failCount++;
+            if (sc.nextInt() != 2)
+                failCount++;
+            if (!sc.next().equals(""))
+                failCount++;
+            if (sc.nextInt() != 5)
+                failCount++;
+        }
         report("Use case 2");
     }
 
     public static void useCase3() throws Exception {
-        File f = new File(System.getProperty("test.src", "."), "input.txt");
-        Scanner sc = new Scanner(f);
-        String testDataTag = sc.findWithinHorizon("usage case 3\n", 0);
-        if (!testDataTag.equals("usage case 3\n"))
-            failCount++;
-        Pattern tagPattern = Pattern.compile("@[a-z]+");
-        Pattern endPattern = Pattern.compile("\\*\\/");
-        String tag;
-        String end = sc.findInLine(endPattern);
+        try (Scanner sc = new Scanner(inputFile)) {
+            String testDataTag = sc.findWithinHorizon("usage case 3\n", 0);
+            if (!testDataTag.equals("usage case 3\n"))
+                failCount++;
+            Pattern tagPattern = Pattern.compile("@[a-z]+");
+            Pattern endPattern = Pattern.compile("\\*\\/");
+            String tag;
+            String end = sc.findInLine(endPattern);
 
-        while (end == null) {
-            if ((tag = sc.findInLine(tagPattern)) != null) {
-                String text = sc.nextLine();
-                text = text.substring(0, text.length() - 1);
-                //System.out.println(text);
-            } else {
-                sc.nextLine();
+            while (end == null) {
+                if ((tag = sc.findInLine(tagPattern)) != null) {
+                    String text = sc.nextLine();
+                    text = text.substring(0, text.length() - 1);
+                    //System.out.println(text);
+                } else {
+                    sc.nextLine();
+                }
+                end = sc.findInLine(endPattern);
             }
-            end = sc.findInLine(endPattern);
         }
         report("Use case 3");
     }
 
     public static void useCase4() throws Exception {
-        File f = new File(System.getProperty("test.src", "."), "input.txt");
-        Scanner sc = new Scanner(f);
-        String testDataTag = sc.findWithinHorizon("usage case 4\n", 0);
-        if (!testDataTag.equals("usage case 4\n"))
-            failCount++;
+        try (Scanner sc = new Scanner(inputFile)) {
+            String testDataTag = sc.findWithinHorizon("usage case 4\n", 0);
+            if (!testDataTag.equals("usage case 4\n"))
+                failCount++;
 
-        // Read some text parts of four hrefs
-        String[] expected = { "Diffs", "Sdiffs", "Old", "New" };
-        for (int i=0; i<4; i++) {
-            sc.findWithinHorizon("<a href", 1000);
-            sc.useDelimiter("[<>\n]+");
-            sc.next();
-            String textOfRef = sc.next();
-            if (!textOfRef.equals(expected[i]))
+            // Read some text parts of four hrefs
+            String[] expected = { "Diffs", "Sdiffs", "Old", "New" };
+            for (int i=0; i<4; i++) {
+                sc.findWithinHorizon("<a href", 1000);
+                sc.useDelimiter("[<>\n]+");
+                sc.next();
+                String textOfRef = sc.next();
+                if (!textOfRef.equals(expected[i]))
+                    failCount++;
+            }
+            // Read some html tags using < and > as delimiters
+            if (!sc.next().equals("/a"))
                 failCount++;
-        }
-        // Read some html tags using < and > as delimiters
-        if (!sc.next().equals("/a"))
-            failCount++;
-        if (!sc.next().equals("b"))
-            failCount++;
+            if (!sc.next().equals("b"))
+                failCount++;
 
-        // Scan some html tags using skip and next
-        Pattern nonTagStart = Pattern.compile("[^<]+");
-        Pattern tag = Pattern.compile("<[^>]+?>");
-        Pattern spotAfterTag = Pattern.compile("(?<=>)");
-        String[] expected2 = { "</b>", "<p>", "<ul>", "<li>" };
-        sc.useDelimiter(spotAfterTag);
-        int tagsFound = 0;
-        while(tagsFound < 4) {
-            if (!sc.hasNext(tag)) {
-                // skip text between tags
-                sc.skip(nonTagStart);
+            // Scan some html tags using skip and next
+            Pattern nonTagStart = Pattern.compile("[^<]+");
+            Pattern tag = Pattern.compile("<[^>]+?>");
+            Pattern spotAfterTag = Pattern.compile("(?<=>)");
+            String[] expected2 = { "</b>", "<p>", "<ul>", "<li>" };
+            sc.useDelimiter(spotAfterTag);
+            int tagsFound = 0;
+            while (tagsFound < 4) {
+                if (!sc.hasNext(tag)) {
+                    // skip text between tags
+                    sc.skip(nonTagStart);
+                }
+                String tagContents = sc.next(tag);
+                if (!tagContents.equals(expected2[tagsFound]))
+                    failCount++;
+                tagsFound++;
             }
-            String tagContents = sc.next(tag);
-            if (!tagContents.equals(expected2[tagsFound]))
-                failCount++;
-            tagsFound++;
         }
 
         report("Use case 4");
     }
 
     public static void useCase5() throws Exception {
-        File f = new File(System.getProperty("test.src", "."), "input.txt");
-        Scanner sc = new Scanner(f);
-        String testDataTag = sc.findWithinHorizon("usage case 5\n", 0);
-        if (!testDataTag.equals("usage case 5\n"))
-            failCount++;
+        try (Scanner sc = new Scanner(inputFile)) {
+            String testDataTag = sc.findWithinHorizon("usage case 5\n", 0);
+            if (!testDataTag.equals("usage case 5\n"))
+                failCount++;
 
-        sc.findWithinHorizon("Share Definitions", 0);
-        sc.nextLine();
-        sc.next("\\[([a-z]+)\\]");
-        String shareName = sc.match().group(1);
-        if (!shareName.equals("homes"))
-            failCount++;
+            sc.findWithinHorizon("Share Definitions", 0);
+            sc.nextLine();
+            sc.next("\\[([a-z]+)\\]");
+            String shareName = sc.match().group(1);
+            if (!shareName.equals("homes"))
+                failCount++;
 
-        String[] keys = { "comment", "browseable", "writable", "valid users" };
-        String[] vals = { "Home Directories", "no", "yes", "%S" };
-        for (int i=0; i<4; i++) {
-            sc.useDelimiter("=");
-            String key = sc.next().trim();
-            if (!key.equals(keys[i]))
-                failCount++;
-            sc.skip("[ =]+");
-            sc.useDelimiter("\n");
-            String value = sc.next();
-            if (!value.equals(vals[i]))
-                failCount++;
-            sc.nextLine();
+            String[] keys = { "comment", "browseable", "writable", "valid users" };
+            String[] vals = { "Home Directories", "no", "yes", "%S" };
+            for (int i=0; i<4; i++) {
+                sc.useDelimiter("=");
+                String key = sc.next().trim();
+                if (!key.equals(keys[i]))
+                    failCount++;
+                sc.skip("[ =]+");
+                sc.useDelimiter("\n");
+                String value = sc.next();
+                if (!value.equals(vals[i]))
+                    failCount++;
+                sc.nextLine();
+            }
         }
 
         report("Use case 5");
@@ -445,12 +450,12 @@
         if (sc.hasNextLine()) failCount++;
 
         // Go through all the lines in a file
-        File f = new File(System.getProperty("test.src", "."), "input.txt");
-        sc = new Scanner(f);
-        String lastLine = "blah";
-        while(sc.hasNextLine())
-            lastLine = sc.nextLine();
-        if (!lastLine.equals("# Data for usage case 6")) failCount++;
+        try (Scanner sc2 = new Scanner(inputFile)) {
+            String lastLine = "blah";
+            while (sc2.hasNextLine())
+                lastLine = sc2.nextLine();
+            if (!lastLine.equals("# Data for usage case 6")) failCount++;
+        }
 
         report("Has next line test");
     }
@@ -629,48 +634,47 @@
         sc.delimiter();
         sc.useDelimiter("blah");
         sc.useDelimiter(Pattern.compile("blah"));
-        for (int i=0; i<NUM_METHODS; i++) {
+
+        for (Consumer<Scanner> method : methodList) {
             try {
-                methodCall(sc, i);
+                method.accept(sc);
                 failCount++;
             } catch (IllegalStateException ise) {
                 // Correct
             }
         }
+
         report("Close test");
     }
 
-    private static int NUM_METHODS = 23;
-
-    private static void methodCall(Scanner sc, int i) {
-        switch(i) {
-            case 0: sc.hasNext(); break;
-            case 1: sc.next(); break;
-            case 2: sc.hasNext(Pattern.compile("blah")); break;
-            case 3: sc.next(Pattern.compile("blah")); break;
-            case 4: sc.hasNextBoolean(); break;
-            case 5: sc.nextBoolean(); break;
-            case 6: sc.hasNextByte(); break;
-            case 7: sc.nextByte(); break;
-            case 8: sc.hasNextShort(); break;
-            case 9: sc.nextShort(); break;
-            case 10: sc.hasNextInt(); break;
-            case 11: sc.nextInt(); break;
-            case 12: sc.hasNextLong(); break;
-            case 13: sc.nextLong(); break;
-            case 14: sc.hasNextFloat(); break;
-            case 15: sc.nextFloat(); break;
-            case 16: sc.hasNextDouble(); break;
-            case 17: sc.nextDouble(); break;
-            case 18: sc.hasNextBigInteger(); break;
-            case 19: sc.nextBigInteger(); break;
-            case 20: sc.hasNextBigDecimal(); break;
-            case 21: sc.nextBigDecimal(); break;
-            case 22: sc.hasNextLine(); break;
-            default:
-                break;
-        }
-    }
+    static List<Consumer<Scanner>> methodList = Arrays.asList(
+        Scanner::hasNext,
+        Scanner::next,
+        sc -> sc.hasNext(Pattern.compile("blah")),
+        sc -> sc.next(Pattern.compile("blah")),
+        Scanner::hasNextBoolean,
+        Scanner::nextBoolean,
+        Scanner::hasNextByte,
+        Scanner::nextByte,
+        Scanner::hasNextShort,
+        Scanner::nextShort,
+        Scanner::hasNextInt,
+        Scanner::nextInt,
+        Scanner::hasNextLong,
+        Scanner::nextLong,
+        Scanner::hasNextFloat,
+        Scanner::nextFloat,
+        Scanner::hasNextDouble,
+        Scanner::nextDouble,
+        Scanner::hasNextBigInteger,
+        Scanner::nextBigInteger,
+        Scanner::hasNextBigDecimal,
+        Scanner::nextBigDecimal,
+        Scanner::hasNextLine,
+        Scanner::tokens,
+        sc -> sc.findAll(Pattern.compile("blah")),
+        sc -> sc.findAll("blah")
+    );
 
     public static void removeTest() throws Exception {
         Scanner sc = new Scanner("testing");
@@ -864,19 +868,20 @@
 
     public static void fromFileTest() throws Exception {
         File f = new File(System.getProperty("test.src", "."), "input.txt");
-        Scanner sc = new Scanner(f).useDelimiter("\n+");
-        String testDataTag = sc.findWithinHorizon("fromFileTest", 0);
-        if (!testDataTag.equals("fromFileTest"))
-            failCount++;
+        try (Scanner sc = new Scanner(f)) {
+            sc.useDelimiter("\n+");
+            String testDataTag = sc.findWithinHorizon("fromFileTest", 0);
+            if (!testDataTag.equals("fromFileTest"))
+                failCount++;
 
-        int count = 0;
-        while (sc.hasNextLong()) {
-            long blah = sc.nextLong();
-            count++;
+            int count = 0;
+            while (sc.hasNextLong()) {
+                long blah = sc.nextLong();
+                count++;
+            }
+            if (count != 7)
+                failCount++;
         }
-        if (count != 7)
-            failCount++;
-        sc.close();
         report("From file");
     }
 
@@ -884,7 +889,7 @@
         Scanner s = new Scanner("1 fish 2 fish red fish blue fish");
         s.useDelimiter("\\s*fish\\s*");
         List <String> results = new ArrayList<String>();
-        while(s.hasNext())
+        while (s.hasNext())
             results.add(s.next());
         System.out.println(results);
     }
@@ -1472,14 +1477,112 @@
         report("Reset test");
     }
 
+    /*
+     * Test that closing the stream also closes the underlying Scanner.
+     * The cases of attempting to open streams on a closed Scanner are
+     * covered by closeTest().
+     */
+    public static void streamCloseTest() throws Exception {
+        Scanner sc;
+
+        Scanner sc1 = new Scanner("xyzzy");
+        sc1.tokens().close();
+        try {
+            sc1.hasNext();
+            failCount++;
+        } catch (IllegalStateException ise) {
+            // Correct result
+        }
+
+        Scanner sc2 = new Scanner("a b c d e f");
+        try {
+            sc2.tokens()
+               .peek(s -> sc2.close())
+               .count();
+        } catch (IllegalStateException ise) {
+            // Correct result
+        }
+
+        Scanner sc3 = new Scanner("xyzzy");
+        sc3.findAll("q").close();
+        try {
+            sc3.hasNext();
+            failCount++;
+        } catch (IllegalStateException ise) {
+            // Correct result
+        }
+
+        try (Scanner sc4 = new Scanner(inputFile)) {
+            sc4.findAll("[0-9]+")
+               .peek(s -> sc4.close())
+               .count();
+            failCount++;
+        } catch (IllegalStateException ise) {
+            // Correct result
+        }
+
+        report("Streams Close test");
+    }
+
+    /*
+     * Test ConcurrentModificationException
+     */
+    public static void streamComodTest() {
+        try {
+            Scanner sc = new Scanner("a b c d e f");
+            sc.tokens()
+              .peek(s -> sc.hasNext())
+              .count();
+            failCount++;
+        } catch (ConcurrentModificationException cme) {
+            // Correct result
+        }
+
+        try {
+            Scanner sc = new Scanner("a b c d e f");
+            Iterator<String> it = sc.tokens().iterator();
+            it.next();
+            sc.next();
+            it.next();
+            failCount++;
+        } catch (ConcurrentModificationException cme) {
+            // Correct result
+        }
+
+        try {
+            String input = IntStream.range(0, 100)
+                                    .mapToObj(String::valueOf)
+                                    .collect(Collectors.joining(" "));
+            Scanner sc = new Scanner(input);
+            sc.findAll("[0-9]+")
+              .peek(s -> sc.hasNext())
+              .count();
+            failCount++;
+        } catch (ConcurrentModificationException cme) {
+            // Correct result
+        }
+
+        try {
+            String input = IntStream.range(0, 100)
+                                    .mapToObj(String::valueOf)
+                                    .collect(Collectors.joining(" "));
+            Scanner sc = new Scanner(input);
+            Iterator<MatchResult> it = sc.findAll("[0-9]+").iterator();
+            it.next();
+            sc.next();
+            it.next();
+            failCount++;
+        } catch (ConcurrentModificationException cme) {
+            // Correct result
+        }
+
+        report("Streams Comod test");
+    }
+
     private static void report(String testName) {
-        int spacesToAdd = 30 - testName.length();
-        StringBuffer paddedNameBuffer = new StringBuffer(testName);
-        for (int i=0; i<spacesToAdd; i++)
-            paddedNameBuffer.append(" ");
-        String paddedName = paddedNameBuffer.toString();
-        System.err.println(paddedName + ": " +
-                           (failCount==0 ? "Passed":"Failed("+failCount+")"));
+        System.err.printf("%-30s: %s%n", testName,
+                          (failCount == 0) ? "Passed" : String.format("Failed(%d)", failCount));
+
         if (failCount > 0)
             failure = true;
         failCount = 0;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/util/Scanner/ScannerStreamTest.java	Wed Sep 16 16:24:35 2015 -0700
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.regex.MatchResult;
+import java.util.regex.Pattern;
+import java.util.stream.LambdaTestHelpers;
+import java.util.stream.OpTestCase;
+import java.util.stream.Stream;
+import java.util.stream.TestData;
+
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8072722
+ * @summary Tests of stream support in java.util.Scanner
+ * @library ../stream/bootlib
+ * @build java.util.stream.OpTestCase
+ * @run testng/othervm ScannerStreamTest
+ */
+
+@Test
+public class ScannerStreamTest extends OpTestCase {
+
+    static File inputFile = new File(System.getProperty("test.src", "."), "input.txt");
+
+    @DataProvider(name = "Patterns")
+    public static Object[][] makeStreamTestData() {
+        // each inner array is [String description, String input, String delimiter]
+        // delimiter may be null
+        List<Object[]> data = new ArrayList<>();
+
+        data.add(new Object[] { "default delimiter", "abc def ghi",           null });
+        data.add(new Object[] { "fixed delimiter",   "abc,def,,ghi",          "," });
+        data.add(new Object[] { "regexp delimiter",  "###abc##def###ghi###j", "#+" });
+
+        return data.toArray(new Object[0][]);
+    }
+
+    Scanner makeScanner(String input, String delimiter) {
+        Scanner sc = new Scanner(input);
+        if (delimiter != null) {
+            sc.useDelimiter(delimiter);
+        }
+        return sc;
+    }
+
+    @Test(dataProvider = "Patterns")
+    public void tokensTest(String description, String input, String delimiter) {
+        // derive expected result by using conventional loop
+        Scanner sc = makeScanner(input, delimiter);
+        List<String> expected = new ArrayList<>();
+        while (sc.hasNext()) {
+            expected.add(sc.next());
+        }
+
+        Supplier<Stream<String>> ss = () -> makeScanner(input, delimiter).tokens();
+        withData(TestData.Factory.ofSupplier(description, ss))
+                .stream(LambdaTestHelpers.identity())
+                .expectedResult(expected)
+                .exercise();
+    }
+
+    Scanner makeFileScanner(File file) {
+        try {
+            return new Scanner(file, "UTF-8");
+        } catch (IOException ioe) {
+            throw new UncheckedIOException(ioe);
+        }
+    }
+
+    public void findAllTest() {
+        // derive expected result by using conventional loop
+        Pattern pat = Pattern.compile("[A-Z]{7,}");
+        List<String> expected = new ArrayList<>();
+
+        try (Scanner sc = makeFileScanner(inputFile)) {
+            String match;
+            while ((match = sc.findWithinHorizon(pat, 0)) != null) {
+                expected.add(match);
+            }
+        }
+
+        Supplier<Stream<String>> ss =
+            () -> makeFileScanner(inputFile).findAll(pat).map(MatchResult::group);
+
+        withData(TestData.Factory.ofSupplier("findAllTest", ss))
+                .stream(LambdaTestHelpers.identity())
+                .expectedResult(expected)
+                .exercise();
+    }
+
+}