alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Play Framework/Scala example source code file (MediaRange.scala)

This example Play Framework source code file (MediaRange.scala) is included in my "Source Code Warehouse" project. The intent of this project is to help you more easily find Play Framework (and Scala) source code examples by using tags.

All credit for the original source code belongs to Play Framework; I'm just trying to make examples easier to find. (For my Scala work, see my Scala examples and tutorials.)

Play Framework tags/keywords

api, boolean, char, expected, mediatype, none, option, parse, parser, play, play framework, seq, string, unable, utilities

The MediaRange.scala Play Framework example source code

/*
 * Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
 */
package play.api.http

import play.api.Play
import scala.util.parsing.input.CharSequenceReader
import scala.util.parsing.combinator.Parsers
import scala.collection.BitSet
import play.api.http.MediaRange.MediaRangeParser

/**
 * A media type as defined by RFC 2616 3.7.
 *
 * @param mediaType The media type
 * @param mediaSubType The media sub type
 * @param parameters The parameters
 */
case class MediaType(mediaType: String, mediaSubType: String, parameters: Seq[(String, Option[String])]) {
  override def toString = {
    mediaType + "/" + mediaSubType + parameters.map { param =>
      "; " + param._1 + param._2.map { value =>
        if (MediaRangeParser.token(new CharSequenceReader(value)).next.atEnd) {
          "=" + value
        } else {
          "=\"" + value.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"") + "\""
        }
      }.getOrElse("")
    }.mkString("")
  }
}

/**
 * A media range as defined by RFC 2616 14.1
 *
 * @param mediaType The media type
 * @param mediaSubType The media sub type
 * @param parameters The parameters
 * @param qValue The Q value
 * @param acceptExtensions The accept extensions
 */
class MediaRange(mediaType: String,
    mediaSubType: String,
    parameters: Seq[(String, Option[String])],
    val qValue: Option[BigDecimal],
    val acceptExtensions: Seq[(String, Option[String])]) extends MediaType(mediaType, mediaSubType, parameters) {

  /**
   * @return true if `mimeType` matches this media type, otherwise false
   */
  def accepts(mimeType: String): Boolean =
    (mediaType + "/" + mediaSubType).equalsIgnoreCase(mimeType) ||
      (mediaSubType == "*" && mediaType.equalsIgnoreCase(mimeType.takeWhile(_ != '/'))) ||
      (mediaType == "*" && mediaSubType == "*")

  override def toString = {
    new MediaType(mediaType, mediaSubType,
      parameters ++ qValue.map(q => ("q", Some(q.toString()))).toSeq ++ acceptExtensions).toString
  }
}

object MediaType {

  /**
   * Function and extractor object for parsing media types.
   */
  object parse {
    import MediaRange.MediaRangeParser

    def unapply(mediaType: String): Option[MediaType] = apply(mediaType)

    def apply(mediaType: String): Option[MediaType] = {
      MediaRangeParser.mediaType(new CharSequenceReader(mediaType)) match {
        case MediaRangeParser.Success(mt: MediaType, next) => {
          if (!next.atEnd) {
            Play.logger.debug("Unable to parse part of media type '" + next.source + "'")
          }
          Some(mt)
        }
        case MediaRangeParser.NoSuccess(err, next) => {
          Play.logger.debug("Unable to parse media type '" + next.source + "'")
          None
        }
      }
    }
  }
}

object MediaRange {

  /**
   * Function and extractor object for parsing media ranges.
   */
  object parse {

    def apply(mediaRanges: String): Seq[MediaRange] = {
      MediaRangeParser(new CharSequenceReader(mediaRanges)) match {
        case MediaRangeParser.Success(mrs: List[MediaRange], next) =>
          if (next.atEnd) {
            Play.logger.debug("Unable to parse part of media range header '" + next.source + "'")
          }
          mrs.sorted
        case MediaRangeParser.NoSuccess(err, _) =>
          Play.logger.debug("Unable to parse media range header '" + mediaRanges + "': " + err)
          Nil
      }
    }
  }

  /**
   * Ordering for MediaRanges, in order of highest priority to lowest priority.
   *
   * The reason it is highest to lowest instead of lowest to highest is to ensure sorting is stable, so if two media
   * ranges have the same ordering weight, they will not change order.
   *
   * Ordering rules for MediaRanges:
   *
   * First compare by qValue, default to 1.  Higher means higher priority.
   * Then compare the media type.  If they are not the same, then the least specific (ie, if one is *) has a lower
   * priority, otherwise if they have same priority.
   * Then compare the sub media type.  If they are the same, the one with the more parameters has a higher priority.
   * Otherwise the least specific has the lower priority, otherwise they have the same priority.
   */
  implicit val ordering = new Ordering[play.api.http.MediaRange] {

    def compareQValues(x: Option[BigDecimal], y: Option[BigDecimal]) = {
      if (x.isEmpty && y.isEmpty) 0
      else if (x.isEmpty) 1
      else if (y.isEmpty) -1
      else x.get.compare(y.get)
    }

    def compare(a: play.api.http.MediaRange, b: play.api.http.MediaRange) = {
      val qCompare = compareQValues(a.qValue, b.qValue)

      if (qCompare != 0) -qCompare
      else if (a.mediaType == b.mediaType) {
        if (a.mediaSubType == b.mediaSubType) b.parameters.size - a.parameters.size
        else if (a.mediaSubType == "*") 1
        else if (b.mediaSubType == "*") -1
        else 0
      } else if (a.mediaType == "*") 1
      else if (b.mediaType == "*") -1
      else 0
    }
  }

