¿Cómo puedo evitar las variables mutables en Scala cuando uso ZipInputStreams y ZipOutpuStreams?
immutability zipoutputstream (5)
Estoy tratando de leer un archivo zip, verifico que tenga algunos archivos requeridos y luego escribo todos los archivos válidos en otro archivo zip. La introducción básica a java.util.zip tiene muchos Java-isms y me encantaría que mi código fuera más nativo de Scala. Específicamente, me gustaría evitar el uso de vars
. Esto es lo que tengo:
val fos = new FileOutputStream("new.zip");
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos));
while (zipIn.available == 1) {
val entry = zipIn.getNextEntry
if (entryIsValid(entry)) {
zipOut.putNewEntry(new ZipEntry("subdir/" + entry.getName())
// read data into the data Array
var data = Array[Byte](1024)
var count = zipIn.read(data, 0, 1024)
while (count != -1) {
zipOut.write(data, 0, count)
count = zipIn.read(data, 0, 1024)
}
}
zipIn.close
}
zipOut.close
Debo añadir que estoy usando Scala 2.7.7.
Basado en http://harrah.github.io/browse/samples/compiler/scala/tools/nsc/io/ZipArchive.scala.html :
private[io] class ZipEntryTraversableClass(in: InputStream) extends Traversable[ZipEntry] {
val zis = new ZipInputStream(in)
def foreach[U](f: ZipEntry => U) {
@tailrec
def loop(x: ZipEntry): Unit = if (x != null) {
f(x)
zis.closeEntry()
loop(zis.getNextEntry())
}
loop(zis.getNextEntry())
}
def writeCurrentEntryTo(os: OutputStream) {
IOUtils.copy(zis, os)
}
}
Intentaría algo como esto (sí, más o menos la misma idea que tuvo sblundy ):
Iterator.continually {
val data = new Array[Byte](100)
zipIn.read(data) match {
case -1 => Array.empty[Byte]
case 0 => new Array[Byte](101) // just to filter it out
case n => java.util.Arrays.copyOf(data, n)
}
} filter (_.size != 101) takeWhile (_.nonEmpty)
Podría simplificarse como a continuación, pero no me gusta mucho. Prefiero que la read
no sea capaz de devolver 0 ...
Iterator.continually {
val data = new Array[Byte](100)
zipIn.read(data) match {
case -1 => new Array[Byte](101)
case n => java.util.Arrays.copyOf(data, n)
}
} takeWhile (_.size != 101)
No creo que haya nada particularmente incorrecto en el uso de clases de Java que estén diseñadas para funcionar de manera imperativa en la forma en que fueron diseñadas. Idiomatic Scala incluye ser capaz de usar Java idiomático como fue pensado, incluso si los estilos chocan un poco.
Sin embargo, si lo desea, tal vez como un ejercicio, o tal vez porque aclara ligeramente la lógica, hacer esto de una manera más funcional y libre de variantes, puede hacerlo. En 2.8, es particularmente bueno, así que aunque estés usando 2.7.7, daré una respuesta 2.8.
Primero, debemos configurar el problema, lo que no hiciste del todo, pero supongamos que tenemos algo como esto:
import java.io._
import java.util.zip._
import scala.collection.immutable.Stream
val fos = new FileOutputStream("new.zip")
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos))
val zipIn = new ZipInputStream(new FileInputStream("old.zip"))
def entryIsValid(ze: ZipEntry) = !ze.isDirectory
Ahora, dado esto queremos copiar el archivo zip. El truco que podemos usar es el método continually
en collection.immutable.Stream
. Lo que hace es realizar un bucle evaluado perezosamente para ti. Luego, puede tomar y filtrar los resultados para terminar y procesar lo que desea. Es un patrón útil para usar cuando tienes algo que quieres que sea un iterador, pero no lo es. (Si el elemento se actualiza solo, puede usar .iterate
en Iterable
o Iterator, que generalmente es aún mejor). Aquí está la aplicación para este caso, que se usa dos veces: una para obtener las entradas y una para leer / escribir fragmentos de datos:
val buffer = new Array[Byte](1024)
Stream.continually(zipIn.getNextEntry).
takeWhile(_ != null).filter(entryIsValid).
foreach(entry => {
zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName))
Stream.continually(zipIn.read(buffer)).takeWhile(_ != -1).
foreach(count => zipOut.write(buffer,0,count))
})
}
zipIn.close
zipOut.close
Preste mucha atención a la .
Al final de unas líneas! Normalmente escribiría esto en una línea larga, pero es mejor tenerlo envuelto para que pueda verlo todo aquí.
En caso de que no quede claro, desempaquetemos uno de los usos de forma continually
.
Stream.continually(zipIn.read(buffer))
Esto le pide que siga llamando a zipIn.read(buffer)
tantas veces como sea necesario, almacenando el número entero resultante.
.takeWhile(_ != -1)
Esto especifica cuántas veces son necesarias, devolviendo una secuencia de longitud indefinida pero que se cerrará cuando llegue a -1
.
.foreach(count => zipOut.write(buffer,0,count))
Esto procesa el flujo, tomando cada elemento por turno (el conteo) y usándolo para escribir el búfer. Esto funciona de una manera un poco astuta, ya que confía en el hecho de que se acaba de llamar a zipIn
para obtener el siguiente elemento de la secuencia: si intentara hacer esto nuevamente, no en un solo paso a través de la secuencia, fallaría porque el buffer
sería sobrescrito. Pero aquí está bien.
Entonces, ahí está: un método un poco más compacto, posiblemente más fácil de entender, y posiblemente menos fácil de entender, que sea más funcional (aunque todavía hay muchos efectos secundarios). En la versión 2.7.7, en cambio, lo haría a la manera de Java porque Stream.continually
no está disponible, y la sobrecarga de construir un Iterator
personalizado no vale la pena en este caso. (Valdría la pena si iba a hacer más procesamiento de archivos zip y pudiera reutilizar el código, sin embargo).
Edición: El método de buscar-disponible-para-ir-cero es algo inestable para detectar el final del archivo zip. Creo que la forma "correcta" es esperar hasta que obtengas un null
de getNextEntry
. Con eso en mente, he editado el código anterior (había un takeWhile(_ => zipIn.available==1)
que ahora es un takeWhile(_ != null)
) y proporcioné una versión basada en el iterador 2.7.7 a continuación (tenga en cuenta lo pequeño que es el bucle principal, una vez que haya terminado con el trabajo de definición de los iteradores, que sí utilizan vars):
val buffer = new Array[Byte](1024)
class ZipIter(zis: ZipInputStream) extends Iterator[ZipEntry] {
private var entry:ZipEntry = zis.getNextEntry
private var cached = true
private def cache { if (entry != null && !cached) {
cached = true; entry = zis.getNextEntry
}}
def hasNext = { cache; entry != null }
def next = {
if (!cached) cache
cached = false
entry
}
}
class DataIter(is: InputStream, ab: Array[Byte]) extends Iterator[(Int,Array[Byte])] {
private var count = 0
private var waiting = false
def hasNext = {
if (!waiting && count != -1) { count = is.read(ab); waiting=true }
count != -1
}
def next = { waiting=false; (count,ab) }
}
(new ZipIter(zipIn)).filter(entryIsValid).foreach(entry => {
zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName))
(new DataIter(zipIn,buffer)).foreach(cb => zipOut.write(cb._2,0,cb._1))
})
zipIn.close
zipOut.close
Sin la recursión de la cola, evitaría la recursión. Usted correría el riesgo de un desbordamiento de pila. Podría envolver zipIn.read(data)
en un scala.BufferedIterator[Byte]
e ir desde allí.
Usando scala2.8 y cola recursiva llamada:
def copyZip(in: ZipInputStream, out: ZipOutputStream, bufferSize: Int = 1024) {
val data = new Array[Byte](bufferSize)
def copyEntry() {
in getNextEntry match {
case null =>
case entry => {
if (entryIsValid(entry)) {
out.putNextEntry(new ZipEntry("subdir/" + entry.getName()))
def copyData() {
in read data match {
case -1 =>
case count => {
out.write(data, 0, count)
copyData()
}
}
}
copyData()
}
copyEntry()
}
}
}
copyEntry()
}