8072722: add stream support to Scanner

Reviewed-by: psandoz, chegar, sherman
This commit is contained in:
Stuart Marks 2015-09-16 16:24:35 -07:00
parent a2f0fe3c94
commit 343e882e7f
3 changed files with 676 additions and 224 deletions

View file

@ -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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -25,16 +25,18 @@
package java.util; package java.util;
import java.nio.file.Path;
import java.nio.file.Files;
import java.util.regex.*;
import java.io.*; import java.io.*;
import java.math.*; import java.math.*;
import java.nio.*; import java.nio.*;
import java.nio.channels.*; import java.nio.channels.*;
import java.nio.charset.*; import java.nio.charset.*;
import java.nio.file.Path;
import java.nio.file.Files;
import java.text.*; 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; import sun.misc.LRUCache;
@ -96,22 +98,25 @@ import sun.misc.LRUCache;
* }</pre></blockquote> * }</pre></blockquote>
* *
* <p>The <a name="default-delimiter">default whitespace delimiter</a> used * <p>The <a name="default-delimiter">default whitespace delimiter</a> used
* by a scanner is as recognized by {@link java.lang.Character}.{@link * by a scanner is as recognized by {@link Character#isWhitespace(char)
* java.lang.Character#isWhitespace(char) isWhitespace}. The {@link #reset} * Character.isWhitespace()}. The {@link #reset reset()}
* method will reset the value of the scanner's delimiter to the default * method will reset the value of the scanner's delimiter to the default
* whitespace delimiter regardless of whether it was previously changed. * whitespace delimiter regardless of whether it was previously changed.
* *
* <p>A scanning operation may block waiting for input. * <p>A scanning operation may block waiting for input.
* *
* <p>The {@link #next} and {@link #hasNext} methods and their * <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 * {@link #hasNextInt}) first skip any input that matches the delimiter
* pattern, and then attempt to return the next token. Both {@code hasNext} * pattern, and then attempt to return the next token. Both {@code hasNext()}
* and {@code next} methods may block waiting for further input. Whether a * and {@code next()} methods may block waiting for further input. Whether a
* {@code hasNext} method blocks has no connection to whether or not its * {@code hasNext()} method blocks has no connection to whether or not its
* associated {@code next} method will block. * 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 * methods operate independently of the delimiter pattern. These methods will
* attempt to match the specified pattern with no regard to delimiters in the * 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 * input and thus can be used in special circumstances where delimiters are
@ -129,7 +134,7 @@ import sun.misc.LRUCache;
* *
* <p> A scanner can read text from any object which implements the {@link * <p> A scanner can read text from any object which implements the {@link
* java.lang.Readable} interface. If an invocation of the underlying * 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 * java.io.IOException} then the scanner assumes that the end of the input
* has been reached. The most recent {@code IOException} thrown by the * has been reached. The most recent {@code IOException} thrown by the
* underlying readable can be retrieved via the {@link #ioException} method. * underlying readable can be retrieved via the {@link #ioException} method.
@ -156,7 +161,7 @@ import sun.misc.LRUCache;
* <a name="initial-locale">initial locale </a>is the value returned by the {@link * <a name="initial-locale">initial locale </a>is the value returned by the {@link
* java.util.Locale#getDefault(Locale.Category) * java.util.Locale#getDefault(Locale.Category)
* Locale.getDefault(Locale.Category.FORMAT)} method; it may be changed via the {@link * 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 * scanner's locale to the initial locale regardless of whether it was
* previously changed. * previously changed.
* *
@ -374,6 +379,11 @@ public final class Scanner implements Iterator<String>, Closeable {
// A holder of the last IOException encountered // A holder of the last IOException encountered
private IOException lastException; 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 // A pattern for java whitespace
private static Pattern WHITESPACE_PATTERN = Pattern.compile( private static Pattern WHITESPACE_PATTERN = Pattern.compile(
"\\p{javaWhitespace}+"); "\\p{javaWhitespace}+");
@ -995,8 +1005,9 @@ public final class Scanner implements Iterator<String>, Closeable {
} }
// Finds the specified pattern in the buffer up to horizon. // Finds the specified pattern in the buffer up to horizon.
// Returns a match for the specified input pattern. // Returns true if the specified input pattern was matched,
private String findPatternInBuffer(Pattern pattern, int horizon) { // and leaves the matcher field with the current match state.
private boolean findPatternInBuffer(Pattern pattern, int horizon) {
matchValid = false; matchValid = false;
matcher.usePattern(pattern); matcher.usePattern(pattern);
int bufferLimit = buf.limit(); int bufferLimit = buf.limit();
@ -1014,7 +1025,7 @@ public final class Scanner implements Iterator<String>, Closeable {
if (searchLimit != horizonLimit) { if (searchLimit != horizonLimit) {
// Hit an artificial end; try to extend the match // Hit an artificial end; try to extend the match
needInput = true; needInput = true;
return null; return false;
} }
// The match could go away depending on what is next // The match could go away depending on what is next
if ((searchLimit == horizonLimit) && matcher.requireEnd()) { if ((searchLimit == horizonLimit) && matcher.requireEnd()) {
@ -1022,27 +1033,28 @@ public final class Scanner implements Iterator<String>, Closeable {
// that it is at the horizon and the end of input is // that it is at the horizon and the end of input is
// required for the match. // required for the match.
needInput = true; needInput = true;
return null; return false;
} }
} }
// Did not hit end, or hit real end, or hit horizon // Did not hit end, or hit real end, or hit horizon
position = matcher.end(); position = matcher.end();
return matcher.group(); return true;
} }
if (sourceClosed) if (sourceClosed)
return null; return false;
// If there is no specified horizon, or if we have not searched // If there is no specified horizon, or if we have not searched
// to the specified horizon yet, get more input // to the specified horizon yet, get more input
if ((horizon == 0) || (searchLimit != horizonLimit)) if ((horizon == 0) || (searchLimit != horizonLimit))
needInput = true; needInput = true;
return null; return false;
} }
// Returns a match for the specified input pattern anchored at // Attempts to match a pattern anchored at the current position.
// the current position // Returns true if the specified input pattern was matched,
private String matchPatternInBuffer(Pattern pattern) { // and leaves the matcher field with the current match state.
private boolean matchPatternInBuffer(Pattern pattern) {
matchValid = false; matchValid = false;
matcher.usePattern(pattern); matcher.usePattern(pattern);
matcher.region(position, buf.limit()); matcher.region(position, buf.limit());
@ -1050,18 +1062,18 @@ public final class Scanner implements Iterator<String>, Closeable {
if (matcher.hitEnd() && (!sourceClosed)) { if (matcher.hitEnd() && (!sourceClosed)) {
// Get more input and try again // Get more input and try again
needInput = true; needInput = true;
return null; return false;
} }
position = matcher.end(); position = matcher.end();
return matcher.group(); return true;
} }
if (sourceClosed) if (sourceClosed)
return null; return false;
// Read more to find pattern // Read more to find pattern
needInput = true; needInput = true;
return null; return false;
} }
// Throws if the scanner is closed // Throws if the scanner is closed
@ -1128,6 +1140,7 @@ public final class Scanner implements Iterator<String>, Closeable {
* @return this scanner * @return this scanner
*/ */
public Scanner useDelimiter(Pattern pattern) { public Scanner useDelimiter(Pattern pattern) {
modCount++;
delimPattern = pattern; delimPattern = pattern;
return this; return this;
} }
@ -1147,6 +1160,7 @@ public final class Scanner implements Iterator<String>, Closeable {
* @return this scanner * @return this scanner
*/ */
public Scanner useDelimiter(String pattern) { public Scanner useDelimiter(String pattern) {
modCount++;
delimPattern = patternCache.forName(pattern); delimPattern = patternCache.forName(pattern);
return this; return this;
} }
@ -1181,6 +1195,7 @@ public final class Scanner implements Iterator<String>, Closeable {
if (locale.equals(this.locale)) if (locale.equals(this.locale))
return this; return this;
modCount++;
this.locale = locale; this.locale = locale;
DecimalFormat df = DecimalFormat df =
(DecimalFormat)NumberFormat.getNumberInstance(locale); (DecimalFormat)NumberFormat.getNumberInstance(locale);
@ -1236,8 +1251,8 @@ public final class Scanner implements Iterator<String>, Closeable {
* number matching regular expressions; see * number matching regular expressions; see
* <a href= "#localized-numbers">localized numbers</a> above. * <a href= "#localized-numbers">localized numbers</a> above.
* *
* <p>If the radix is less than {@code Character.MIN_RADIX} * <p>If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX}
* or greater than {@code Character.MAX_RADIX}, then an * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an
* {@code IllegalArgumentException} is thrown. * {@code IllegalArgumentException} is thrown.
* *
* <p>Invoking the {@link #reset} method will set the scanner's radix to * <p>Invoking the {@link #reset} method will set the scanner's radix to
@ -1253,6 +1268,7 @@ public final class Scanner implements Iterator<String>, Closeable {
if (this.defaultRadix == radix) if (this.defaultRadix == radix)
return this; return this;
modCount++;
this.defaultRadix = radix; this.defaultRadix = radix;
// Force rebuilding and recompilation of radix dependent patterns // Force rebuilding and recompilation of radix dependent patterns
integerPattern = null; integerPattern = null;
@ -1275,15 +1291,15 @@ public final class Scanner implements Iterator<String>, Closeable {
* if no match has been performed, or if the last match was * if no match has been performed, or if the last match was
* not successful. * 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 * make a match result available if they complete without throwing an
* exception. For instance, after an invocation of the {@link #nextInt} * exception. For instance, after an invocation of the {@link #nextInt}
* method that returned an int, this method returns a * method that returned an int, this method returns a
* {@code MatchResult} for the search of the * {@code MatchResult} for the search of the
* <a href="#Integer-regex"><i>Integer</i></a> regular expression * <a href="#Integer-regex"><i>Integer</i></a> regular expression
* defined above. Similarly the {@link #findInLine}, * defined above. Similarly the {@link #findInLine findInLine()},
* {@link #findWithinHorizon}, and {@link #skip} methods will make a * {@link #findWithinHorizon findWithinHorizon()}, and {@link #skip skip()}
* match available if they succeed. * methods will make a match available if they succeed.
* *
* @return a match result for the last match operation * @return a match result for the last match operation
* @throws IllegalStateException If no match result is available * @throws IllegalStateException If no match result is available
@ -1333,6 +1349,7 @@ public final class Scanner implements Iterator<String>, Closeable {
public boolean hasNext() { public boolean hasNext() {
ensureOpen(); ensureOpen();
saveState(); saveState();
modCount++;
while (!sourceClosed) { while (!sourceClosed) {
if (hasTokenInBuffer()) if (hasTokenInBuffer())
return revertState(true); return revertState(true);
@ -1357,6 +1374,7 @@ public final class Scanner implements Iterator<String>, Closeable {
public String next() { public String next() {
ensureOpen(); ensureOpen();
clearCaches(); clearCaches();
modCount++;
while (true) { while (true) {
String token = getCompleteTokenInBuffer(null); String token = getCompleteTokenInBuffer(null);
@ -1435,6 +1453,7 @@ public final class Scanner implements Iterator<String>, Closeable {
throw new NullPointerException(); throw new NullPointerException();
hasNextPattern = null; hasNextPattern = null;
saveState(); saveState();
modCount++;
while (true) { while (true) {
if (getCompleteTokenInBuffer(pattern) != null) { if (getCompleteTokenInBuffer(pattern) != null) {
@ -1466,6 +1485,7 @@ public final class Scanner implements Iterator<String>, Closeable {
if (pattern == null) if (pattern == null)
throw new NullPointerException(); throw new NullPointerException();
modCount++;
// Did we already find this pattern? // Did we already find this pattern?
if (hasNextPattern == pattern) if (hasNextPattern == pattern)
return getCachedResult(); return getCachedResult();
@ -1497,6 +1517,7 @@ public final class Scanner implements Iterator<String>, Closeable {
public boolean hasNextLine() { public boolean hasNextLine() {
saveState(); saveState();
modCount++;
String result = findWithinHorizon(linePattern(), 0); String result = findWithinHorizon(linePattern(), 0);
if (result != null) { if (result != null) {
MatchResult mr = this.match(); MatchResult mr = this.match();
@ -1531,6 +1552,7 @@ public final class Scanner implements Iterator<String>, Closeable {
* @throws IllegalStateException if this scanner is closed * @throws IllegalStateException if this scanner is closed
*/ */
public String nextLine() { public String nextLine() {
modCount++;
if (hasNextPattern == linePattern()) if (hasNextPattern == linePattern())
return getCachedResult(); return getCachedResult();
clearCaches(); clearCaches();
@ -1589,12 +1611,12 @@ public final class Scanner implements Iterator<String>, Closeable {
if (pattern == null) if (pattern == null)
throw new NullPointerException(); throw new NullPointerException();
clearCaches(); clearCaches();
modCount++;
// Expand buffer to include the next newline or end of input // Expand buffer to include the next newline or end of input
int endPosition = 0; int endPosition = 0;
saveState(); saveState();
while (true) { while (true) {
String token = findPatternInBuffer(separatorPattern(), 0); if (findPatternInBuffer(separatorPattern(), 0)) {
if (token != null) {
endPosition = matcher.start(); endPosition = matcher.start();
break; // up to next newline break; // up to next newline
} }
@ -1623,7 +1645,7 @@ public final class Scanner implements Iterator<String>, Closeable {
* <p>An invocation of this method of the form * <p>An invocation of this method of the form
* {@code findWithinHorizon(pattern)} behaves in exactly the same way as * {@code findWithinHorizon(pattern)} behaves in exactly the same way as
* the invocation * 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 pattern a string specifying the pattern to search for
* @param horizon the search horizon * @param horizon the search horizon
@ -1673,13 +1695,13 @@ public final class Scanner implements Iterator<String>, Closeable {
if (horizon < 0) if (horizon < 0)
throw new IllegalArgumentException("horizon < 0"); throw new IllegalArgumentException("horizon < 0");
clearCaches(); clearCaches();
modCount++;
// Search for the pattern // Search for the pattern
while (true) { while (true) {
String token = findPatternInBuffer(pattern, horizon); if (findPatternInBuffer(pattern, horizon)) {
if (token != null) {
matchValid = true; matchValid = true;
return token; return matcher.group();
} }
if (needInput) if (needInput)
readInput(); readInput();
@ -1717,11 +1739,11 @@ public final class Scanner implements Iterator<String>, Closeable {
if (pattern == null) if (pattern == null)
throw new NullPointerException(); throw new NullPointerException();
clearCaches(); clearCaches();
modCount++;
// Search for the pattern // Search for the pattern
while (true) { while (true) {
String token = matchPatternInBuffer(pattern); if (matchPatternInBuffer(pattern)) {
if (token != null) {
matchValid = true; matchValid = true;
position = matcher.end(); position = matcher.end();
return this; return this;
@ -1932,7 +1954,7 @@ public final class Scanner implements Iterator<String>, Closeable {
* *
* <p> An invocation of this method of the form * <p> An invocation of this method of the form
* {@code nextShort()} behaves in exactly the same way as the * {@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. * is the default radix of this scanner.
* *
* @return the {@code short} scanned from the input * @return the {@code short} scanned from the input
@ -2590,8 +2612,10 @@ public final class Scanner implements Iterator<String>, Closeable {
* Resets this scanner. * Resets this scanner.
* *
* <p> Resetting a scanner discards all of its explicit state * <p> Resetting a scanner discards all of its explicit state
* information which may have been changed by invocations of {@link * information which may have been changed by invocations of
* #useDelimiter}, {@link #useLocale}, or {@link #useRadix}. * {@link #useDelimiter useDelimiter()},
* {@link #useLocale useLocale()}, or
* {@link #useRadix useRadix()}.
* *
* <p> An invocation of this method of the form * <p> An invocation of this method of the form
* {@code scanner.reset()} behaves in exactly the same way as the * {@code scanner.reset()} behaves in exactly the same way as the
@ -2612,6 +2636,206 @@ public final class Scanner implements Iterator<String>, Closeable {
useLocale(Locale.getDefault(Locale.Category.FORMAT)); useLocale(Locale.getDefault(Locale.Category.FORMAT));
useRadix(10); useRadix(10);
clearCaches(); clearCaches();
modCount++;
return this; 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
}
}
}
} }

View file

@ -24,25 +24,30 @@
/** /**
* @test * @test
* @bug 4313885 4926319 4927634 5032610 5032622 5049968 5059533 6223711 6277261 6269946 6288823 * @bug 4313885 4926319 4927634 5032610 5032622 5049968 5059533 6223711 6277261 6269946 6288823
* 8072722
* @summary Basic tests of java.util.Scanner methods * @summary Basic tests of java.util.Scanner methods
* @key randomness * @key randomness
* @run main/othervm ScanTest * @run main/othervm ScanTest
*/ */
import java.util.*;
import java.text.*;
import java.io.*; import java.io.*;
import java.nio.*;
import java.util.regex.*;
import java.math.*; import java.math.*;
import java.nio.*;
import java.text.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.*;
import java.util.stream.*;
public class ScanTest { public class ScanTest {
private static boolean failure = false; private static boolean failure = false;
private static int failCount = 0; private static int failCount = 0;
private static int NUM_SOURCE_TYPES = 2; 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 { public static void main(String[] args) throws Exception {
Locale reservedLocale = Locale.getDefault(); Locale reservedLocale = Locale.getDefault();
String lang = reservedLocale.getLanguage(); String lang = reservedLocale.getLanguage();
try { try {
@ -70,8 +75,10 @@ public class ScanTest {
cacheTest2(); cacheTest2();
nonASCIITest(); nonASCIITest();
resetTest(); resetTest();
streamCloseTest();
streamComodTest();
for (int j=0; j<NUM_SOURCE_TYPES; j++) { for (int j = 0; j < NUM_SOURCE_TYPES; j++) {
hasNextTest(j); hasNextTest(j);
nextTest(j); nextTest(j);
hasNextPatternTest(j); hasNextPatternTest(j);
@ -115,149 +122,147 @@ public class ScanTest {
} }
public static void useCase1() throws Exception { public static void useCase1() throws Exception {
File f = new File(System.getProperty("test.src", "."), "input.txt"); try (Scanner sc = new Scanner(inputFile)) {
Scanner sc = new Scanner(f); sc.findWithinHorizon("usage case 1", 0);
sc.findWithinHorizon("usage case 1", 0); String[] names = new String[4];
String[] names = new String[4]; for (int i=0; i<4; i++) {
for (int i=0; i<4; i++) { while (sc.hasNextFloat())
while(sc.hasNextFloat()) sc.nextFloat();
sc.nextFloat(); names[i] = sc.next();
names[i] = sc.next(); sc.nextLine();
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"); report("Use case 1");
} }
public static void useCase2() throws Exception { public static void useCase2() throws Exception {
File f = new File(System.getProperty("test.src", "."), "input.txt"); try (Scanner sc = new Scanner(inputFile).useDelimiter("-")) {
Scanner sc = new Scanner(f).useDelimiter("-"); String testDataTag = sc.findWithinHorizon("usage case 2\n", 0);
String testDataTag = sc.findWithinHorizon("usage case 2\n", 0); if (!testDataTag.equals("usage case 2\n"))
if (!testDataTag.equals("usage case 2\n")) failCount++;
failCount++; if (!sc.next().equals("cat"))
if (!sc.next().equals("cat")) failCount++;
failCount++; if (sc.nextInt() != 9)
if (sc.nextInt() != 9) failCount++;
failCount++; if (!sc.next().equals("dog"))
if (!sc.next().equals("dog")) failCount++;
failCount++; if (sc.nextInt() != 6)
if (sc.nextInt() != 6) failCount++;
failCount++; if (!sc.next().equals("pig"))
if (!sc.next().equals("pig")) failCount++;
failCount++; if (sc.nextInt() != 2)
if (sc.nextInt() != 2) failCount++;
failCount++; if (!sc.next().equals(""))
if (!sc.next().equals("")) failCount++;
failCount++; if (sc.nextInt() != 5)
if (sc.nextInt() != 5) failCount++;
failCount++; }
sc.close();
report("Use case 2"); report("Use case 2");
} }
public static void useCase3() throws Exception { public static void useCase3() throws Exception {
File f = new File(System.getProperty("test.src", "."), "input.txt"); try (Scanner sc = new Scanner(inputFile)) {
Scanner sc = new Scanner(f); String testDataTag = sc.findWithinHorizon("usage case 3\n", 0);
String testDataTag = sc.findWithinHorizon("usage case 3\n", 0); if (!testDataTag.equals("usage case 3\n"))
if (!testDataTag.equals("usage case 3\n")) failCount++;
failCount++; Pattern tagPattern = Pattern.compile("@[a-z]+");
Pattern tagPattern = Pattern.compile("@[a-z]+"); Pattern endPattern = Pattern.compile("\\*\\/");
Pattern endPattern = Pattern.compile("\\*\\/"); String tag;
String tag; String end = sc.findInLine(endPattern);
String end = sc.findInLine(endPattern);
while (end == null) { while (end == null) {
if ((tag = sc.findInLine(tagPattern)) != null) { if ((tag = sc.findInLine(tagPattern)) != null) {
String text = sc.nextLine(); String text = sc.nextLine();
text = text.substring(0, text.length() - 1); text = text.substring(0, text.length() - 1);
//System.out.println(text); //System.out.println(text);
} else { } else {
sc.nextLine(); sc.nextLine();
}
end = sc.findInLine(endPattern);
} }
end = sc.findInLine(endPattern);
} }
report("Use case 3"); report("Use case 3");
} }
public static void useCase4() throws Exception { public static void useCase4() throws Exception {
File f = new File(System.getProperty("test.src", "."), "input.txt"); try (Scanner sc = new Scanner(inputFile)) {
Scanner sc = new Scanner(f); String testDataTag = sc.findWithinHorizon("usage case 4\n", 0);
String testDataTag = sc.findWithinHorizon("usage case 4\n", 0); if (!testDataTag.equals("usage case 4\n"))
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]))
failCount++; failCount++;
}
// Read some html tags using < and > as delimiters
if (!sc.next().equals("/a"))
failCount++;
if (!sc.next().equals("b"))
failCount++;
// Scan some html tags using skip and next // Read some text parts of four hrefs
Pattern nonTagStart = Pattern.compile("[^<]+"); String[] expected = { "Diffs", "Sdiffs", "Old", "New" };
Pattern tag = Pattern.compile("<[^>]+?>"); for (int i=0; i<4; i++) {
Pattern spotAfterTag = Pattern.compile("(?<=>)"); sc.findWithinHorizon("<a href", 1000);
String[] expected2 = { "</b>", "<p>", "<ul>", "<li>" }; sc.useDelimiter("[<>\n]+");
sc.useDelimiter(spotAfterTag); sc.next();
int tagsFound = 0; String textOfRef = sc.next();
while(tagsFound < 4) { if (!textOfRef.equals(expected[i]))
if (!sc.hasNext(tag)) { failCount++;
// skip text between tags
sc.skip(nonTagStart);
} }
String tagContents = sc.next(tag); // Read some html tags using < and > as delimiters
if (!tagContents.equals(expected2[tagsFound])) if (!sc.next().equals("/a"))
failCount++; failCount++;
tagsFound++; 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);
}
String tagContents = sc.next(tag);
if (!tagContents.equals(expected2[tagsFound]))
failCount++;
tagsFound++;
}
} }
report("Use case 4"); report("Use case 4");
} }
public static void useCase5() throws Exception { public static void useCase5() throws Exception {
File f = new File(System.getProperty("test.src", "."), "input.txt"); try (Scanner sc = new Scanner(inputFile)) {
Scanner sc = new Scanner(f); String testDataTag = sc.findWithinHorizon("usage case 5\n", 0);
String testDataTag = sc.findWithinHorizon("usage case 5\n", 0); if (!testDataTag.equals("usage case 5\n"))
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++;
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++; failCount++;
sc.findWithinHorizon("Share Definitions", 0);
sc.nextLine(); 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();
}
} }
report("Use case 5"); report("Use case 5");
@ -445,12 +450,12 @@ public class ScanTest {
if (sc.hasNextLine()) failCount++; if (sc.hasNextLine()) failCount++;
// Go through all the lines in a file // Go through all the lines in a file
File f = new File(System.getProperty("test.src", "."), "input.txt"); try (Scanner sc2 = new Scanner(inputFile)) {
sc = new Scanner(f); String lastLine = "blah";
String lastLine = "blah"; while (sc2.hasNextLine())
while(sc.hasNextLine()) lastLine = sc2.nextLine();
lastLine = sc.nextLine(); if (!lastLine.equals("# Data for usage case 6")) failCount++;
if (!lastLine.equals("# Data for usage case 6")) failCount++; }
report("Has next line test"); report("Has next line test");
} }
@ -629,48 +634,47 @@ public class ScanTest {
sc.delimiter(); sc.delimiter();
sc.useDelimiter("blah"); sc.useDelimiter("blah");
sc.useDelimiter(Pattern.compile("blah")); sc.useDelimiter(Pattern.compile("blah"));
for (int i=0; i<NUM_METHODS; i++) {
for (Consumer<Scanner> method : methodList) {
try { try {
methodCall(sc, i); method.accept(sc);
failCount++; failCount++;
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
// Correct // Correct
} }
} }
report("Close test"); report("Close test");
} }
private static int NUM_METHODS = 23; static List<Consumer<Scanner>> methodList = Arrays.asList(
Scanner::hasNext,
private static void methodCall(Scanner sc, int i) { Scanner::next,
switch(i) { sc -> sc.hasNext(Pattern.compile("blah")),
case 0: sc.hasNext(); break; sc -> sc.next(Pattern.compile("blah")),
case 1: sc.next(); break; Scanner::hasNextBoolean,
case 2: sc.hasNext(Pattern.compile("blah")); break; Scanner::nextBoolean,
case 3: sc.next(Pattern.compile("blah")); break; Scanner::hasNextByte,
case 4: sc.hasNextBoolean(); break; Scanner::nextByte,
case 5: sc.nextBoolean(); break; Scanner::hasNextShort,
case 6: sc.hasNextByte(); break; Scanner::nextShort,
case 7: sc.nextByte(); break; Scanner::hasNextInt,
case 8: sc.hasNextShort(); break; Scanner::nextInt,
case 9: sc.nextShort(); break; Scanner::hasNextLong,
case 10: sc.hasNextInt(); break; Scanner::nextLong,
case 11: sc.nextInt(); break; Scanner::hasNextFloat,
case 12: sc.hasNextLong(); break; Scanner::nextFloat,
case 13: sc.nextLong(); break; Scanner::hasNextDouble,
case 14: sc.hasNextFloat(); break; Scanner::nextDouble,
case 15: sc.nextFloat(); break; Scanner::hasNextBigInteger,
case 16: sc.hasNextDouble(); break; Scanner::nextBigInteger,
case 17: sc.nextDouble(); break; Scanner::hasNextBigDecimal,
case 18: sc.hasNextBigInteger(); break; Scanner::nextBigDecimal,
case 19: sc.nextBigInteger(); break; Scanner::hasNextLine,
case 20: sc.hasNextBigDecimal(); break; Scanner::tokens,
case 21: sc.nextBigDecimal(); break; sc -> sc.findAll(Pattern.compile("blah")),
case 22: sc.hasNextLine(); break; sc -> sc.findAll("blah")
default: );
break;
}
}
public static void removeTest() throws Exception { public static void removeTest() throws Exception {
Scanner sc = new Scanner("testing"); Scanner sc = new Scanner("testing");
@ -864,19 +868,20 @@ public class ScanTest {
public static void fromFileTest() throws Exception { public static void fromFileTest() throws Exception {
File f = new File(System.getProperty("test.src", "."), "input.txt"); File f = new File(System.getProperty("test.src", "."), "input.txt");
Scanner sc = new Scanner(f).useDelimiter("\n+"); try (Scanner sc = new Scanner(f)) {
String testDataTag = sc.findWithinHorizon("fromFileTest", 0); sc.useDelimiter("\n+");
if (!testDataTag.equals("fromFileTest")) String testDataTag = sc.findWithinHorizon("fromFileTest", 0);
failCount++; if (!testDataTag.equals("fromFileTest"))
failCount++;
int count = 0; int count = 0;
while (sc.hasNextLong()) { while (sc.hasNextLong()) {
long blah = sc.nextLong(); long blah = sc.nextLong();
count++; count++;
}
if (count != 7)
failCount++;
} }
if (count != 7)
failCount++;
sc.close();
report("From file"); report("From file");
} }
@ -884,7 +889,7 @@ public class ScanTest {
Scanner s = new Scanner("1 fish 2 fish red fish blue fish"); Scanner s = new Scanner("1 fish 2 fish red fish blue fish");
s.useDelimiter("\\s*fish\\s*"); s.useDelimiter("\\s*fish\\s*");
List <String> results = new ArrayList<String>(); List <String> results = new ArrayList<String>();
while(s.hasNext()) while (s.hasNext())
results.add(s.next()); results.add(s.next());
System.out.println(results); System.out.println(results);
} }
@ -1472,14 +1477,112 @@ public class ScanTest {
report("Reset test"); 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) { private static void report(String testName) {
int spacesToAdd = 30 - testName.length(); System.err.printf("%-30s: %s%n", testName,
StringBuffer paddedNameBuffer = new StringBuffer(testName); (failCount == 0) ? "Passed" : String.format("Failed(%d)", failCount));
for (int i=0; i<spacesToAdd; i++)
paddedNameBuffer.append(" ");
String paddedName = paddedNameBuffer.toString();
System.err.println(paddedName + ": " +
(failCount==0 ? "Passed":"Failed("+failCount+")"));
if (failCount > 0) if (failCount > 0)
failure = true; failure = true;
failCount = 0; failCount = 0;

View file

@ -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();
}
}