提交 dc6bcee1 编写于 作者: 梦境迷离's avatar 梦境迷离

support read/write file

上级 c62edcd9
......@@ -13,6 +13,7 @@
.idea/
*.iml
.bsp
*.csv
examples/scala2-11/target/
examples/scala2-12/target/
examples/scala2-13/target/
......
......@@ -11,7 +11,7 @@ ThisBuild / resolvers ++= Seq(
lazy val scala212 = "2.12.14"
lazy val scala211 = "2.11.12"
lazy val scala213 = "2.13.8"
lazy val lastVersionForExamples = "0.4.2"
lazy val lastVersionForExamples = "0.5.2"
lazy val scalatestVersion = "3.2.12"
lazy val zioVersion = "1.0.14"
......
......@@ -31,6 +31,8 @@ import org.bitlap.csv.core.macros.DeriveCsvableBuilder
*/
class CsvableBuilder[T] {
import java.io.File
/**
* Convert this CSV column string to any Scala types.
*
......@@ -70,6 +72,15 @@ class CsvableBuilder[T] {
*/
def convert(ts: List[T]): String = macro DeriveCsvableBuilder.convertDefaultImpl[T]
/**
* Convert the sequence of Scala case class to CSV string and write to file.
*
* @param ts The sequence of Scala case class.
* @param file File to save CSV string.
* @return
*/
def writeTo(ts: List[T], file: File): Boolean = macro DeriveCsvableBuilder.writeToFileImpl[T]
}
object CsvableBuilder {
......
/*
* Copyright (c) 2022 bitlap
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.bitlap.csv.core
import java.io._
import scala.io.Source
import scala.language.reflectiveCalls
import scala.util.control.Exception.ignoring
import scala.collection.mutable.ListBuffer
/**
* @author 梦境迷离
* @version 1.0,5/13/22
*/
object FileUtils {
type Closable = {
def close(): Unit
}
def using[R <: Closable, T](resource: => R)(f: R => T): T =
try f(resource)
finally ignoring(classOf[Throwable]) apply {
resource.close()
}
def writer(file: File, lines: List[String]): Boolean = {
checkFile(file)
val bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file))
try using(new PrintWriter(bufferedOutputStream, true)) { r =>
lines.foreach(r.println)
} finally bufferedOutputStream.close()
true
}
def reader(file: InputStream, charset: String = "UTF-8"): List[String] =
try using(Source.fromInputStream(new BufferedInputStream(file), charset)) { lines =>
lines.getLines().toList
} finally file.close()
def checkFile(file: File): Unit = {
if (file.isDirectory) {
throw new Exception(s"File path: $file is a directory.")
}
if (!file.exists()) {
file.createNewFile()
}
}
def readFileFunc[T](reader: BufferedReader, func: String => Option[T]): List[Option[T]] = {
val ts = ListBuffer[Option[T]]()
var line: String = null
FileUtils.using(new BufferedReader(reader)) { input =>
while ({
line = input.readLine()
line != null
})
ts.append(func(line))
}
ts.result()
}
}
......@@ -22,6 +22,7 @@
package org.bitlap.csv.core
import org.bitlap.csv.core.macros.DeriveScalableBuilder
import java.io.InputStream
/**
* Builder to create a custom Csv Decoder.
......@@ -59,7 +60,7 @@ class ScalableBuilder[T] {
/**
* Convert all CSV lines to the sequence of Scala case class.
*
* @param lines All CSV lines.
* @param lines All CSV lines.
* @param columnSeparator The separator for CSV column value.
* @return
*/
......@@ -70,6 +71,15 @@ class ScalableBuilder[T] {
*/
def convert(lines: List[String]): List[Option[T]] = macro DeriveScalableBuilder.convertDefaultImpl[T]
/**
* Read all CSV lines of the file and convert them to the sequence of Scala case class.
*
* @param file InputStream of the CSV file.
* @param charset String charset of the CSV file content.
* @return
*/
def readFrom(file: InputStream, charset: String): List[Option[T]] = macro DeriveScalableBuilder.readFromFileImpl[T]
}
object ScalableBuilder {
......
......@@ -21,32 +21,23 @@
package org.bitlap.csv.core
import java.io.{ BufferedReader, InputStreamReader }
import scala.collection.mutable.ListBuffer
import scala.util.Using
import java.io.{ BufferedReader, File, FileReader, InputStreamReader }
/**
* Tool class for parsing CSV files.
*
* @author 梦境迷离
* @version 1.0,2022/5/2
* @version 1.0,2022/5/13
*/
object ScalableHelper {
def readCsvFromClassPath[T <: Product](fileName: String)(func: String => Option[T]): List[Option[T]] = {
val ts = ListBuffer[Option[T]]()
val reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(fileName))
val bufferedReader = new BufferedReader(reader)
var line: String = null
Using.resource(bufferedReader) { input =>
while ({
line = input.readLine()
line != null
})
ts.append(func(line))
}
ts.result()
FileUtils.readFileFunc[T](new BufferedReader(reader), func)
}
def readCsvFromFile[T <: Product](file: File)(func: String => Option[T]): List[Option[T]] = {
val reader = new BufferedReader(new FileReader(file))
FileUtils.readFileFunc[T](reader, func)
}
}
......@@ -25,6 +25,7 @@ import org.bitlap.csv.core.{ Csvable, CsvableBuilder }
import scala.collection.mutable
import scala.reflect.macros.whitebox
import java.io.File
/**
* @author 梦境迷离
......@@ -44,10 +45,8 @@ class DeriveCsvableBuilder(override val c: whitebox.Context) extends AbstractMac
private val csvableImplClassNamePrefix = "_CsvAnno$"
private val funcArgsTempTermName = TermName("temp")
def setFieldImpl[T: WeakTypeTag, SF: WeakTypeTag](
scalaField: Expr[T => SF],
value: Expr[SF => String]
): Expr[CsvableBuilder[T]] = {
// scalafmt: { maxColumn = 400 }
def setFieldImpl[T: WeakTypeTag, SF: WeakTypeTag](scalaField: Expr[T => SF], value: Expr[SF => String]): Expr[CsvableBuilder[T]] = {
val Function(_, Select(_, termName)) = scalaField.tree
val builderId = getBuilderId(annoBuilderPrefix)
MacroCache.builderFunctionTrees.getOrElseUpdate(builderId, mutable.Map.empty).update(termName.toString, value)
......@@ -70,6 +69,9 @@ class DeriveCsvableBuilder(override val c: whitebox.Context) extends AbstractMac
def convertDefaultImpl[T: WeakTypeTag](ts: Expr[List[T]]): Expr[String] =
deriveFullCsvableImpl[T](ts, c.Expr[Char](q"','"))
def writeToFileImpl[T: WeakTypeTag](ts: Expr[List[T]], file: Expr[File]): Expr[Boolean] =
deriveFullIntoFileCsvableImpl[T](ts, file, c.Expr[Char](q"','"))
private def deriveBuilderApplyImpl[T: WeakTypeTag]: Expr[CsvableBuilder[T]] = {
val className = TypeName(annoBuilderPrefix + MacroCache.getBuilderId)
val caseClazzName = TypeName(weakTypeOf[T].typeSymbol.name.decodedName.toString)
......@@ -93,29 +95,30 @@ class DeriveCsvableBuilder(override val c: whitebox.Context) extends AbstractMac
customTrees -> preTrees
}
private def deriveFullCsvableImpl[T: WeakTypeTag](
ts: Expr[List[T]],
columnSeparator: Expr[Char]
): Expr[String] = {
// scalafmt: { maxColumn = 400 }
private def deriveFullIntoFileCsvableImpl[T: WeakTypeTag](ts: Expr[List[T]], file: Expr[File], columnSeparator: Expr[Char]): Expr[Boolean] = {
val clazzName = resolveClazzTypeName[T]
val (customTrees, preTrees) = getCustomPreTress
val tree = q"""
..$preTrees
..${getAnnoClassObject[T](customTrees, columnSeparator)}
$packageName.FileUtils.writer($file, $ts.map { ($innerTName: $clazzName) =>
$csvableInstanceTermName.$innerTmpTermName = $innerTName
$csvableInstanceTermName.toCsvString
}
)
"""
exprPrintTree[Boolean](force = false, tree)
}
// scalafmt: { maxColumn = 400 }
private def deriveFullCsvableImpl[T: WeakTypeTag](ts: Expr[List[T]], columnSeparator: Expr[Char]): Expr[String] = {
val clazzName = resolveClazzTypeName[T]
val (customTrees, preTrees) = getCustomPreTress
val annoClassName = TermName(csvableImplClassNamePrefix + MacroCache.getIdentityId)
val separator = q"$columnSeparator"
val tree =
q"""
..$preTrees
object $annoClassName extends $packageName.Csvable[$clazzName] {
var $innerTmpTermName: $clazzName = _
lazy private val toCsv = ($funcArgsTempTermName: $clazzName) => {
val fields = ${clazzName.toTermName}.unapply($funcArgsTempTermName).orNull
if (null == fields) "" else ${fieldsToString[T](funcArgsTempTermName, customTrees)}.mkString($separator.toString)
}
override def toCsvString: String = toCsv($annoClassName.$innerTmpTermName)
}
final lazy private val $csvableInstanceTermName = $annoClassName
..${getAnnoClassObject[T](customTrees, columnSeparator)}
$ts.map { ($innerTName: $clazzName) =>
$csvableInstanceTermName.$innerTmpTermName = $innerTName
$csvableInstanceTermName.toCsvString
......@@ -124,6 +127,25 @@ class DeriveCsvableBuilder(override val c: whitebox.Context) extends AbstractMac
exprPrintTree[String](force = false, tree)
}
private def getAnnoClassObject[T: WeakTypeTag](customTrees: mutable.Map[String, Any], columnSeparator: Expr[Char]): Tree = {
val clazzName = resolveClazzTypeName[T]
val annoClassName = TermName(csvableImplClassNamePrefix + MacroCache.getIdentityId)
val separator = q"$columnSeparator"
q"""
object $annoClassName extends $packageName.Csvable[$clazzName] {
var $innerTmpTermName: $clazzName = _
lazy private val toCsv = ($funcArgsTempTermName: $clazzName) => {
val fields = ${clazzName.toTermName}.unapply($funcArgsTempTermName).orNull
if (null == fields) "" else ${fieldsToString[T](funcArgsTempTermName, customTrees)}.mkString($separator.toString)
}
override def toCsvString: String = toCsv($annoClassName.$innerTmpTermName)
}
final lazy private val $csvableInstanceTermName = $annoClassName
"""
}
private def deriveCsvableImpl[T: WeakTypeTag](t: Expr[T], columnSeparator: Expr[Char]): Expr[Csvable[T]] = {
val clazzName = resolveClazzTypeName[T]
val (customTrees, preTrees) = getCustomPreTress
......@@ -146,16 +168,13 @@ class DeriveCsvableBuilder(override val c: whitebox.Context) extends AbstractMac
exprPrintTree[Csvable[T]](force = false, tree)
}
private def fieldsToString[T: WeakTypeTag](
innerVarTermName: TermName,
customTrees: mutable.Map[String, Any]
): List[Tree] = {
// scalafmt: { maxColumn = 400 }
private def fieldsToString[T: WeakTypeTag](innerVarTermName: TermName, customTrees: mutable.Map[String, Any]): List[Tree] = {
val clazzName = resolveClazzTypeName[T]
val (fieldNames, indexTypes) = checkCaseClassZip
val indexByName = (i: Int) => TermName(fieldNames(i))
indexTypes.map { idxType =>
val customFunction = () =>
q"${TermName(builderFunctionPrefix + fieldNames(idxType._1))}.apply($innerVarTermName.${indexByName(idxType._1)})"
val customFunction = () => q"${TermName(builderFunctionPrefix + fieldNames(idxType._1))}.apply($innerVarTermName.${indexByName(idxType._1)})"
idxType._2 match {
case t if t <:< typeOf[List[_]] =>
if (customTrees.contains(fieldNames(idxType._1))) {
......@@ -180,11 +199,10 @@ class DeriveCsvableBuilder(override val c: whitebox.Context) extends AbstractMac
if (customTrees.contains(fieldNames(idxType._1))) {
customFunction()
} else {
// scalafmt: { maxColumn = 400 }
q"""
$packageName.Csvable[${genericType.typeSymbol.name.toTypeName}]._toCsvString {
if ($innerVarTermName.${indexByName(idxType._1)}.isEmpty) "" else $innerVarTermName.${indexByName(
idxType._1
)}.get
if ($innerVarTermName.${indexByName(idxType._1)}.isEmpty) "" else $innerVarTermName.${indexByName(idxType._1)}.get
}
"""
}
......
......@@ -23,6 +23,7 @@ package org.bitlap.csv.core.macros
import org.bitlap.csv.core.{ Scalable, ScalableBuilder }
import java.io.InputStream
import scala.collection.mutable
import scala.reflect.macros.whitebox
......@@ -39,16 +40,13 @@ class DeriveScalableBuilder(override val c: whitebox.Context) extends AbstractMa
private val builderFunctionPrefix = "_ScalableBuilderFunction$"
private val innerColumnFuncTermName = TermName("_columns")
private val innerLName = q"_l"
private val innerTempTermName = TermName("_line")
private val scalableInstanceTermName = TermName("_scalableInstance")
private val scalableImplClassNamePrefix = "_ScalaAnno$"
def setFieldImpl[T: WeakTypeTag, SF: WeakTypeTag](
scalaField: Expr[T => SF],
value: Expr[String => SF]
): Expr[ScalableBuilder[T]] = {
// scalafmt: { maxColumn = 400 }
def setFieldImpl[T: WeakTypeTag, SF: WeakTypeTag](scalaField: Expr[T => SF], value: Expr[String => SF]): Expr[ScalableBuilder[T]] = {
val Function(_, Select(_, termName)) = scalaField.tree
val builderId = getBuilderId(annoBuilderPrefix)
MacroCache.builderFunctionTrees.getOrElseUpdate(builderId, mutable.Map.empty).update(termName.toString, value)
......@@ -79,6 +77,11 @@ class DeriveScalableBuilder(override val c: whitebox.Context) extends AbstractMa
deriveScalableImpl[T](clazzName, line, c.Expr[Char](q"','"))
}
def readFromFileImpl[T: WeakTypeTag](file: Expr[InputStream], charset: Expr[String]): Expr[List[Option[T]]] = {
val clazzName = resolveClazzTypeName[T]
deriveFullFromFileScalableImpl[T](clazzName, file, charset, c.Expr[Char](q"','"))
}
private def deriveBuilderApplyImpl[T: WeakTypeTag]: Expr[ScalableBuilder[T]] = {
val className = TypeName(annoBuilderPrefix + MacroCache.getBuilderId)
val caseClazzName = TypeName(weakTypeOf[T].typeSymbol.name.decodedName.toString)
......@@ -102,24 +105,28 @@ class DeriveScalableBuilder(override val c: whitebox.Context) extends AbstractMa
preTrees
}
private def deriveFullScalableImpl[T: WeakTypeTag](
clazzName: TypeName,
lines: Expr[List[String]],
columnSeparator: Expr[Char]
): Expr[List[Option[T]]] = {
val annoClassName = TermName(scalableImplClassNamePrefix + MacroCache.getIdentityId)
// scalafmt: { maxColumn = 400 }
private def deriveFullFromFileScalableImpl[T: WeakTypeTag](clazzName: TypeName, file: Expr[InputStream], charset: Expr[String], columnSeparator: Expr[Char]): Expr[List[Option[T]]] = {
// NOTE: preTrees must be at the same level as Scalable
val tree =
q"""
..$getPreTree
object $annoClassName extends $packageName.Scalable[$clazzName] {
var $innerTempTermName: String = _
private val $innerColumnFuncTermName = () => _root_.org.bitlap.csv.core.StringUtils.splitColumns(${annoClassName.toTermName}.$innerTempTermName, $columnSeparator)
..${scalableBody[T](clazzName, innerColumnFuncTermName)}
..${getAnnoClassObject[T](clazzName, columnSeparator)}
$packageName.FileUtils.reader($file, $charset).map { ($innerLName: String) =>
$scalableInstanceTermName.$innerTempTermName = ${TermName(innerLName.toString())}
$scalableInstanceTermName.toScala
}
private final lazy val $scalableInstanceTermName = $annoClassName
"""
exprPrintTree[List[Option[T]]](force = false, tree)
}
// scalafmt: { maxColumn = 400 }
private def deriveFullScalableImpl[T: WeakTypeTag](clazzName: TypeName, lines: Expr[List[String]], columnSeparator: Expr[Char]): Expr[List[Option[T]]] = {
// NOTE: preTrees must be at the same level as Scalable
val tree =
q"""
..$getPreTree
..${getAnnoClassObject[T](clazzName, columnSeparator)}
$lines.map { ($innerLName: String) =>
$scalableInstanceTermName.$innerTempTermName = ${TermName(innerLName.toString())}
$scalableInstanceTermName.toScala
......@@ -128,18 +135,27 @@ class DeriveScalableBuilder(override val c: whitebox.Context) extends AbstractMa
exprPrintTree[List[Option[T]]](force = false, tree)
}
private def deriveScalableImpl[T: WeakTypeTag](
clazzName: TypeName,
line: Expr[String],
columnSeparator: Expr[Char]
): Expr[Scalable[T]] = {
private def getAnnoClassObject[T: WeakTypeTag](clazzName: TypeName, columnSeparator: Expr[Char]): Tree = {
val annoClassName = TermName(scalableImplClassNamePrefix + MacroCache.getIdentityId)
q"""
object $annoClassName extends $packageName.Scalable[$clazzName] {
var $innerTempTermName: String = _
private val $innerColumnFuncTermName = () => $packageName.StringUtils.splitColumns(${annoClassName.toTermName}.$innerTempTermName, $columnSeparator)
..${scalableBody[T](clazzName, innerColumnFuncTermName)}
}
private final lazy val $scalableInstanceTermName = $annoClassName
"""
}
// scalafmt: { maxColumn = 400 }
private def deriveScalableImpl[T: WeakTypeTag](clazzName: TypeName, line: Expr[String], columnSeparator: Expr[Char]): Expr[Scalable[T]] = {
val annoClassName = TermName(scalableImplClassNamePrefix + MacroCache.getIdentityId)
// NOTE: preTrees must be at the same level as Scalable
val tree =
q"""
..$getPreTree
object $annoClassName extends $packageName.Scalable[$clazzName] {
final lazy private val $innerColumnFuncTermName = () => _root_.org.bitlap.csv.core.StringUtils.splitColumns($line, $columnSeparator)
final lazy private val $innerColumnFuncTermName = () => $packageName.StringUtils.splitColumns($line, $columnSeparator)
..${scalableBody[T](clazzName, innerColumnFuncTermName)}
}
$annoClassName
......@@ -147,10 +163,8 @@ class DeriveScalableBuilder(override val c: whitebox.Context) extends AbstractMa
exprPrintTree[Scalable[T]](force = false, tree)
}
private def scalableBody[T: WeakTypeTag](
clazzName: TypeName,
innerFuncTermName: TermName
): Tree = {
// scalafmt: { maxColumn = 400 }
private def scalableBody[T: WeakTypeTag](clazzName: TypeName, innerFuncTermName: TermName): Tree = {
val customTrees = MacroCache.builderFunctionTrees.getOrElse(getBuilderId(annoBuilderPrefix), mutable.Map.empty)
val params = getCaseClassParams[T]()
val fieldNames = params.map(_.name.decodedName.toString)
......
......@@ -34,10 +34,8 @@ object DeriveToCaseClass {
class Macro(override val c: blackbox.Context) extends AbstractMacroProcessor(c) {
import c.universe._
def macroImpl[T <: Product: c.WeakTypeTag](
line: c.Expr[String],
columnSeparator: c.Expr[Char]
): c.Expr[Option[T]] = {
// scalafmt: { maxColumn = 400 }
def macroImpl[T <: Product: c.WeakTypeTag](line: c.Expr[String], columnSeparator: c.Expr[Char]): c.Expr[Option[T]] = {
val clazzName = c.weakTypeOf[T].typeSymbol.name
val innerFuncTermName = TermName("_columns")
val fields = (columnsFunc: TermName) =>
......@@ -72,7 +70,7 @@ object DeriveToCaseClass {
}
val tree =
q"""
lazy val $innerFuncTermName = () => _root_.org.bitlap.csv.core.StringUtils.splitColumns($line, $columnSeparator)
lazy val $innerFuncTermName = () => $packageName.StringUtils.splitColumns($line, $columnSeparator)
Option(${TermName(clazzName.decodedName.toString)}(..${fields(innerFuncTermName)}))
"""
exprPrintTree[T](force = false, tree)
......
......@@ -43,12 +43,11 @@ object DeriveToString {
val fieldsToString = indexTypes.map { idxType =>
if (idxType._2 <:< typeOf[Option[_]]) {
val genericType = c.typecheck(q"${idxType._2}", c.TYPEmode).tpe.typeArgs.head
// scalafmt: { maxColumn = 400 }
q"""$packageName.Converter[${genericType.typeSymbol.name.toTypeName}].toCsvString {
if ($innerVarTermName.${indexByName(idxType._1)}.isEmpty) "" else $innerVarTermName.${indexByName(
idxType._1
)}.get
}
"""
if ($innerVarTermName.${indexByName(idxType._1)}.isEmpty) "" else $innerVarTermName.${indexByName(idxType._1)}.get
}
"""
} else {
q"$packageName.Converter[${TypeName(idxType._2.typeSymbol.name.decodedName.toString)}].toCsvString($innerVarTermName.${indexByName(idxType._1)})"
}
......
......@@ -28,6 +28,7 @@ import org.scalatest.matchers.should.Matchers
import org.bitlap.csv.core.ScalableBuilder
import org.bitlap.csv.core.CsvableBuilder
import org.bitlap.csv.core.ScalableHelper
import java.io.File
/**
* Complex use of common tests
......@@ -212,6 +213,7 @@ class CsvableAndScalableTest extends AnyFlatSpec with Matchers {
object _ScalaAnno$1 extends _root_.org.bitlap.csv.core.Scalable[Metric2] {
var _line: String = _;
private val _columns = (() => _root_.org.bitlap.csv.core.StringUtils.splitColumns(_ScalaAnno$1._line, ','));
override def toScala: Option[Metric2] = Option(
Metric2(
_root_.org.bitlap.csv.core.Scalable[Long]._toScala(_columns()(0)).getOrElse(0L),
......@@ -266,6 +268,7 @@ class CsvableAndScalableTest extends AnyFlatSpec with Matchers {
)
.mkString(','.toString)
});
override def toCsvString: String = toCsv(_CsvAnno$2._tt)
};
lazy val _csvableInstance = _CsvAnno$2;
......@@ -280,4 +283,28 @@ class CsvableAndScalableTest extends AnyFlatSpec with Matchers {
println(csv)
}
"CsvableAndScalable8" should "ok when reading from file" in {
val metrics =
ScalableBuilder[Metric2]
.setField[Seq[Dimension3]](
_.dimensions,
dims => StringUtils.extractJsonValues[Dimension3](dims)((k, v) => Dimension3(k, v))
)
.readFrom(ClassLoader.getSystemResourceAsStream("simple_data.csv"), "utf-8")
println(metrics)
assert(metrics.nonEmpty)
val file = new File("./simple_data.csv")
CsvableBuilder[Metric2]
.setField[Seq[Dimension3]](
_.dimensions,
ds => s"""\"{${ds.map(kv => s"""\"\"${kv.key}\"\":\"\"${kv.value}\"\"""").mkString(",")}}\""""
)
.writeTo(metrics.filter(_.isDefined).map(_.get), file)
file.delete()
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册