1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.bumptech.glide.disklrucache;
18
19import java.io.ByteArrayOutputStream;
20import java.io.Closeable;
21import java.io.EOFException;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.UnsupportedEncodingException;
25import java.nio.charset.Charset;
26
27/**
28 * Buffers input from an {@link InputStream} for reading lines.
29 *
30 * <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
31 * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
32 * line at end of input is invalid and will be ignored, the caller may use {@code
33 * hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
34 *
35 * <p>This class is intended for reading input that strictly consists of lines, such as line-based
36 * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
37 * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
38 * end-of-input reporting and a more restrictive definition of a line.
39 *
40 * <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
41 * and 10, respectively, and the representation of no other character contains these values.
42 * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
43 * The default charset is US_ASCII.
44 */
45class StrictLineReader implements Closeable {
46  private static final byte CR = (byte) '\r';
47  private static final byte LF = (byte) '\n';
48
49  private final InputStream in;
50  private final Charset charset;
51
52  /*
53   * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
54   * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
55   * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
56   * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
57   */
58  private byte[] buf;
59  private int pos;
60  private int end;
61
62  /**
63   * Constructs a new {@code LineReader} with the specified charset and the default capacity.
64   *
65   * @param in the {@code InputStream} to read data from.
66   * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
67   * supported.
68   * @throws NullPointerException if {@code in} or {@code charset} is null.
69   * @throws IllegalArgumentException if the specified charset is not supported.
70   */
71  public StrictLineReader(InputStream in, Charset charset) {
72    this(in, 8192, charset);
73  }
74
75  /**
76   * Constructs a new {@code LineReader} with the specified capacity and charset.
77   *
78   * @param in the {@code InputStream} to read data from.
79   * @param capacity the capacity of the buffer.
80   * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
81   * supported.
82   * @throws NullPointerException if {@code in} or {@code charset} is null.
83   * @throws IllegalArgumentException if {@code capacity} is negative or zero
84   * or the specified charset is not supported.
85   */
86  public StrictLineReader(InputStream in, int capacity, Charset charset) {
87    if (in == null || charset == null) {
88      throw new NullPointerException();
89    }
90    if (capacity < 0) {
91      throw new IllegalArgumentException("capacity <= 0");
92    }
93    if (!(charset.equals(Util.US_ASCII))) {
94      throw new IllegalArgumentException("Unsupported encoding");
95    }
96
97    this.in = in;
98    this.charset = charset;
99    buf = new byte[capacity];
100  }
101
102  /**
103   * Closes the reader by closing the underlying {@code InputStream} and
104   * marking this reader as closed.
105   *
106   * @throws IOException for errors when closing the underlying {@code InputStream}.
107   */
108  public void close() throws IOException {
109    synchronized (in) {
110      if (buf != null) {
111        buf = null;
112        in.close();
113      }
114    }
115  }
116
117  /**
118   * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
119   * this end of line marker is not included in the result.
120   *
121   * @return the next line from the input.
122   * @throws IOException for underlying {@code InputStream} errors.
123   * @throws EOFException for the end of source stream.
124   */
125  public String readLine() throws IOException {
126    synchronized (in) {
127      if (buf == null) {
128        throw new IOException("LineReader is closed");
129      }
130
131      // Read more data if we are at the end of the buffered data.
132      // Though it's an error to read after an exception, we will let {@code fillBuf()}
133      // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
134      if (pos >= end) {
135        fillBuf();
136      }
137      // Try to find LF in the buffered data and return the line if successful.
138      for (int i = pos; i != end; ++i) {
139        if (buf[i] == LF) {
140          int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
141          String res = new String(buf, pos, lineEnd - pos, charset.name());
142          pos = i + 1;
143          return res;
144        }
145      }
146
147      // Let's anticipate up to 80 characters on top of those already read.
148      ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
149        @Override
150        public String toString() {
151          int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
152          try {
153            return new String(buf, 0, length, charset.name());
154          } catch (UnsupportedEncodingException e) {
155            throw new AssertionError(e); // Since we control the charset this will never happen.
156          }
157        }
158      };
159
160      while (true) {
161        out.write(buf, pos, end - pos);
162        // Mark unterminated line in case fillBuf throws EOFException or IOException.
163        end = -1;
164        fillBuf();
165        // Try to find LF in the buffered data and return the line if successful.
166        for (int i = pos; i != end; ++i) {
167          if (buf[i] == LF) {
168            if (i != pos) {
169              out.write(buf, pos, i - pos);
170            }
171            pos = i + 1;
172            return out.toString();
173          }
174        }
175      }
176    }
177  }
178
179  public boolean hasUnterminatedLine() {
180    return end == -1;
181  }
182
183  /**
184   * Reads new input data into the buffer. Call only with pos == end or end == -1,
185   * depending on the desired outcome if the function throws.
186   */
187  private void fillBuf() throws IOException {
188    int result = in.read(buf, 0, buf.length);
189    if (result == -1) {
190      throw new EOFException();
191    }
192    pos = 0;
193    end = result;
194  }
195}
196
197