|
| 1 | +/* |
| 2 | + * The MIT License |
| 3 | + * |
| 4 | + * Copyright (c) 2022 Fulcrum Genomics |
| 5 | + * |
| 6 | + * Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 | + * of this software and associated documentation files (the "Software"), to deal |
| 8 | + * in the Software without restriction, including without limitation the rights |
| 9 | + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 10 | + * copies of the Software, and to permit persons to whom the Software is |
| 11 | + * furnished to do so, subject to the following conditions: |
| 12 | + * |
| 13 | + * The above copyright notice and this permission notice shall be included in |
| 14 | + * all copies or substantial portions of the Software. |
| 15 | + * |
| 16 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 17 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 18 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 19 | + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 20 | + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 | + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| 22 | + * THE SOFTWARE. |
| 23 | + * |
| 24 | + */ |
| 25 | + |
| 26 | +package com.fulcrumgenomics.util |
| 27 | + |
| 28 | +import com.fulcrumgenomics.cmdline.FgBioMain.FailureException |
| 29 | +import com.fulcrumgenomics.commons.CommonsDef.{forloop, unreachable} |
| 30 | +import com.fulcrumgenomics.commons.reflect.{ReflectionUtil, ReflectiveBuilder} |
| 31 | +import com.fulcrumgenomics.commons.util.LazyLogging |
| 32 | + |
| 33 | +import java.io.{PrintWriter, StringWriter} |
| 34 | +import scala.reflect.runtime.{universe => ru} |
| 35 | +import scala.util.{Failure, Success} |
| 36 | + |
| 37 | +/** Class for building metrics of type [[T]]. |
| 38 | + * |
| 39 | + * This is not thread-safe. |
| 40 | + * |
| 41 | + * @param source optionally, the source of reading (e.g. file) |
| 42 | + * @tparam T the metric type |
| 43 | + */ |
| 44 | +class MetricBuilder[T <: Metric](source: Option[String] = None)(implicit tt: ru.TypeTag[T]) extends LazyLogging { |
| 45 | + // The main reason why a builder is necessary is to cache some expensive reflective calls. |
| 46 | + private val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] |
| 47 | + private val reflectiveBuilder = new ReflectiveBuilder(clazz) |
| 48 | + private val names = Metric.names[T] |
| 49 | + |
| 50 | + /** Builds a metric from a delimited line |
| 51 | + * |
| 52 | + * @param line the line with delimited values |
| 53 | + * @param delim the delimiter of the values |
| 54 | + * @param lineNumber optionally, the line number when building a metric from a line in a file |
| 55 | + * @return |
| 56 | + */ |
| 57 | + def fromLine(line: String, delim: String = Metric.DelimiterAsString, lineNumber: Option[Int] = None): T = { |
| 58 | + fromValues(values = line.split(delim), lineNumber = lineNumber) |
| 59 | + } |
| 60 | + |
| 61 | + /** Builds a metric from values for the complete set of metric fields |
| 62 | + * |
| 63 | + * @param values the values in the same order as the names defined in the class |
| 64 | + * @param lineNumber optionally, the line number when building a metric from a line in a file |
| 65 | + * @return |
| 66 | + */ |
| 67 | + def fromValues(values: Iterable[String], lineNumber: Option[Int] = None): T = { |
| 68 | + val vals = values.toIndexedSeq |
| 69 | + if (names.length != vals.length) { |
| 70 | + fail(message = f"Failed decoding: expected '${names.length}' fields, found '${vals.length}'.", lineNumber = lineNumber) |
| 71 | + } |
| 72 | + fromArgMap(argMap = names.zip(values).toMap, lineNumber = lineNumber) |
| 73 | + } |
| 74 | + |
| 75 | + /** Builds a metric of type [[T]] |
| 76 | + * |
| 77 | + * @param argMap map of field names to values. All required fields must be given. Can be in any order. |
| 78 | + * @param lineNumber optionally, the line number when building a metric from a line in a file |
| 79 | + * @return a new instance of type [[T]] |
| 80 | + */ |
| 81 | + def fromArgMap(argMap: Map[String, String], lineNumber: Option[Int] = None): T = { |
| 82 | + reflectiveBuilder.reset() // reset the arguments to their initial values |
| 83 | + |
| 84 | + val names = argMap.keys.toIndexedSeq |
| 85 | + forloop(from = 0, until = names.length) { i => |
| 86 | + reflectiveBuilder.argumentLookup.forField(names(i)) match { |
| 87 | + case Some(arg) => |
| 88 | + val value = { |
| 89 | + val tmp = argMap(names(i)) |
| 90 | + if (tmp.isEmpty && arg.argumentType == classOf[Option[_]]) ReflectionUtil.SpecialEmptyOrNoneToken else tmp |
| 91 | + } |
| 92 | + |
| 93 | + val argumentValue = ReflectionUtil.constructFromString(arg.argumentType, arg.unitType, value) match { |
| 94 | + case Success(v) => v |
| 95 | + case Failure(thr) => |
| 96 | + fail( |
| 97 | + message = s"Could not construct value for column '${arg.name}' of type '${arg.typeDescription}' from '$value'", |
| 98 | + throwable = Some(thr), |
| 99 | + lineNumber = lineNumber |
| 100 | + ) |
| 101 | + } |
| 102 | + arg.value = argumentValue |
| 103 | + case None => |
| 104 | + fail( |
| 105 | + message = s"Did not have a field with name '${names(i)}'.", |
| 106 | + lineNumber = lineNumber |
| 107 | + ) |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + // build it. NB: if arguments are missing values, then an exception will be thrown here |
| 112 | + // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as |
| 113 | + // missing. |
| 114 | + val params = reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}")) |
| 115 | + reflectiveBuilder.build(params) |
| 116 | + } |
| 117 | + |
| 118 | + /** Logs the throwable, if given, and throws a [[FailureException]] with information about when reading metrics fails |
| 119 | + * |
| 120 | + * @param message the message to include in the exception thrown |
| 121 | + * @param throwable optionally, a throwable that should be logged |
| 122 | + * @param lineNumber optionally, the line number when building a metric from a line in a file |
| 123 | + */ |
| 124 | + def fail(message: String, throwable: Option[Throwable] = None, lineNumber: Option[Int] = None): Unit = { |
| 125 | + throwable.foreach { thr => |
| 126 | + val stringWriter = new StringWriter |
| 127 | + thr.printStackTrace(new PrintWriter(stringWriter)) |
| 128 | + val banner = "#" * 80 |
| 129 | + logger.debug(banner) |
| 130 | + logger.debug(stringWriter.toString) |
| 131 | + logger.debug(banner) |
| 132 | + } |
| 133 | + val sourceMessage = source.map("\nIn source: " + _).getOrElse("") |
| 134 | + val prefix = lineNumber match { |
| 135 | + case None => "For metric" |
| 136 | + case Some(n) => s"On line #$n for metric" |
| 137 | + } |
| 138 | + val fullMessage = s"$prefix '${clazz.getSimpleName}'$sourceMessage\n$message" |
| 139 | + |
| 140 | + throw FailureException(message = Some(fullMessage)) |
| 141 | + } |
| 142 | +} |
0 commit comments