|
| 1 | +/* |
| 2 | + * Scala (https://www.scala-lang.org) |
| 3 | + * |
| 4 | + * Copyright EPFL and Lightbend, Inc. dba Akka |
| 5 | + * |
| 6 | + * Licensed under Apache License 2.0 |
| 7 | + * (http://www.apache.org/licenses/LICENSE-2.0). |
| 8 | + * |
| 9 | + * See the NOTICE file distributed with this work for |
| 10 | + * additional information regarding copyright ownership. |
| 11 | + */ |
| 12 | + |
| 13 | +package scala |
| 14 | +package io |
| 15 | + |
| 16 | +import scala.collection.{AbstractIterator, BufferedIterator} |
| 17 | +import java.io.{Closeable, FileInputStream, FileNotFoundException, InputStream, PrintStream, File => JFile} |
| 18 | +import java.net.{URI, URL} |
| 19 | + |
| 20 | +import scala.annotation.nowarn |
| 21 | + |
| 22 | +/** This object provides convenience methods to create an iterable |
| 23 | + * representation of a source file. |
| 24 | + */ |
| 25 | +object Source { |
| 26 | + val DefaultBufSize = 2048 |
| 27 | + |
| 28 | + /** Creates a `Source` from System.in. |
| 29 | + */ |
| 30 | + def stdin = fromInputStream(System.in) |
| 31 | + |
| 32 | + /** Creates a Source from an Iterable. |
| 33 | + * |
| 34 | + * @param iterable the Iterable |
| 35 | + * @return the Source |
| 36 | + */ |
| 37 | + def fromIterable(iterable: Iterable[Char]): Source = new Source { |
| 38 | + val iter = iterable.iterator |
| 39 | + } withReset(() => fromIterable(iterable)) |
| 40 | + |
| 41 | + /** Creates a Source instance from a single character. |
| 42 | + */ |
| 43 | + def fromChar(c: Char): Source = fromIterable(Array(c)) |
| 44 | + |
| 45 | + /** creates Source from array of characters, with empty description. |
| 46 | + */ |
| 47 | + def fromChars(chars: Array[Char]): Source = fromIterable(chars) |
| 48 | + |
| 49 | + /** creates Source from a String, with no description. |
| 50 | + */ |
| 51 | + def fromString(s: String): Source = fromIterable(s) |
| 52 | + |
| 53 | + /** creates Source from file with given name, setting its description to |
| 54 | + * filename. |
| 55 | + */ |
| 56 | + def fromFile(name: String)(implicit codec: Codec): BufferedSource = |
| 57 | + fromFile(new JFile(name))(codec) |
| 58 | + |
| 59 | + /** creates Source from file with given name, using given encoding, setting |
| 60 | + * its description to filename. |
| 61 | + */ |
| 62 | + def fromFile(name: String, enc: String): BufferedSource = |
| 63 | + fromFile(name)(Codec(enc)) |
| 64 | + |
| 65 | + /** creates `source` from file with given file `URI`. |
| 66 | + */ |
| 67 | + def fromFile(uri: URI)(implicit codec: Codec): BufferedSource = |
| 68 | + fromFile(new JFile(uri))(codec) |
| 69 | + |
| 70 | + /** creates Source from file with given file: URI |
| 71 | + */ |
| 72 | + def fromFile(uri: URI, enc: String): BufferedSource = |
| 73 | + fromFile(uri)(Codec(enc)) |
| 74 | + |
| 75 | + /** creates Source from file, using default character encoding, setting its |
| 76 | + * description to filename. |
| 77 | + */ |
| 78 | + def fromFile(file: JFile)(implicit codec: Codec): BufferedSource = |
| 79 | + fromFile(file, Source.DefaultBufSize)(codec) |
| 80 | + |
| 81 | + /** same as fromFile(file, enc, Source.DefaultBufSize) |
| 82 | + */ |
| 83 | + def fromFile(file: JFile, enc: String): BufferedSource = |
| 84 | + fromFile(file)(Codec(enc)) |
| 85 | + |
| 86 | + def fromFile(file: JFile, enc: String, bufferSize: Int): BufferedSource = |
| 87 | + fromFile(file, bufferSize)(Codec(enc)) |
| 88 | + |
| 89 | + /** Creates Source from `file`, using given character encoding, setting |
| 90 | + * its description to filename. Input is buffered in a buffer of size |
| 91 | + * `bufferSize`. |
| 92 | + */ |
| 93 | + def fromFile(file: JFile, bufferSize: Int)(implicit codec: Codec): BufferedSource = { |
| 94 | + val inputStream = new FileInputStream(file) |
| 95 | + |
| 96 | + createBufferedSource( |
| 97 | + inputStream, |
| 98 | + bufferSize, |
| 99 | + () => fromFile(file, bufferSize)(codec), |
| 100 | + () => inputStream.close() |
| 101 | + )(codec) withDescription s"file:${file.getAbsolutePath}" |
| 102 | + } |
| 103 | + |
| 104 | + /** Create a `Source` from array of bytes, decoding |
| 105 | + * the bytes according to codec. |
| 106 | + * |
| 107 | + * @return the created `Source` instance. |
| 108 | + */ |
| 109 | + def fromBytes(bytes: Array[Byte])(implicit codec: Codec): Source = |
| 110 | + fromString(new String(bytes, codec.name)) |
| 111 | + |
| 112 | + def fromBytes(bytes: Array[Byte], enc: String): Source = |
| 113 | + fromBytes(bytes)(Codec(enc)) |
| 114 | + |
| 115 | + /** Create a `Source` from array of bytes, assuming |
| 116 | + * one byte per character (ISO-8859-1 encoding.) |
| 117 | + */ |
| 118 | + @deprecated("Use `fromBytes` and specify an encoding", since="2.13.9") |
| 119 | + def fromRawBytes(bytes: Array[Byte]): Source = |
| 120 | + fromString(new String(bytes, Codec.ISO8859.charSet)) |
| 121 | + |
| 122 | + /** creates `Source` from file with given file: URI |
| 123 | + */ |
| 124 | + def fromURI(uri: URI)(implicit codec: Codec): BufferedSource = |
| 125 | + fromFile(new JFile(uri))(codec) |
| 126 | + |
| 127 | + /** same as fromURL(new URL(s))(Codec(enc)) |
| 128 | + */ |
| 129 | + def fromURL(s: String, enc: String): BufferedSource = |
| 130 | + fromURL(s)(Codec(enc)) |
| 131 | + |
| 132 | + /** same as fromURL(new URL(s)) |
| 133 | + */ |
| 134 | + def fromURL(s: String)(implicit codec: Codec): BufferedSource = |
| 135 | + fromURL(new URI(s).toURL)(codec) |
| 136 | + |
| 137 | + /** same as fromInputStream(url.openStream())(Codec(enc)) |
| 138 | + */ |
| 139 | + def fromURL(url: URL, enc: String): BufferedSource = |
| 140 | + fromURL(url)(Codec(enc)) |
| 141 | + |
| 142 | + /** same as fromInputStream(url.openStream())(codec) |
| 143 | + */ |
| 144 | + def fromURL(url: URL)(implicit codec: Codec): BufferedSource = |
| 145 | + fromInputStream(url.openStream())(codec) |
| 146 | + |
| 147 | + /** Reads data from inputStream with a buffered reader, using the encoding |
| 148 | + * in implicit parameter codec. |
| 149 | + * |
| 150 | + * @param inputStream the input stream from which to read |
| 151 | + * @param bufferSize buffer size (defaults to Source.DefaultBufSize) |
| 152 | + * @param reset a () => Source which resets the stream (if unset, reset() will throw an Exception) |
| 153 | + * @param close a () => Unit method which closes the stream (if unset, close() will do nothing) |
| 154 | + * @param codec (implicit) a scala.io.Codec specifying behavior (defaults to Codec.default) |
| 155 | + * @return the buffered source |
| 156 | + */ |
| 157 | + def createBufferedSource( |
| 158 | + inputStream: InputStream, |
| 159 | + bufferSize: Int = DefaultBufSize, |
| 160 | + reset: () => Source = null, |
| 161 | + close: () => Unit = null |
| 162 | + )(implicit codec: Codec): BufferedSource = { |
| 163 | + // workaround for default arguments being unable to refer to other parameters |
| 164 | + val resetFn = if (reset == null) () => createBufferedSource(inputStream, bufferSize, reset, close)(codec) else reset |
| 165 | + |
| 166 | + new BufferedSource(inputStream, bufferSize)(codec) withReset resetFn withClose close |
| 167 | + } |
| 168 | + |
| 169 | + def fromInputStream(is: InputStream, enc: String): BufferedSource = |
| 170 | + fromInputStream(is)(Codec(enc)) |
| 171 | + |
| 172 | + def fromInputStream(is: InputStream)(implicit codec: Codec): BufferedSource = |
| 173 | + createBufferedSource(is, reset = () => fromInputStream(is)(codec), close = () => is.close())(codec) |
| 174 | + |
| 175 | + /** Reads data from a classpath resource, using either a context classloader (default) or a passed one. |
| 176 | + * |
| 177 | + * @param resource name of the resource to load from the classpath |
| 178 | + * @param classLoader classloader to be used, or context classloader if not specified |
| 179 | + * @return the buffered source |
| 180 | + */ |
| 181 | + def fromResource(resource: String, classLoader: ClassLoader = Thread.currentThread().getContextClassLoader())(implicit codec: Codec): BufferedSource = |
| 182 | + Option(classLoader.getResourceAsStream(resource)) match { |
| 183 | + case Some(in) => fromInputStream(in) |
| 184 | + case None => throw new FileNotFoundException(s"resource '$resource' was not found in the classpath from the given classloader.") |
| 185 | + } |
| 186 | + |
| 187 | +} |
| 188 | + |
| 189 | +/** An iterable representation of source data. |
| 190 | + * It may be reset with the optional [[reset]] method. |
| 191 | + * |
| 192 | + * Subclasses must supply [[scala.io.Source.iter the underlying iterator]]. |
| 193 | + * |
| 194 | + * Error handling may be customized by overriding the [[scala.io.Source.report report]] method. |
| 195 | + * |
| 196 | + * The [[scala.io.Source.ch current input]] and [[scala.io.Source.pos position]], |
| 197 | + * as well as the [[scala.io.Source.next next character]] methods delegate to |
| 198 | + * [[scala.io.Source#Positioner the positioner]]. |
| 199 | + * |
| 200 | + * The default positioner encodes line and column numbers in the position passed to [[report]]. |
| 201 | + * This behavior can be changed by supplying a |
| 202 | + * [[scala.io.Source.withPositioning(pos:* custom positioner]]. |
| 203 | + * |
| 204 | + */ |
| 205 | +abstract class Source extends Iterator[Char] with Closeable { |
| 206 | + /** the actual iterator */ |
| 207 | + protected val iter: Iterator[Char] |
| 208 | + |
| 209 | + // ------ public values |
| 210 | + |
| 211 | + /** description of this source, default empty */ |
| 212 | + var descr: String = "" |
| 213 | + var nerrors = 0 |
| 214 | + var nwarnings = 0 |
| 215 | + |
| 216 | + private def lineNum(line: Int): String = (getLines() drop (line - 1) take 1).mkString |
| 217 | + |
| 218 | + class LineIterator extends AbstractIterator[String] with Iterator[String] { |
| 219 | + private[this] val sb = new StringBuilder |
| 220 | + |
| 221 | + lazy val iter: BufferedIterator[Char] = Source.this.iter.buffered |
| 222 | + def isNewline(ch: Char): Boolean = ch == '\r' || ch == '\n' |
| 223 | + def getc(): Boolean = iter.hasNext && { |
| 224 | + val ch = iter.next() |
| 225 | + if (ch == '\n') false |
| 226 | + else if (ch == '\r') { |
| 227 | + if (iter.hasNext && iter.head == '\n') |
| 228 | + iter.next() |
| 229 | + |
| 230 | + false |
| 231 | + } |
| 232 | + else { |
| 233 | + sb append ch |
| 234 | + true |
| 235 | + } |
| 236 | + } |
| 237 | + def hasNext: Boolean = iter.hasNext |
| 238 | + def next(): String = { |
| 239 | + sb.clear() |
| 240 | + while (getc()) { } |
| 241 | + sb.toString |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + /** Returns an iterator who returns lines (NOT including newline character(s)). |
| 246 | + * It will treat any of \r\n, \r, or \n as a line separator (longest match) - if |
| 247 | + * you need more refined behavior you can subclass Source#LineIterator directly. |
| 248 | + */ |
| 249 | + def getLines(): Iterator[String] = new LineIterator() |
| 250 | + |
| 251 | + /** Returns `'''true'''` if this source has more characters. |
| 252 | + */ |
| 253 | + def hasNext: Boolean = iter.hasNext |
| 254 | + |
| 255 | + /** Returns next character. |
| 256 | + */ |
| 257 | + def next(): Char = positioner.next() |
| 258 | + |
| 259 | + @nowarn("cat=deprecation") |
| 260 | + class Positioner(encoder: Position) { |
| 261 | + def this() = this(RelaxedPosition) |
| 262 | + /** the last character returned by next. */ |
| 263 | + var ch: Char = _ |
| 264 | + |
| 265 | + /** position of last character returned by next */ |
| 266 | + var pos = 0 |
| 267 | + |
| 268 | + /** current line and column */ |
| 269 | + var cline = 1 |
| 270 | + var ccol = 1 |
| 271 | + |
| 272 | + /** default col increment for tabs '\t', set to 4 initially */ |
| 273 | + var tabinc = 4 |
| 274 | + |
| 275 | + def next(): Char = { |
| 276 | + ch = iter.next() |
| 277 | + pos = encoder.encode(cline, ccol) |
| 278 | + ch match { |
| 279 | + case '\n' => |
| 280 | + ccol = 1 |
| 281 | + cline += 1 |
| 282 | + case '\t' => |
| 283 | + ccol += tabinc |
| 284 | + case _ => |
| 285 | + ccol += 1 |
| 286 | + } |
| 287 | + ch |
| 288 | + } |
| 289 | + } |
| 290 | + /** A Position implementation which ignores errors in |
| 291 | + * the positions. |
| 292 | + */ |
| 293 | + @nowarn("cat=deprecation") |
| 294 | + object RelaxedPosition extends Position { |
| 295 | + private val _ = Source.this |
| 296 | + def checkInput(line: Int, column: Int): Unit = () |
| 297 | + } |
| 298 | + object RelaxedPositioner extends Positioner(RelaxedPosition) { } |
| 299 | + object NoPositioner extends Positioner(Position) { |
| 300 | + override def next(): Char = iter.next() |
| 301 | + } |
| 302 | + def ch: Char = positioner.ch |
| 303 | + def pos: Int = positioner.pos |
| 304 | + |
| 305 | + /** Reports an error message to the output stream `out`. |
| 306 | + * |
| 307 | + * @param pos the source position (line/column) |
| 308 | + * @param msg the error message to report |
| 309 | + * @param out PrintStream to use (optional: defaults to `Console.err`) |
| 310 | + */ |
| 311 | + def reportError( |
| 312 | + pos: Int, |
| 313 | + msg: String, |
| 314 | + out: PrintStream = Console.err): Unit = |
| 315 | + { |
| 316 | + nerrors += 1 |
| 317 | + report(pos, msg, out) |
| 318 | + } |
| 319 | + |
| 320 | + private def spaces(n: Int) = List.fill(n)(' ').mkString |
| 321 | + /** |
| 322 | + * @param pos the source position (line/column) |
| 323 | + * @param msg the error message to report |
| 324 | + * @param out PrintStream to use |
| 325 | + */ |
| 326 | + def report(pos: Int, msg: String, out: PrintStream): Unit = { |
| 327 | + val line = Position line pos |
| 328 | + val col = Position column pos |
| 329 | + |
| 330 | + out println "%s:%d:%d: %s%s%s^".format(descr, line, col, msg, lineNum(line), spaces(col - 1)) |
| 331 | + } |
| 332 | + |
| 333 | + /** |
| 334 | + * @param pos the source position (line/column) |
| 335 | + * @param msg the warning message to report |
| 336 | + * @param out PrintStream to use (optional: defaults to `Console.out`) |
| 337 | + */ |
| 338 | + def reportWarning( |
| 339 | + pos: Int, |
| 340 | + msg: String, |
| 341 | + out: PrintStream = Console.out): Unit = |
| 342 | + { |
| 343 | + nwarnings += 1 |
| 344 | + report(pos, "warning! " + msg, out) |
| 345 | + } |
| 346 | + |
| 347 | + private[this] var resetFunction: () => Source = null |
| 348 | + private[this] var closeFunction: () => Unit = null |
| 349 | + private[this] var positioner: Positioner = RelaxedPositioner |
| 350 | + |
| 351 | + def withReset(f: () => Source): this.type = { |
| 352 | + resetFunction = f |
| 353 | + this |
| 354 | + } |
| 355 | + def withClose(f: () => Unit): this.type = { |
| 356 | + closeFunction = f |
| 357 | + this |
| 358 | + } |
| 359 | + def withDescription(text: String): this.type = { |
| 360 | + descr = text |
| 361 | + this |
| 362 | + } |
| 363 | + /** Change or disable the positioner. */ |
| 364 | + def withPositioning(on: Boolean): this.type = { |
| 365 | + positioner = if (on) RelaxedPositioner else NoPositioner |
| 366 | + this |
| 367 | + } |
| 368 | + def withPositioning(pos: Positioner): this.type = { |
| 369 | + positioner = pos |
| 370 | + this |
| 371 | + } |
| 372 | + |
| 373 | + /** The close() method closes the underlying resource. */ |
| 374 | + def close(): Unit = { |
| 375 | + if (closeFunction != null) closeFunction() |
| 376 | + } |
| 377 | + |
| 378 | + /** The reset() method creates a fresh copy of this Source. */ |
| 379 | + def reset(): Source = |
| 380 | + if (resetFunction != null) resetFunction() |
| 381 | + else throw new UnsupportedOperationException("Source's reset() method was not set.") |
| 382 | +} |
0 commit comments