{"action":"created","comment":{"url":"https://api.github.com/repos/playframework/playframework/pulls/comments/58432342","id":58432342,"diff_hunk":"@@ -0,0 +1,346 @@\n+/*\n+ * Copyright (C) 2009-2016 Lightbend Inc. <https://www.lightbend.com>\n+ */\n+package play.api.mvc\n+\n+import akka.NotUsed\n+import akka.stream.scaladsl.{ Flow, Source, StreamConverters }\n+import akka.stream.stage._\n+import akka.util.ByteString\n+import play.api.http.HeaderNames._\n+import play.api.http.Status._\n+import play.api.http.{ ContentTypes, HttpEntity }\n+\n+import scala.math.Ordered.orderingToOrdered\n+\n+// Long should be good enough to represent even very large files\n+// considering that Long.MAX_VALUE is 9223372036854775807 which\n+// would be enough to represent Petabytes files. Also, consider\n+// that File.length() returns a long value.\n+private[mvc] case class ByteRange(start: Long, end: Long) extends Ordered[ByteRange] {\n+\n+  override def compare(that: ByteRange): Int = {\n+    val startCompare = this.start - that.start\n+    if (startCompare != 0) startCompare.toInt\n+    else (this.end - that.end).toInt\n+  }\n+\n+  def length: Long = this.end - this.start\n+\n+  def distance(other: ByteRange): Long = {\n+    mergedEnd(other) - mergedStart(other) - (length + other.length)\n+  }\n+\n+  private def mergedStart(other: ByteRange) = math.min(start, other.start)\n+  private def mergedEnd(other: ByteRange) = math.max(end, other.end)\n+}\n+\n+private[mvc] case class Range(entityLength: Long, start: Option[Long], end: Option[Long]) extends Ordered[Range] {\n+\n+  // Rules according to RFC 7233:\n+  // 1. If the last-byte-pos value is absent, or if the value is greater\n+  //    than or equal to the current length of the representation data,\n+  //    the byte range is interpreted as the remainder of the representation\n+  //    (i.e., the server replaces the value of last-byte-pos with a value\n+  //    that is one less than the current length of the selected representation)\n+  // 2. A client can request the last N bytes of the selected representation\n+  //    using a suffix-byte-range-spec. If the selected representation is shorter\n+  //    than the specified suffix-length, the entire representation is used.\n+  lazy val byteRange: ByteRange = {\n+    (start, end) match {\n+      case (Some(_start), Some(_end)) => ByteRange(_start, math.min(_end, entityLength - 1))\n+      case (Some(_start), None) => ByteRange(_start, entityLength - 1)\n+      case (None, Some(_end)) => ByteRange(math.max(0, entityLength - _end), entityLength - 1)\n+      case (None, None) => ByteRange(0, 0)\n+    }\n+  }\n+\n+  // RFC 7233:\n+  // 1. A byte-range-spec is invalid if the last-byte-pos value is present\n+  //    and less than the first-byte-pos.\n+  // 2. For byte ranges, failing to overlap the current extent means that the\n+  //    first-byte-pos of all of the byte-range-spec values were greater than\n+  //    the current length of the selected representation.\n+  def isValid: Boolean = byteRange.start <= byteRange.end && byteRange.start < entityLength\n+\n+  def length = byteRange.length + 1 // the range end is inclusive\n+\n+  def merge(other: Range): Range = {\n+    val thisByteRange = this.byteRange\n+    val otherByteRange = other.byteRange\n+    Range(\n+      entityLength,\n+      Some(math.min(thisByteRange.start, otherByteRange.start)),\n+      Some(math.max(thisByteRange.end, otherByteRange.end))\n+    )\n+  }\n+\n+  override def toString: String = s\"${byteRange.start}-${byteRange.end}\"\n+\n+  override def compare(that: Range): Int = this.byteRange.compare(that.byteRange)\n+}\n+\n+private[mvc] object Range {\n+\n+  // Since the typical overhead between parts of a multipart/byteranges\n+  // payload is around 80 bytes, depending on the selected representation's\n+  // media type and the chosen boundary parameter length, it can be less\n+  // efficient to transfer many small disjoint parts than it is to transfer\n+  // the entire selected representation.\n+  val minimumDistance = 80 // TODO this should be configurable\n+\n+  // A server that supports range requests MAY ignore or reject a Range\n+  // header field that consists of [...] a set of many small ranges that\n+  // are not listed in ascending order, since both are indications of either\n+  // a broken client or a deliberate denial-of-service attack (Section 6.1).\n+  // http://tools.ietf.org/html/rfc7233#section-6.1\n+  val maxNumberOfRanges = 16 // TODO this should be configurable\n+\n+  val RangePattern = \"\"\"(\\d*)-(\\d*)\"\"\".r\n+\n+  def apply(entityLength: Long, range: String): Option[Range] = range match {\n+    case RangePattern(first, last) =>\n+      val firstByte = asOptionLong(first)\n+      val lastByte = asOptionLong(last)\n+\n+      if ((firstByte ++ lastByte).isEmpty) return None // unsatisfiable range\n+\n+      Some(Range(entityLength, firstByte, lastByte))\n+    case _ => None // unsatisfiable range\n+  }\n+\n+  private def asOptionLong(string: String) = if (string.isEmpty) None else Some(string.toLong)\n+}\n+\n+private[mvc] trait RangeSet {\n+\n+  def ranges: Seq[Option[Range]]\n+\n+  def entityLength: Long\n+\n+  // Rules according to RFC 7233:\n+  // 1. If a valid byte-range-set includes at least one byte-range-spec with\n+  //    a first-byte-pos that is less than the current length of the\n+  //    representation, or at least one suffix-byte-range-spec with a\n+  //    non-zero suffix-length, then the byte-range-set is satisfiable.\n+  //    Otherwise, the byte-range-set is unsatisfiable.\n+  // 2. A server that supports range requests MAY ignore or reject a Range\n+  //    header field that consists of more than two overlapping ranges, or a\n+  //    set of many small ranges that are not listed in ascending order,\n+  //    since both are indications of either a broken client or a deliberate\n+  //    denial-of-service attack.\n+  // 3. When multiple ranges are requested, a server MAY coalesce any of the\n+  //    ranges that overlap, or that are separated by a gap that is smaller\n+  //    than the overhead of sending multiple parts, regardless of the order\n+  //    in which the corresponding byte-range-spec appeared in the received\n+  //    Range header field\n+  // 4. When a multipart response payload is generated, the server SHOULD\n+  //    send the parts in the same order that the corresponding\n+  //    byte-range-spec appeared in the received Range header field,\n+  //    excluding those ranges that were deemed unsatisfiable or that were\n+  //    coalesced into other ranges.\n+  def normalize: RangeSet = {\n+    if (isValid) {\n+      flattenRanges.sorted match {\n+        case seq if seq.isEmpty => UnsatisfiableRangeSet(entityLength)\n+        case seq => SatisfiableRangeSet(entityLength, ranges = coalesce(seq.toList).map(Option.apply))\n+      }\n+    } else {\n+      UnsatisfiableRangeSet(entityLength)\n+    }\n+  }\n+\n+  private def coalesce(rangeSeq: List[Range]): List[Range] = {\n+    rangeSeq.foldLeft(List.empty[Range]) { (coalesced, current) =>\n+      val (mergeCandidates, otherCandidates) = coalesced.partition(_.byteRange.distance(current.byteRange) <= Range.minimumDistance)\n+      val merged = mergeCandidates.foldLeft(current)(_ merge _)\n+      otherCandidates :+ merged\n+    }\n+  }\n+\n+  def first = ranges.head match {\n+    case Some(r) => r\n+    case None => Range(entityLength = entityLength, start = Some(0), end = Some(entityLength))\n+  }\n+\n+  private def isValid: Boolean = !flattenRanges.exists(!_.isValid)\n+\n+  private def flattenRanges = ranges.filter(_.isDefined).flatten\n+\n+  override def toString = s\"bytes ${flattenRanges.mkString(\",\")}/$entityLength\"\n+}\n+\n+private[mvc] abstract class DefaultRangeSet(entityLength: Long) extends RangeSet {\n+  override def ranges: Seq[Option[Range]] = Seq.empty\n+}\n+\n+private[mvc] case class SatisfiableRangeSet(entityLength: Long, override val ranges: Seq[Option[Range]]) extends DefaultRangeSet(entityLength)\n+\n+private[mvc] case class UnsatisfiableRangeSet(entityLength: Long) extends DefaultRangeSet(entityLength)\n+\n+private[mvc] case class NoHeaderRangeSet(entityLength: Long) extends DefaultRangeSet(entityLength)\n+\n+private[mvc] object RangeSet {\n+\n+  // Play accepts only bytes as the range unit. According to RFC 7233:\n+  //\n+  //     An origin server MUST ignore a Range header field that contains a\n+  //     range unit it does not understand.  A proxy MAY discard a Range\n+  //     header field that contains a range unit it does not understand.\n+  val RangeSetPattern = \"\"\"^bytes=[0-9,-]+\"\"\".r\n+\n+  def apply(entityLength: Long, rangeHeader: Option[String]): RangeSet = rangeHeader match {\n+    case Some(header) => rangeSet(entityLength, header).normalize\n+    case None => NoHeaderRangeSet(entityLength)\n+  }\n+\n+  private def rangeSet(entityLength: Long, rangeHeader: String): RangeSet = rangeHeader match {\n+    case RangeSetPattern() =>\n+      val ranges = rangeHeader.split(\"=\")(1).split(\",\").map { r => Range(entityLength, r) }\n+      SatisfiableRangeSet(entityLength, ranges)\n+    case _ => NoHeaderRangeSet(entityLength)\n+  }\n+}\n+\n+object RangeResult {\n+\n+  /**\n+   * Stream path using range headers.\n+   *\n+   * @param path The path.\n+   * @param rangeHeader The HTTP Range header from user's request.\n+   * @param contentType The HTTP Content Type header for the response.\n+   */\n+  def ofPath(path: java.nio.file.Path, rangeHeader: Option[String], contentType: Option[String]): Result = {\n+    ofPath(path, rangeHeader, path.getFileName.toString, contentType)\n+  }\n+\n+  /**\n+   * Stream path using range headers.\n+   *\n+   * @param path The path.\n+   * @param rangeHeader The HTTP Range header from user's request.\n+   * @param fileName The file name for the HTTP Content-Disposition header as attachment attribute.\n+   * @param contentType The HTTP Content Type header for the response.\n+   */\n+  def ofPath(path: java.nio.file.Path, rangeHeader: Option[String], fileName: String, contentType: Option[String]): Result = {\n+    // TODO use FileIO.fromFile after https://github.com/akka/akka/issues/20031 was released\n+    val source = StreamConverters.fromInputStream(() => java.nio.file.Files.newInputStream(path))\n+    ofSource(path.toFile.length(), source, rangeHeader, Option(fileName), contentType)\n+  }\n+\n+  /**\n+   * Stream file using range headers.\n+   *\n+   * @param file The file.\n+   * @param rangeHeader The HTTP Range header from user's request.\n+   * @param contentType The HTTP Content Type header for the response.\n+   */\n+  def ofFile(file: java.io.File, rangeHeader: Option[String], contentType: Option[String]): Result = {\n+    ofFile(file, rangeHeader, file.getName, contentType)\n+  }\n+\n+  /**\n+   * Stream file using range headers.\n+   *\n+   * @param file The file.\n+   * @param rangeHeader The HTTP Range header from user's request.\n+   * @param fileName The file name for the HTTP Content-Disposition header as attachment attribute.\n+   * @param contentType The HTTP Content Type header for the response.\n+   */\n+  def ofFile(file: java.io.File, rangeHeader: Option[String], fileName: String, contentType: Option[String]): Result = {\n+    // TODO use FileIO.fromFile after https://github.com/akka/akka/issues/20031 was released\n+    val source = StreamConverters.fromInputStream(() => new java.io.FileInputStream(file))\n+    ofSource(file.length(), source, rangeHeader, Option(fileName), contentType)\n+  }\n+\n+  def ofSource(entityLength: Long, source: Source[ByteString, _], rangeHeader: Option[String], fileName: Option[String], contentType: Option[String]): Result = {\n+    val commonHeaders = Seq(\n+      Some(ACCEPT_RANGES -> \"bytes\"),\n+      Some(CONTENT_TYPE -> contentType.getOrElse(ContentTypes.BINARY)),\n+      fileName.map(f => CONTENT_DISPOSITION -> s\"\"\"attachment; filename=\"$f\"\"\"\")\n+    ).flatten.toMap\n+\n+    RangeSet(entityLength, rangeHeader) match {\n+      case rangeSet: SatisfiableRangeSet =>\n+        val firstRange = rangeSet.first\n+        val byteRange = firstRange.byteRange\n+\n+        val entitySource = source.via(sliceBytesTransformer(byteRange.start, firstRange.length))\n+\n+        Result(\n+          ResponseHeader(\n+            status = PARTIAL_CONTENT,\n+            headers = Map(CONTENT_RANGE -> rangeSet.toString) ++ commonHeaders\n+          ),\n+          HttpEntity.Streamed(\n+            entitySource,\n+            Some(firstRange.length),\n+            contentType.orElse(Some(ContentTypes.BINARY))\n+          )\n+        )\n+      case rangeSet: UnsatisfiableRangeSet =>\n+        Result(\n+          ResponseHeader(\n+            status = REQUESTED_RANGE_NOT_SATISFIABLE,\n+            headers = Map(CONTENT_RANGE -> s\"bytes */$entityLength\") ++ commonHeaders\n+          ),\n+          HttpEntity.Strict(\n+            data = ByteString.empty,\n+            contentType\n+          )\n+        )\n+      case rangeSet: NoHeaderRangeSet =>\n+        if (entityLength > 0) {\n+          Result(\n+            ResponseHeader(\n+              status = OK,\n+              headers = commonHeaders\n+            ),\n+            HttpEntity.Streamed(\n+              source,\n+              Some(entityLength),\n+              contentType.orElse(Some(ContentTypes.BINARY))\n+            )\n+          )\n+        } else {\n+          Results.Ok.sendEntity(HttpEntity.Strict(ByteString.empty, contentType))\n+        }\n+    }\n+  }\n+\n+  // See https://github.com/akka/akka/blob/v2.4.2/akka-http-core/src/main/scala/akka/http/impl/util/StreamUtils.scala#L83-L115\n+  def sliceBytesTransformer(start: Long, length: Long): Flow[ByteString, ByteString, NotUsed] = {","path":"framework/src/play/src/main/scala/play/api/mvc/RangeResult.scala","position":313,"original_position":313,"commit_id":"8039222fc8a6ff7acbf805a208e6f94bb35aed0f","original_commit_id":"8039222fc8a6ff7acbf805a208e6f94bb35aed0f","user":{"login":"gmethvin","id":171663,"avatar_url":"https://avatars.githubusercontent.com/u/171663?v=3","gravatar_id":"","url":"https://api.github.com/users/gmethvin","html_url":"https://github.com/gmethvin","followers_url":"https://api.github.com/users/gmethvin/followers","following_url":"https://api.github.com/users/gmethvin/following{/other_user}","gists_url":"https://api.github.com/users/gmethvin/gists{/gist_id}","starred_url":"https://api.github.com/users/gmethvin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gmethvin/subscriptions","organizations_url":"https://api.github.com/users/gmethvin/orgs","repos_url":"https://api.github.com/users/gmethvin/repos","events_url":"https://api.github.com/users/gmethvin/events{/privacy}","received_events_url":"https://api.github.com/users/gmethvin/received_events","type":"User","site_admin":false},"body":"Actually it should be `play.api.libs.streams.AkkaStreams`.","created_at":"2016-04-04T19:22:23Z","updated_at":"2016-04-04T19:22:23Z","html_url":"https://github.com/playframework/playframework/pull/5961#discussion_r58432342","pull_request_url":"https://api.github.com/repos/playframework/playframework/pulls/5961","_links":{"self":{"href":"https://api.github.com/repos/playframework/playframework/pulls/comments/58432342"},"html":{"href":"https://github.com/playframework/playframework/pull/5961#discussion_r58432342"},"pull_request":{"href":"https://api.github.com/repos/playframework/playframework/pulls/5961"}}},"pull_request":{"url":"https://api.github.com/repos/playframework/playframework/pulls/5961","id":64412935,"html_url":"https://github.com/playframework/playframework/pull/5961","diff_url":"https://github.com/playframework/playframework/pull/5961.diff","patch_url":"https://github.com/playframework/playframework/pull/5961.patch","issue_url":"https://api.github.com/repos/playframework/playframework/issues/5961","number":5961,"state":"open","locked":false,"title":"Range requests support","user":{"login":"marcospereira","id":4576,"avatar_url":"https://avatars.githubusercontent.com/u/4576?v=3","gravatar_id":"","url":"https://api.github.com/users/marcospereira","html_url":"https://github.com/marcospereira","followers_url":"https://api.github.com/users/marcospereira/followers","following_url":"https://api.github.com/users/marcospereira/following{/other_user}","gists_url":"https://api.github.com/users/marcospereira/gists{/gist_id}","starred_url":"https://api.github.com/users/marcospereira/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/marcospereira/subscriptions","organizations_url":"https://api.github.com/users/marcospereira/orgs","repos_url":"https://api.github.com/users/marcospereira/repos","events_url":"https://api.github.com/users/marcospereira/events{/privacy}","received_events_url":"https://api.github.com/users/marcospereira/received_events","type":"User","site_admin":false},"body":"## Pull Request Checklist\r\n\r\n* [x] Have you added copyright headers to new files?\r\n* [x] Have you checked that both Scala and Java APIs are updated?\r\n* [x] Have you updated the documentation for both Scala and Java sections?\r\n* [x] Have you added tests for any changed functionality?\r\n\r\n## Fixes\r\n\r\nFixes #1097 and replaces #4865.\r\n\r\n## Purpose\r\n\r\nThis PR is based on the amazing work done by @bjfletcher at #4865, some Akka Http code and at [RFC 7233](http://tools.ietf.org/html/rfc7233). In relation to #4865, it adds:\r\n\r\n1. More tests\r\n2. More complete range parsing\r\n3. Handle invalid ranges by returning `416 Requested Range Not Satisfiable`\r\n4. A \"pure\" Akka Stream implementation, not depending on enumerators\r\n5. Detailed comments from relevant excerpts of the RFC 7233.\r\n\r\nIt stills lacks:\r\n\r\n- [x] ~~Support for `multipart/byteranges` requests~~\r\n- [x] Java API on par with the Scala version\r\n- [x] Proper documentation\r\n\r\n## Open to discussion:\r\n\r\nWe can refactoring the `Results` code to made `sendFile`, `sendPath` and `sendStream` methods supports range request transparently. This would required that these methods implicitly (Scala) receive a `request` and then check for the `Range` header. Of course, this breaks binary compatibility at a very used API and then it should not be done for Play 2.5.x releases. See @schmitch [comments at gitter](https://gitter.im/playframework/contributors?at=56f90608d39de41b495e40f8).\r\n\r\n## References\r\n\r\n1. #1097 \r\n2. #4865 \r\n3. [Gitter conversation](https://gitter.im/playframework/contributors?at=56f90608d39de41b495e40f8)","created_at":"2016-03-29T04:42:21Z","updated_at":"2016-04-04T19:22:23Z","closed_at":null,"merged_at":null,"merge_commit_sha":"d216f08d1f1d90952f62bbd8b31062521744fdff","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/playframework/playframework/pulls/5961/commits","review_comments_url":"https://api.github.com/repos/playframework/playframework/pulls/5961/comments","review_comment_url":"https://api.github.com/repos/playframework/playframework/pulls/comments{/number}","comments_url":"https://api.github.com/repos/playframework/playframework/issues/5961/comments","statuses_url":"https://api.github.com/repos/playframework/playframework/statuses/8039222fc8a6ff7acbf805a208e6f94bb35aed0f","head":{"label":"marcospereira:range-request","ref":"range-request","sha":"8039222fc8a6ff7acbf805a208e6f94bb35aed0f","user":{"login":"marcospereira","id":4576,"avatar_url":"https://avatars.githubusercontent.com/u/4576?v=3","gravatar_id":"","url":"https://api.github.com/users/marcospereira","html_url":"https://github.com/marcospereira","followers_url":"https://api.github.com/users/marcospereira/followers","following_url":"https://api.github.com/users/marcospereira/following{/other_user}","gists_url":"https://api.github.com/users/marcospereira/gists{/gist_id}","starred_url":"https://api.github.com/users/marcospereira/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/marcospereira/subscriptions","organizations_url":"https://api.github.com/users/marcospereira/orgs","repos_url":"https://api.github.com/users/marcospereira/repos","events_url":"https://api.github.com/users/marcospereira/events{/privacy}","received_events_url":"https://api.github.com/users/marcospereira/received_events","type":"User","site_admin":false},"repo":{"id":34361186,"name":"playframework","full_name":"marcospereira/playframework","owner":{"login":"marcospereira","id":4576,"avatar_url":"https://avatars.githubusercontent.com/u/4576?v=3","gravatar_id":"","url":"https://api.github.com/users/marcospereira","html_url":"https://github.com/marcospereira","followers_url":"https://api.github.com/users/marcospereira/followers","following_url":"https://api.github.com/users/marcospereira/following{/other_user}","gists_url":"https://api.github.com/users/marcospereira/gists{/gist_id}","starred_url":"https://api.github.com/users/marcospereira/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/marcospereira/subscriptions","organizations_url":"https://api.github.com/users/marcospereira/orgs","repos_url":"https://api.github.com/users/marcospereira/repos","events_url":"https://api.github.com/users/marcospereira/events{/privacy}","received_events_url":"https://api.github.com/users/marcospereira/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/marcospereira/playframework","description":"Play Framework","fork":true,"url":"https://api.github.com/repos/marcospereira/playframework","forks_url":"https://api.github.com/repos/marcospereira/playframework/forks","keys_url":"https://api.github.com/repos/marcospereira/playframework/keys{/key_id}","collaborators_url":"https://api.github.com/repos/marcospereira/playframework/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/marcospereira/playframework/teams","hooks_url":"https://api.github.com/repos/marcospereira/playframework/hooks","issue_events_url":"https://api.github.com/repos/marcospereira/playframework/issues/events{/number}","events_url":"https://api.github.com/repos/marcospereira/playframework/events","assignees_url":"https://api.github.com/repos/marcospereira/playframework/assignees{/user}","branches_url":"https://api.github.com/repos/marcospereira/playframework/branches{/branch}","tags_url":"https://api.github.com/repos/marcospereira/playframework/tags","blobs_url":"https://api.github.com/repos/marcospereira/playframework/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/marcospereira/playframework/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/marcospereira/playframework/git/refs{/sha}","trees_url":"https://api.github.com/repos/marcospereira/playframework/git/trees{/sha}","statuses_url":"https://api.github.com/repos/marcospereira/playframework/statuses/{sha}","languages_url":"https://api.github.com/repos/marcospereira/playframework/languages","stargazers_url":"https://api.github.com/repos/marcospereira/playframework/stargazers","contributors_url":"https://api.github.com/repos/marcospereira/playframework/contributors","subscribers_url":"https://api.github.com/repos/marcospereira/playframework/subscribers","subscription_url":"https://api.github.com/repos/marcospereira/playframework/subscription","commits_url":"https://api.github.com/repos/marcospereira/playframework/commits{/sha}","git_commits_url":"https://api.github.com/repos/marcospereira/playframework/git/commits{/sha}","comments_url":"https://api.github.com/repos/marcospereira/playframework/comments{/number}","issue_comment_url":"https://api.github.com/repos/marcospereira/playframework/issues/comments{/number}","contents_url":"https://api.github.com/repos/marcospereira/playframework/contents/{+path}","compare_url":"https://api.github.com/repos/marcospereira/playframework/compare/{base}...{head}","merges_url":"https://api.github.com/repos/marcospereira/playframework/merges","archive_url":"https://api.github.com/repos/marcospereira/playframework/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/marcospereira/playframework/downloads","issues_url":"https://api.github.com/repos/marcospereira/playframework/issues{/number}","pulls_url":"https://api.github.com/repos/marcospereira/playframework/pulls{/number}","milestones_url":"https://api.github.com/repos/marcospereira/playframework/milestones{/number}","notifications_url":"https://api.github.com/repos/marcospereira/playframework/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/marcospereira/playframework/labels{/name}","releases_url":"https://api.github.com/repos/marcospereira/playframework/releases{/id}","deployments_url":"https://api.github.com/repos/marcospereira/playframework/deployments","created_at":"2015-04-22T01:20:04Z","updated_at":"2016-02-01T17:32:34Z","pushed_at":"2016-04-04T00:33:19Z","git_url":"git://github.com/marcospereira/playframework.git","ssh_url":"git@github.com:marcospereira/playframework.git","clone_url":"https://github.com/marcospereira/playframework.git","svn_url":"https://github.com/marcospereira/playframework","homepage":"http://www.playframework.com/","size":81700,"stargazers_count":0,"watchers_count":0,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"playframework:master","ref":"master","sha":"6bcfbbe07d7e995291052de342c4d39988dfbd44","user":{"login":"playframework","id":319107,"avatar_url":"https://avatars.githubusercontent.com/u/319107?v=3","gravatar_id":"","url":"https://api.github.com/users/playframework","html_url":"https://github.com/playframework","followers_url":"https://api.github.com/users/playframework/followers","following_url":"https://api.github.com/users/playframework/following{/other_user}","gists_url":"https://api.github.com/users/playframework/gists{/gist_id}","starred_url":"https://api.github.com/users/playframework/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/playframework/subscriptions","organizations_url":"https://api.github.com/users/playframework/orgs","repos_url":"https://api.github.com/users/playframework/repos","events_url":"https://api.github.com/users/playframework/events{/privacy}","received_events_url":"https://api.github.com/users/playframework/received_events","type":"Organization","site_admin":false},"repo":{"id":2340549,"name":"playframework","full_name":"playframework/playframework","owner":{"login":"playframework","id":319107,"avatar_url":"https://avatars.githubusercontent.com/u/319107?v=3","gravatar_id":"","url":"https://api.github.com/users/playframework","html_url":"https://github.com/playframework","followers_url":"https://api.github.com/users/playframework/followers","following_url":"https://api.github.com/users/playframework/following{/other_user}","gists_url":"https://api.github.com/users/playframework/gists{/gist_id}","starred_url":"https://api.github.com/users/playframework/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/playframework/subscriptions","organizations_url":"https://api.github.com/users/playframework/orgs","repos_url":"https://api.github.com/users/playframework/repos","events_url":"https://api.github.com/users/playframework/events{/privacy}","received_events_url":"https://api.github.com/users/playframework/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/playframework/playframework","description":"Play Framework","fork":false,"url":"https://api.github.com/repos/playframework/playframework","forks_url":"https://api.github.com/repos/playframework/playframework/forks","keys_url":"https://api.github.com/repos/playframework/playframework/keys{/key_id}","collaborators_url":"https://api.github.com/repos/playframework/playframework/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/playframework/playframework/teams","hooks_url":"https://api.github.com/repos/playframework/playframework/hooks","issue_events_url":"https://api.github.com/repos/playframework/playframework/issues/events{/number}","events_url":"https://api.github.com/repos/playframework/playframework/events","assignees_url":"https://api.github.com/repos/playframework/playframework/assignees{/user}","branches_url":"https://api.github.com/repos/playframework/playframework/branches{/branch}","tags_url":"https://api.github.com/repos/playframework/playframework/tags","blobs_url":"https://api.github.com/repos/playframework/playframework/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/playframework/playframework/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/playframework/playframework/git/refs{/sha}","trees_url":"https://api.github.com/repos/playframework/playframework/git/trees{/sha}","statuses_url":"https://api.github.com/repos/playframework/playframework/statuses/{sha}","languages_url":"https://api.github.com/repos/playframework/playframework/languages","stargazers_url":"https://api.github.com/repos/playframework/playframework/stargazers","contributors_url":"https://api.github.com/repos/playframework/playframework/contributors","subscribers_url":"https://api.github.com/repos/playframework/playframework/subscribers","subscription_url":"https://api.github.com/repos/playframework/playframework/subscription","commits_url":"https://api.github.com/repos/playframework/playframework/commits{/sha}","git_commits_url":"https://api.github.com/repos/playframework/playframework/git/commits{/sha}","comments_url":"https://api.github.com/repos/playframework/playframework/comments{/number}","issue_comment_url":"https://api.github.com/repos/playframework/playframework/issues/comments{/number}","contents_url":"https://api.github.com/repos/playframework/playframework/contents/{+path}","compare_url":"https://api.github.com/repos/playframework/playframework/compare/{base}...{head}","merges_url":"https://api.github.com/repos/playframework/playframework/merges","archive_url":"https://api.github.com/repos/playframework/playframework/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/playframework/playframework/downloads","issues_url":"https://api.github.com/repos/playframework/playframework/issues{/number}","pulls_url":"https://api.github.com/repos/playframework/playframework/pulls{/number}","milestones_url":"https://api.github.com/repos/playframework/playframework/milestones{/number}","notifications_url":"https://api.github.com/repos/playframework/playframework/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/playframework/playframework/labels{/name}","releases_url":"https://api.github.com/repos/playframework/playframework/releases{/id}","deployments_url":"https://api.github.com/repos/playframework/playframework/deployments","created_at":"2011-09-07T09:24:08Z","updated_at":"2016-04-04T16:20:16Z","pushed_at":"2016-04-04T19:20:50Z","git_url":"git://github.com/playframework/playframework.git","ssh_url":"git@github.com:playframework/playframework.git","clone_url":"https://github.com/playframework/playframework.git","svn_url":"https://github.com/playframework/playframework","homepage":"http://www.playframework.com/","size":93002,"stargazers_count":7654,"watchers_count":7654,"language":"Scala","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2671,"mirror_url":null,"open_issues_count":267,"forks":2671,"open_issues":267,"watchers":7654,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/playframework/playframework/pulls/5961"},"html":{"href":"https://github.com/playframework/playframework/pull/5961"},"issue":{"href":"https://api.github.com/repos/playframework/playframework/issues/5961"},"comments":{"href":"https://api.github.com/repos/playframework/playframework/issues/5961/comments"},"review_comments":{"href":"https://api.github.com/repos/playframework/playframework/pulls/5961/comments"},"review_comment":{"href":"https://api.github.com/repos/playframework/playframework/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/playframework/playframework/pulls/5961/commits"},"statuses":{"href":"https://api.github.com/repos/playframework/playframework/statuses/8039222fc8a6ff7acbf805a208e6f94bb35aed0f"}}}}