1 /*
   2  * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /*
  27  * @test
  28  * @bug 8003258
  29  * @run testng Lines
  30  */
  31 
  32 import java.io.BufferedReader;
  33 import java.io.Reader;
  34 import java.io.StringReader;
  35 import java.io.LineNumberReader;
  36 import java.io.IOException;
  37 import java.io.UncheckedIOException;
  38 import java.util.HashMap;
  39 import java.util.Map;
  40 import java.util.ArrayList;
  41 import java.util.Iterator;
  42 import java.util.NoSuchElementException;
  43 import java.util.stream.Stream;
  44 import java.util.concurrent.atomic.AtomicInteger;
  45 import org.testng.annotations.Test;
  46 import static org.testng.Assert.*;
  47 
  48 @Test(groups = "unit")
  49 public class Lines {
  50     private static final Map<String, Integer> cases = new HashMap<>();
  51 
  52     static {
  53         cases.put("", 0);
  54         cases.put("Line 1", 1);
  55         cases.put("Line 1\n", 1);
  56         cases.put("Line 1\n\n\n", 3);
  57         cases.put("Line 1\nLine 2\nLine 3", 3);
  58         cases.put("Line 1\nLine 2\nLine 3\n", 3);
  59         cases.put("Line 1\n\nLine 3\n\nLine5", 5);
  60     }
  61 
  62     /**
  63      * Helper Reader class which generate specified number of lines contents
  64      * with each line will be "<code>Line &lt;line_number&gt;</code>".
  65      *
  66      * <p>This class also support to simulate {@link IOException} when read pass
  67      * a specified line number.
  68      */
  69     private static class MockLineReader extends Reader {
  70         final int line_count;
  71         boolean closed = false;
  72         int line_no = 0;
  73         String line = null;
  74         int pos = 0;
  75         int inject_ioe_after_line;
  76 
  77         MockLineReader(int cnt) {
  78             this(cnt, cnt);
  79         }
  80 
  81         MockLineReader(int cnt, int inject_ioe) {
  82             line_count = cnt;
  83             inject_ioe_after_line = inject_ioe;
  84         }
  85 
  86         public void reset() {
  87             synchronized(lock) {
  88                 line = null;
  89                 line_no = 0;
  90                 pos = 0;
  91                 closed = false;
  92             }
  93         }
  94 
  95         public void inject_ioe() {
  96             inject_ioe_after_line = line_no;
  97         }
  98 
  99         public int getLineNumber() {
 100             synchronized(lock) {
 101                 return line_no;
 102             }
 103         }
 104 
 105         @Override
 106         public void close() { closed = true; }
 107 
 108         @Override
 109         public int read(char[] buf, int off, int len) throws IOException {
 110             synchronized(lock) {
 111                 if (closed) {
 112                     throw new IOException("Stream is closed.");
 113                 }
 114 
 115                 if (line == null) {
 116                     if (line_count > line_no) {
 117                         line_no += 1;
 118                         if (line_no > inject_ioe_after_line) {
 119                             throw new IOException("Failed to read line " + line_no);
 120                         }
 121                         line = "Line " + line_no + "\n";
 122                         pos = 0;
 123                     } else {
 124                         return -1; // EOS reached
 125                     }
 126                 }
 127 
 128                 int cnt = line.length() - pos;
 129                 assert(cnt != 0);
 130                 // try to fill with remaining
 131                 if (cnt >= len) {
 132                     line.getChars(pos, pos + len, buf, off);
 133                     pos += len;
 134                     if (cnt == len) {
 135                         assert(pos == line.length());
 136                         line = null;
 137                     }
 138                     return len;
 139                 } else {
 140                     line.getChars(pos, pos + cnt, buf, off);
 141                     off += cnt;
 142                     len -= cnt;
 143                     line = null;
 144                     /* hold for next read, so we won't IOE during fill buffer
 145                     int more = read(buf, off, len);
 146                     return (more == -1) ? cnt : cnt + more;
 147                     */
 148                     return cnt;
 149                 }
 150             }
 151         }
 152     }
 153 
 154     private static void verify(Map.Entry<String, Integer> e) {
 155         final String data = e.getKey();
 156         final int total_lines = e.getValue();
 157         try (BufferedReader br = new BufferedReader(
 158                                     new StringReader(data))) {
 159             assertEquals(br.lines()
 160                            .mapToInt(l -> 1).reduce(0, (x, y) -> x + y),
 161                          total_lines,
 162                          data + " should produce " + total_lines + " lines.");
 163         } catch (IOException ioe) {
 164             fail("Should not have any exception.");
 165         }
 166     }
 167 
 168     public void testLinesBasic() {
 169         // Basic test cases
 170         cases.entrySet().stream().forEach(Lines::verify);
 171         // Similar test, also verify MockLineReader is correct
 172         for (int i = 0; i < 10; i++) {
 173             try (BufferedReader br = new BufferedReader(new MockLineReader(i))) {
 174                 assertEquals(br.lines()
 175                                .peek(l -> assertTrue(l.matches("^Line \\d+$")))
 176                                .mapToInt(l -> 1).reduce(0, (x, y) -> x + y),
 177                              i,
 178                              "MockLineReader(" + i + ") should produce " + i + " lines.");
 179             } catch (IOException ioe) {
 180                 fail("Unexpected IOException.");
 181             }
 182         }
 183     }
 184 
 185     public void testUncheckedIOException() throws IOException {
 186         MockLineReader r = new MockLineReader(10, 3);
 187         ArrayList<String> ar = new ArrayList<>();
 188         try (BufferedReader br = new BufferedReader(r)) {
 189             br.lines().limit(3L).forEach(ar::add);
 190             assertEquals(ar.size(), 3, "Should be able to read 3 lines.");
 191         } catch (UncheckedIOException uioe) {
 192             fail("Unexpected UncheckedIOException");
 193         }
 194         r.reset();
 195         try (BufferedReader br = new BufferedReader(r)) {
 196             br.lines().forEach(ar::add);
 197             fail("Should had thrown UncheckedIOException.");
 198         } catch (UncheckedIOException uioe) {
 199             assertEquals(r.getLineNumber(), 4, "should fail to read 4th line");
 200             assertEquals(ar.size(), 6, "3 + 3 lines read");
 201         }
 202         for (int i = 0; i < ar.size(); i++) {
 203             assertEquals(ar.get(i), "Line " + (i % 3 + 1));
 204         }
 205     }
 206 
 207     public void testIterator() throws IOException {
 208         MockLineReader r = new MockLineReader(6);
 209         BufferedReader br = new BufferedReader(r);
 210         String line = br.readLine();
 211         assertEquals(r.getLineNumber(), 1, "Read one line");
 212         Stream<String> s = br.lines();
 213         Iterator<String> it = s.iterator();
 214         // Ensure iterate with only next works
 215         for (int i = 0; i < 5; i++) {
 216             String str = it.next();
 217             assertEquals(str, "Line " + (i + 2), "Addtional five lines");
 218         }
 219         // NoSuchElementException
 220         try {
 221             it.next();
 222             fail("Should have run out of lines.");
 223         } catch (NoSuchElementException nsse) {}
 224     }
 225 
 226     public void testPartialReadAndLineNo() throws IOException {
 227         MockLineReader r = new MockLineReader(5);
 228         LineNumberReader lr = new LineNumberReader(r);
 229         char[] buf = new char[5];
 230         lr.read(buf, 0, 5);
 231         assertEquals(0, lr.getLineNumber(), "LineNumberReader start with line 0");
 232         assertEquals(1, r.getLineNumber(), "MockLineReader start with line 1");
 233         assertEquals(new String(buf), "Line ");
 234         String l1 = lr.readLine();
 235         assertEquals(l1, "1", "Remaining of the first line");
 236         assertEquals(1, lr.getLineNumber(), "Line 1 is read");
 237         assertEquals(1, r.getLineNumber(), "MockLineReader not yet go next line");
 238         lr.read(buf, 0, 4);
 239         assertEquals(1, lr.getLineNumber(), "In the middle of line 2");
 240         assertEquals(new String(buf, 0, 4), "Line");
 241         ArrayList<String> ar = lr.lines()
 242              .peek(l -> assertEquals(lr.getLineNumber(), r.getLineNumber()))
 243              .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
 244         assertEquals(ar.get(0), " 2", "Remaining in the second line");
 245         for (int i = 1; i < ar.size(); i++) {
 246             assertEquals(ar.get(i), "Line " + (i + 2), "Rest are full lines");
 247         }
 248     }
 249 
 250     public void testInterlacedRead() throws IOException {
 251         MockLineReader r = new MockLineReader(10);
 252         BufferedReader br = new BufferedReader(r);
 253         char[] buf = new char[5];
 254         Stream<String> s = br.lines();
 255         Iterator<String> it = s.iterator();
 256 
 257         br.read(buf);
 258         assertEquals(new String(buf), "Line ");
 259         assertEquals(it.next(), "1");
 260         try {
 261             s.iterator().next();
 262             fail("Should failed on second attempt to get iterator from s");
 263         } catch (IllegalStateException ise) {}
 264         br.read(buf, 0, 2);
 265         assertEquals(new String(buf, 0, 2), "Li");
 266         // Get stream again should continue from where left
 267         // Only read remaining of the line
 268         br.lines().limit(1L).forEach(line -> assertEquals(line, "ne 2"));
 269         br.read(buf, 0, 2);
 270         assertEquals(new String(buf, 0, 2), "Li");
 271         br.read(buf, 0, 2);
 272         assertEquals(new String(buf, 0, 2), "ne");
 273         assertEquals(it.next(), " 3");
 274         // Line 4
 275         br.readLine();
 276         // interator pick
 277         assertEquals(it.next(), "Line 5");
 278         // Another stream instantiated by lines()
 279         AtomicInteger line_no = new AtomicInteger(6);
 280         br.lines().forEach(l -> assertEquals(l, "Line " + line_no.getAndIncrement()));
 281         // Read after EOL
 282         assertFalse(it.hasNext());
 283     }
 284 }