app/helpers/RangeHeader.scala (78 lines of code) (raw):

package helpers import org.slf4j.LoggerFactory import scala.util.{Failure, Success, Try} //see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range object RangeHeader extends ((Option[Long],Option[Long])=>RangeHeader) { private val logger = LoggerFactory.getLogger(getClass) private val fullRangeXtractor = "^(\\d+)-(\\d+)".r private val partialStartRangeXtractor = "(\\d+)-$".r private val partialEndRangeXtractor = "^-(\\d+)$".r private val unitsXtractor = "^(\\w+)=([\\d,\\-\\s]+)$".r private val groupSeparator = "\\s*,\\s*".r protected def extractRange(rangePart:String):Try[RangeHeader] = rangePart match { case fullRangeXtractor(startStr:String, endStr:String)=> val hdr = new RangeHeader(Some(startStr.toLong), Some(endStr.toLong)) if(hdr.start.get>hdr.end.get){ Failure(new BadDataError("range start must be earlier than range end")) } else { Success(hdr) } case partialStartRangeXtractor(startStr:String)=> val startNum = startStr.toLong val hdr = new RangeHeader(Some(startNum), None) Success(hdr) case partialEndRangeXtractor(endStr:String)=> val endNum = endStr.toLong if(endNum>0){ Success(new RangeHeader(None, Some(endNum))) } else { Failure(new BadDataError("range end must be a positive integer")) } case _=> Failure(new BadDataError(s"incorrect range format '$rangePart'")) } /** * checks for any overlapping ranges in the headers and returns a Failure if so. * it's assumed that the heder sequence is sorted BEFORE going into this method * @param headers Array of RangeHeader structs * @return a Try containing the sorted range or a BadDataError */ protected def checkForOverlap(headers: Array[RangeHeader]):Try[Seq[RangeHeader]] = { if(headers.head.end.isEmpty && headers.length>1){ Failure(new BadDataError("First header specifies until end of file but there is more than one header")) } for(i <- 0 until headers.length-1){ if(i>0 && headers(i).start.isEmpty){ return Failure(new BadDataError("Open start range that is not the first in sequence")) } else if(i<headers.length-1 && headers(i).end.isEmpty) { return Failure(new BadDataError(s"Open end range at position $i / ${headers.length} that is not the last in sequence")) } else { if (headers(i).end.isDefined && headers(i + 1).start.isDefined) { if (headers(i).end.get > headers(i + 1).start.get) { return Failure(new BadDataError(s"Ranges ${headers(i).toString} and ${headers(i+1).toString} overlap")) } } } } Success(headers) } /** * parses a header string into a sequence of RangeHeader values * @param str * @return */ def fromStringHeader(str:String):Try[Seq[RangeHeader]] = { str match { case unitsXtractor(units:String,remainder:String)=> if(units.toLowerCase!="bytes"){ Failure(new BadDataError("only ranges in bytes are supported")) } else { val remainingParts = groupSeparator.split(remainder) val ranges = remainingParts.map(extractRange) val failures = ranges.collect({case Failure(err)=>err}) if(failures.nonEmpty){ Failure(failures.head) } else { val sortedRanges = ranges.collect({case Success(range)=>range}).sortBy(_.start) sortedRanges.foreach(h=>logger.debug(s"sorted range: $h")) checkForOverlap(sortedRanges) } } case _=>Failure(new RuntimeException(s"Could not get start and end parameters from $str")) } } } case class RangeHeader (start:Option[Long], end:Option[Long]) { /** * convenience method to always return values - if there is no set start point then the start point is 0, * if there is no set end point then the end point is the provided file length * @param fileLength file length as a Long integer * @return a tuple of (start,end) both as Long integers */ def getAbsolute(fileLength:Long):(Long,Long) = (start.getOrElse(0L), end.getOrElse(fileLength)) def headerString:String = { s"${start.map(_.toString).getOrElse("")}-${end.map(_.toString).getOrElse("")}" } }