# HG changeset patch # User smarks # Date 1441759021 25200 # Tue Sep 08 17:37:01 2015 -0700 # Node ID 40d4d0697f8fdf673ef6e3b4379d38a27992fe05 # Parent 4f3c5f831833e8db7c25f5b70e92ef3fd6296145 8072722: add stream support to Scanner Reviewed-by: psandoz diff --git a/src/java.base/share/classes/java/util/Scanner.java b/src/java.base/share/classes/java/util/Scanner.java --- a/src/java.base/share/classes/java/util/Scanner.java +++ b/src/java.base/share/classes/java/util/Scanner.java @@ -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 @@ * } * *

The default whitespace delimiter 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. * *

A scanning operation may block waiting for input. * *

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. * - *

The {@link #findInLine}, {@link #findWithinHorizon}, and {@link #skip} + *

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 @@ * *

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 @@ * initial locale 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 * localized numbers above. * - *

If the radix is less than {@code Character.MIN_RADIX} - * or greater than {@code Character.MAX_RADIX}, then an + *

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. * *

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. * - *

The various {@code next}methods of {@code Scanner} + *

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 * Integer 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 @@ *

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 @@ * *

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. * *

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()}. * *

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} returns false. + * + *

The resulting stream is sequential and ordered. All stream elements are + * non-null. + * + *

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 fail-fast and will, on a best-effort basis, throw a + * {@link java.util.ConcurrentModificationException} if any such calls are detected + * during pipeline execution. + * + *

After pipeline execution completes, this scanner is left in an indeterminate + * state and cannot be reused. + * + *

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 pipeline execution. + * + *

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: + * + *

{@code
+     * List result = new Scanner("abc,def,,ghi")
+     *     .useDelimiter(",")
+     *     .tokens()
+     *     .collect(Collectors.toList());
+     * }
+ * + *

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 tokens() { + ensureOpen(); + Stream stream = StreamSupport.stream(new TokenSpliterator(), false); + return stream.onClose(this::close); + } + + class TokenSpliterator extends Spliterators.AbstractSpliterator { + int expectedCount = -1; + + TokenSpliterator() { + super(Long.MAX_VALUE, + Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED); + } + + @Override + public boolean tryAdvance(Consumer 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. + * + *

The resulting stream is sequential and ordered. All stream elements are + * non-null. + * + *

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 fail-fast and will, on a best-effort basis, throw a + * {@link java.util.ConcurrentModificationException} if any such calls are detected + * during pipeline execution. + * + *

After pipeline execution completes, this scanner is left in an indeterminate + * state and cannot be reused. + * + *

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 pipeline execution. + * + *

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: + * + *

{@code
+     * try (Scanner sc = new Scanner(Paths.get("input.txt"))) {
+     *     Pattern pat = Pattern.compile("[A-Z]{7,}");
+     *     List capWords = sc.findAll(pat)
+     *                               .map(MatchResult::group)
+     *                               .collect(Collectors.toList());
+     * }
+     * }
+ * + * @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 findAll(Pattern pattern) { + Objects.requireNonNull(pattern); + ensureOpen(); + Stream 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: + * + *
{@code
+     *     scanner.findAll(Pattern.compile(patString))
+     * }
+ * + * @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 findAll(String patString) { + Objects.requireNonNull(patString); + ensureOpen(); + return findAll(patternCache.forName(patString)); + } + + class FindSpliterator extends Spliterators.AbstractSpliterator { + 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 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 + } + } + } } diff --git a/test/java/util/Scanner/ScanTest.java b/test/java/util/Scanner/ScanTest.java --- a/test/java/util/Scanner/ScanTest.java +++ b/test/java/util/Scanner/ScanTest.java @@ -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.io.*; +import java.math.*; +import java.nio.*; +import java.text.*; import java.util.*; -import java.text.*; -import java.io.*; -import java.nio.*; +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\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("\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 = { "", "

", "

    ", "
  • " }; - 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 = { "", "

    ", "

      ", "
    • " }; + 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 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> 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 results = new ArrayList(); - 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 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 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 0) failure = true; failCount = 0; diff --git a/test/java/util/Scanner/ScannerStreamTest.java b/test/java/util/Scanner/ScannerStreamTest.java new file mode 100644 --- /dev/null +++ b/test/java/util/Scanner/ScannerStreamTest.java @@ -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 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 expected = new ArrayList<>(); + while (sc.hasNext()) { + expected.add(sc.next()); + } + + Supplier> 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 expected = new ArrayList<>(); + + try (Scanner sc = makeFileScanner(inputFile)) { + String match; + while ((match = sc.findWithinHorizon(pat, 0)) != null) { + expected.add(match); + } + } + + Supplier> ss = + () -> makeFileScanner(inputFile).findAll(pat).map(MatchResult::group); + + withData(TestData.Factory.ofSupplier("findAllTest", ss)) + .stream(LambdaTestHelpers.identity()) + .expectedResult(expected) + .exercise(); + } + +}