  /**
   * Parser for media ranges.
   *
   * Parses based on RFC2616 section 14.1, and 3.7.
   *
   * Unfortunately the specs are quite ambiguous, leaving much to our discretion.
   */
  private[http] object MediaRangeParser extends Parsers {

    val separatorChars = "()<>@,;:\\\"/[]?={} \t"
    val separatorBitSet = BitSet(separatorChars.toCharArray.map(_.toInt): _*)

    type Elem = Char

    val any = acceptIf(_ => true)(_ => "Expected any character")
    val end = not(any)

    /*
     * RFC 2616 section 2.2
     *
     * These patterns are translated directly using the same naming
     */
    val ctl = acceptIf { c =>
      (c >= 0 && c <= 0x1F) || c == 0x7F
    }(_ => "Expected a control character")
    val char = acceptIf(_ < 0x80)(_ => "Expected an ascii character")
    val text = not(ctl) ~> any
    val separators = {
      acceptIf(c => separatorBitSet(c))(_ => "Expected one of " + separatorChars)
    }

    val token = rep1(not(separators | ctl) ~> any) ^^ charSeqToString

    def badPart(p: Char => Boolean, msg: => String) = rep1(acceptIf(p)(ignoreErrors)) ^^ {
      case chars =>
        Play.logger.debug(msg + ": " + charSeqToString(chars))
        None
    }
    val badParameter = badPart(c => c != ',' && c != ';', "Bad media type parameter")
    val badMediaType = badPart(c => c != ',', "Bad media type")

    def tolerant[T](p: Parser[T], bad: Parser[Option[T]]) = p.map(Some.apply) | bad

    // The spec is really vague about what a quotedPair means. We're going to assume that it's just to quote quotes,
    // which means all we have to do for the result of it is ignore the slash.
    val quotedPair = '\\' ~> char
    val qdtext = not('"') ~> text
    val quotedString = '"' ~> rep(quotedPair | qdtext) <~ '"' ^^ charSeqToString

    /*
     * RFC 2616 section 3.7
     */
    val parameter = token ~ opt('=' ~> (token | quotedString)) <~ rep(' ') ^^ {
      case name ~ value => name -> value
    }

    // Either it's a valid parameter followed immediately by the end, a comma or a semicolon, or it's a bad parameter
    val tolerantParameter = tolerant(parameter <~ guard(end | ';' | ','), badParameter)

    val parameters = rep(';' ~> rep(' ') ~> tolerantParameter <~ rep(' '))
    val mediaType: Parser[MediaType] = (token <~ '/') ~ (token <~ rep(' ')) ~ parameters ^^ {
      case mainType ~ subType ~ ps => MediaType(mainType, subType, ps.flatten)
    }

    /*
     * RFC 2616 section 14.1
     *
     * Only difference between this and the spec is it treats * as valid.
     */

    // Some clients think that '*' is a valid media range.  Spec says it isn't, but it's used widely enough that we
    // need to support it.
    val mediaRange = (mediaType | ('*' ~> parameters.map(ps => MediaType("*", "*", ps.flatten)))) ^^ { mediaType =>
      val (params, rest) = mediaType.parameters.span(_._1 != "q")
      val (qValueStr, acceptParams) = rest match {
        case q :: ps => (q._2, ps)
        case _ => (None, Nil)
      }
      val qValue = qValueStr.flatMap { q =>
        try {
          val qbd = BigDecimal(q)
          if (qbd > 1) {
            Play.logger.debug("Invalid q value: " + q)
            None
          } else {
            Some(BigDecimal(q))
          }
        } catch {
          case _: NumberFormatException =>
            Play.logger.debug("Invalid q value: " + q)
            None
        }
      }
      new MediaRange(mediaType.mediaType, mediaType.mediaSubType, params, qValue, acceptParams)
    }

    // Either it's a valid media range followed immediately by the end or a comma, or it's a bad media type
    val tolerantMediaRange = tolerant(mediaRange <~ guard(end | ','), badMediaType)

    val mediaRanges = rep1sep(tolerantMediaRange, ',' ~ rep(' ')).map(_.flatten)

    def apply(in: Input): ParseResult[List[MediaRange]] = mediaRanges(in)

    def ignoreErrors(c: Char) = ""

    def charSeqToString(chars: Seq[Char]) = new String(chars.toArray)
  }

}

Other Play Framework source code examples

Here is a short list of links related to this Play Framework MediaRange.scala source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.