Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #126: Typeclassed ARM #159

Merged
merged 14 commits into from
May 23, 2017
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [Issue #152](https://github.com/pathikrit/better-files/issues/152): Streamed unzipping
* [Issue #150](https://github.com/pathikrit/better-files/issues/150): `ManagedResource[File]` for temp files
* [Issue #129](https://github.com/pathikrit/better-files/issues/129): JSR-203 and JimFS compatibility
* [Issue #126](https://github.com/pathikrit/better-files/pull/159): New Typeclassed approach to ARM

## v3.0.0

Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,15 +470,25 @@ for {
} foo(reader)

// or simply:
file.bufferedReader.map(foo)
file.bufferedReader.foreach(foo)
```
Or use a [utility to convert any closeable to an iterator](http://pathikrit.github.io/better-files/latest/api/better/files/Implicits$CloseableOps.html):

You can also define your own custom disposable resources e.g.:
```scala
val eof = -1
val bytes: Iterator[Byte] = inputStream.autoClosedIterator(_.read())(_ != eof).map(_.toByte)
trait Shutdownable {
def shutdown(): Unit = ()
}

object Shutdownable {
implicit val disposable: Disposable[Shutdownable] = Disposable(_.shutdown())
}

val s: Shutdownable = ....

for {
instance <- new ManagedResource(s)
} doSomething(s) // s is disposed after this
```
Note: The `autoClosedIterator` only closes the resource when `hasNext` i.e. `(_ != eof)` returns false.
If you only partially use the iterator e.g. `.take(5)`, it may leave the resource open. In those cases, use the managed `autoClosed` version instead.

### Scanner
Although [`java.util.Scanner`](http://docs.oracle.com/javase/8/docs/api/java/util/Scanner.html) has a feature-rich API, it only allows parsing primitives.
Expand Down
3 changes: 3 additions & 0 deletions akka/src/main/scala/better/files/FileWatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ object FileWatcher {
case class RemoveCallback(event: Event, callback: Callback) extends Message
}

implicit val disposeActorSystem: Disposable[ActorSystem] =
Disposable(_.terminate())

implicit class FileWatcherOps(file: File) {
def watcherProps(recursive: Boolean): Props =
Props(new FileWatcher(file, recursive))
Expand Down
18 changes: 7 additions & 11 deletions core/src/main/scala/better/files/File.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import scala.util.Properties
/**
* Scala wrapper around java.nio.files.Path
*/
class File private(val path: Path)(implicit val fileSystem: FileSystem = path.getFileSystem) extends AutoCloseable {
class File private(val path: Path)(implicit val fileSystem: FileSystem = path.getFileSystem) {
//TODO: LinkOption?

def pathAsString: String =
Expand Down Expand Up @@ -222,8 +222,8 @@ class File private(val path: Path)(implicit val fileSystem: FileSystem = path.ge
def lineIterator(implicit charset: Charset = File.defaultCharset): Iterator[String] =
Files.lines(path, charset).toAutoClosedIterator

def tokens(implicit config: Scanner.Config = Scanner.Config.default, charset: Charset = File.defaultCharset): Traversable[String] =
bufferedReader(charset).flatMap(_.tokens(config))
def tokens(implicit config: Scanner.Config = Scanner.Config.default, charset: Charset = File.defaultCharset): Iterator[String] =
newBufferedReader(charset).tokens(config)

def contentAsString(implicit charset: Charset = File.defaultCharset): String =
new String(byteArray, charset)
Expand Down Expand Up @@ -398,7 +398,7 @@ class File private(val path: Path)(implicit val fileSystem: FileSystem = path.ge
* @return
*/
def readDeserialized[A](implicit openOptions: File.OpenOptions = File.OpenOptions.default): A =
inputStream(openOptions).map(_.buffered.asObjectInputStream.readObject().asInstanceOf[A]).head
inputStream(openOptions).map(_.buffered.asObjectInputStream.readObject().asInstanceOf[A])

def register(service: WatchService, events: File.Events = File.Events.all): this.type = {
path.register(service, events.toArray)
Expand Down Expand Up @@ -483,7 +483,7 @@ class File private(val path: Path)(implicit val fileSystem: FileSystem = path.ge
}

def usingLock[U](mode: File.RandomAccessMode)(f: FileChannel => U): U =
using(newRandomAccess(mode).getChannel)(f)
newRandomAccess(mode).getChannel.autoClosed.map(f)

def isReadLocked(position: Long = 0L, size: Long = Long.MaxValue, isShared: Boolean = false) =
isLocked(File.RandomAccessMode.read, position, size, isShared)
Expand Down Expand Up @@ -903,16 +903,12 @@ class File private(val path: Path)(implicit val fileSystem: FileSystem = path.ge
* This util auto-deletes the resource when done using the ManagedResource facility
*
* Example usage:
* File.managedTemporaryDirectory().foreach(tempDir => doSomething(tempDir)
* File.temporaryDirectory().foreach(tempDir => doSomething(tempDir)
*
* @return
*/
def toTemporary: ManagedResource[File] =
this.autoClosed

override def close() = {
val _ = delete(swallowIOExceptions = true)
}
new ManagedResource(this)(Disposable.fileDisposer)

//TODO: add features from https://github.com/sbt/io
}
Expand Down
10 changes: 2 additions & 8 deletions core/src/main/scala/better/files/FileMonitor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package better.files
import java.nio.file._

import scala.concurrent.ExecutionContext
import scala.util.control.NonFatal
import scala.util.Try

/**
* Implementation of File.Monitor
Expand Down Expand Up @@ -48,13 +48,7 @@ abstract class FileMonitor(val root: File, maxDepth: Int) extends File.Monitor {
} else {
when(file.exists)(file.parent).iterator // There is no way to watch a regular file; so watch its parent instead
}
toWatch foreach {f =>
try {
f.register(service)
} catch {
case NonFatal(e) => onException(e)
}
}
toWatch.foreach(f => Try[Unit](f.register(service)).recover(PartialFunction(onException)).get)
}

override def start()(implicit executionContext: ExecutionContext) = {
Expand Down
95 changes: 22 additions & 73 deletions core/src/main/scala/better/files/Implicits.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package better.files

import java.io.{File => JFile, _}, StreamTokenizer.{TT_EOF => eof}
import java.io.{File => JFile, _}
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
import java.nio.charset.Charset
Expand All @@ -11,7 +11,8 @@ import java.util.stream.{Stream => JStream}
import java.util.zip._

import scala.annotation.tailrec
import scala.util.control.NonFatal
import scala.collection.JavaConverters._
import scala.util.Try

/**
* Container for various implicits
Expand Down Expand Up @@ -39,6 +40,15 @@ trait Implicits {
File(file.getPath)
}

//TODO: Rename all Ops to Extensions

implicit class IteratorExtensions[A](it: Iterator[A]) {
def withHasNext(f: => Boolean): Iterator[A] = new Iterator[A] {
override def hasNext = f && it.hasNext
override def next() = it.next()
}
}

implicit class InputStreamOps(in: InputStream) {
def pipeTo(out: OutputStream, bufferSize: Int = defaultBufferSize): Unit =
pipeTo(out, Array.ofDim[Byte](bufferSize))
Expand Down Expand Up @@ -73,7 +83,7 @@ trait Implicits {
reader(charset).buffered.lines().toAutoClosedIterator

def bytes: Iterator[Byte] =
in.autoClosedIterator(_.read())(_ != eof).map(_.toByte)
in.autoClosed.flatMap(res => eofReader(res.read()).map(_.toByte))
}

implicit class OutputStreamOps(val out: OutputStream) {
Expand Down Expand Up @@ -109,7 +119,7 @@ trait Implicits {

implicit class BufferedReaderOps(reader: BufferedReader) {
def chars: Iterator[Char] =
reader.autoClosedIterator(_.read())(_ != eof).map(_.toChar)
reader.autoClosed.flatMap(res => eofReader(res.read()).map(_.toChar))

private[files] def tokenizers(implicit config: Scanner.Config = Scanner.Config.default) =
reader.lines().toAutoClosedIterator.map(line => new StringTokenizer(line, config.delimiter, config.includeDelimiters))
Expand Down Expand Up @@ -180,13 +190,10 @@ trait Implicits {
override def hasNext = entry != null

override def next() = {
val result = try {
f(entry)
} finally {
val _ = scala.util.Try(in.closeEntry())
}
val result = Try(f(entry))
Try(in.closeEntry())
entry = in.getNextEntry
result
result.get
}
}
}
Expand Down Expand Up @@ -219,54 +226,8 @@ trait Implicits {
*
* @return
*/
def autoClosed: ManagedResource[A] = new Traversable[A] {
var isClosed = false
override def foreach[U](f: A => U) = try {
val _ = f(resource)
} finally {
if (!isClosed) {
resource.close()
isClosed = true
}
}
}

/**
* Provides an iterator that closes the underlying resource when done
*
* e.g.
* <pre>
* inputStream.autoClosedIterator(_.read())(_ != -1).map(_.toByte)
* </pre>
*
* @param generator next element from this resource
* @param isValidElement a function which tells if there is no more B left e.g. certain iterators may return nulls
* @tparam B
* @return An iterator that closes the underlying resource when done
*/
def autoClosedIterator[B](generator: A => B)(isValidElement: B => Boolean): Iterator[B] = {
var isClosed = false
def isOpen(item: B) = {
if (!isClosed && !isValidElement(item)) close()
!isClosed
}

def close() = try {
if (!isClosed) resource.close()
} finally {
isClosed = true
}

def next() = try {
generator(resource)
} catch {
case NonFatal(e) =>
close()
throw e
}

Iterator.continually(next()).takeWhile(isOpen)
}
def autoClosed: ManagedResource[A] =
new ManagedResource(resource)(Disposable.closableDisposer)
}

implicit class JStreamOps[A](stream: JStream[A]) {
Expand All @@ -276,20 +237,8 @@ trait Implicits {
*
* @return
*/
def toAutoClosedIterator: Iterator[A] = {
val iterator = stream.iterator()
var isOpen = true
produce(iterator.next()) till {
if (isOpen && !iterator.hasNext) {
try {
stream.close()
} finally {
isOpen = false
}
}
isOpen
}
}
def toAutoClosedIterator: Iterator[A] =
stream.autoClosed.flatMap(_.iterator().asScala)
}

private[files] implicit class OrderingOps[A](order: Ordering[A]) {
Expand All @@ -304,7 +253,7 @@ trait Implicits {
Charset.forName(charsetName)

implicit def tokenizerToIterator(s: StringTokenizer): Iterator[String] =
produce(s.nextToken()).till(s.hasMoreTokens)
Iterator.continually(s.nextToken()).withHasNext(s.hasMoreTokens)

//implicit def posixPermissionToFileAttribute(perm: PosixFilePermission) =
// PosixFilePermissions.asFileAttribute(Set(perm))
Expand Down
63 changes: 63 additions & 0 deletions core/src/main/scala/better/files/ManagedResource.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package better.files

import java.util.concurrent.atomic.AtomicBoolean

import scala.util.Try

/**
* A typeclass to denote a disposable resource
* @tparam A
*/
trait Disposable[-A] {
def dispose(resource: A): Unit

def disposeSilently(resource: A): Unit = {
val _ = Try(dispose(resource))
}
}

object Disposable {
def apply[A](disposeMethod: A => Any): Disposable[A] = new Disposable[A] {
override def dispose(resource: A) = {
val _ = disposeMethod(resource)
}
}

implicit val closableDisposer: Disposable[Closeable] =
Disposable(_.close())

val fileDisposer: Disposable[File] =
Disposable(_.delete(swallowIOExceptions = true))
}

class ManagedResource[A](resource: A)(implicit disposer: Disposable[A]) {
private[this] val isDisposed = new AtomicBoolean(false)
private[this] def disposeOnce() = if (!isDisposed.getAndSet(true)) disposer.disposeSilently(resource)

def foreach[U](f: A => U): Unit = {
val _ = map(f)
}

def map[B](f: A => B): B = {
val result = Try(f(resource))
disposeOnce()
result.get
}

/**
* This handles lazy operations (e.g. Iterators)
* for which resource needs to be disposed only after iteration is done
*
* @param f
* @tparam B
* @return
*/
def flatMap[B](f: A => Iterator[B]): Iterator[B] = {
val it = f(resource)
it withHasNext {
val result = Try(it.hasNext)
if (!result.getOrElse(false)) disposeOnce()
result.get
}
}
}
6 changes: 3 additions & 3 deletions core/src/main/scala/better/files/Scanner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ trait Scanner extends Iterator[String] with AutoCloseable {

def tillDelimiter(delimiter: String): String

def tillEndOfLine() = tillDelimiter(Scanner.Config.Delimiters.lines)
def tillEndOfLine(): String = tillDelimiter(Scanner.Config.Delimiters.lines)

def nonEmptyLines: Iterator[String] = produce(tillEndOfLine()).till(hasNext)
def nonEmptyLines: Iterator[String] = Iterator.continually(tillEndOfLine()).withHasNext(hasNext)
}

/**
Expand Down Expand Up @@ -86,7 +86,7 @@ object Scanner {
}

/**
* Implement this trait to make thing parseable
* Implement this trait to make thing parsable
*/
trait Scannable[A] {
def apply(scanner: Scanner): A
Expand Down
Loading