{"action":"created","comment":{"url":"https://api.github.com/repos/dhlab-basel/Knora/pulls/comments/224726549","pull_request_review_id":164166632,"id":224726549,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIyNDcyNjU0OQ==","diff_hunk":"@@ -0,0 +1,543 @@\n+/*\n+ * Copyright © 2015-2018 the contributors (see Contributors.md).\n+ *\n+ * This file is part of Knora.\n+ *\n+ * Knora is free software: you can redistribute it and/or modify\n+ * it under the terms of the GNU Affero General Public License as published\n+ * by the Free Software Foundation, either version 3 of the License, or\n+ * (at your option) any later version.\n+ *\n+ * Knora is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+ * GNU Affero General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Affero General Public\n+ * License along with Knora.  If not, see <http://www.gnu.org/licenses/>.\n+ */\n+\n+package org.knora.webapi.util.date\n+\n+import java.util.Date\n+\n+import com.ibm.icu.util._\n+import org.knora.webapi._\n+import org.knora.webapi.util.StringFormatter\n+\n+/**\n+  * Indicates the era (CE or BCE) in Gregorian and Julian calendar dates.\n+  */\n+sealed trait DateEraV2\n+\n+object DateEraV2 {\n+    /**\n+      * Parses a calendar era.\n+      *\n+      * @param eraStr a string representing the era.\n+      * @param errorFun a function that throws an exception. It will be called if the string cannot be parsed.\n+      * @return a [[DateEraV2]] representing the era.\n+      */\n+    def parse(eraStr: String, errorFun: => Nothing): DateEraV2 = {\n+        eraStr match {\n+            case StringFormatter.Era_AD | StringFormatter.Era_CE => DateEraCE\n+            case StringFormatter.Era_BC | StringFormatter.Era_BCE => DateEraBCE\n+            case _ => errorFun\n+        }\n+    }\n+}\n+\n+/**\n+  * Represents the era BCE in a Gregorian or Julian calendar date.\n+  */\n+case object DateEraBCE extends DateEraV2 {\n+    override def toString: String = StringFormatter.Era_BCE\n+}\n+\n+/**\n+  * Represents the era CE in a Gregorian or Julian calendar date.\n+  */\n+case object DateEraCE extends DateEraV2 {\n+    override def toString: String = StringFormatter.Era_CE\n+}\n+\n+/**\n+  * Represents the precision of a date.\n+  */\n+sealed trait DatePrecisionV2\n+\n+object DatePrecisionV2 {\n+    /**\n+      * Parses the name of a date precision.\n+      *\n+      * @param precisionStr the string to be parsed.\n+      * @param errorFun a function that throws an exception. It will be called if the string cannot be parsed.\n+      * @return a [[DatePrecisionV2]] representing the date precision.\n+      */\n+    def parse(precisionStr: String, errorFun: => Nothing): DatePrecisionV2 = {\n+        precisionStr match {\n+            case StringFormatter.PrecisionDay => DatePrecisionDay\n+            case StringFormatter.PrecisionMonth => DatePrecisionMonth\n+            case StringFormatter.PrecisionYear => DatePrecisionYear\n+            case _ => errorFun\n+        }\n+    }\n+}\n+\n+/**\n+  * Indicates that a date has year precision.\n+  */\n+case object DatePrecisionYear extends DatePrecisionV2 {\n+    override def toString: String = StringFormatter.PrecisionYear\n+}\n+\n+/**\n+  * Indicates that a date has month precision.\n+  */\n+case object DatePrecisionMonth extends DatePrecisionV2 {\n+    override def toString: String = StringFormatter.PrecisionMonth\n+}\n+\n+/**\n+  * Indicates that a date has day precision.\n+  */\n+case object DatePrecisionDay extends DatePrecisionV2 {\n+    override def toString: String = StringFormatter.PrecisionDay\n+}\n+\n+/**\n+  * Represents the name of a calendar.\n+  */\n+sealed trait CalendarNameV2\n+\n+object CalendarNameV2 {\n+    /**\n+      * Parses the name of a calendar.\n+      *\n+      * @param calendarNameStr a string representing the name of the calendar.\n+      * @param errorFun a function that throws an exception. It will be called if the string cannot\n+      *                 be parsed.\n+      * @return a [[CalendarNameV2]] representing the name of the calendar.\n+      */\n+    def parse(calendarNameStr: String, errorFun: => Nothing): CalendarNameV2 = {\n+        calendarNameStr match {\n+            case StringFormatter.CalendarGregorian => CalendarNameGregorian\n+            case StringFormatter.CalendarJulian => CalendarNameJulian\n+            case _ => errorFun\n+        }\n+    }\n+}\n+\n+/**\n+  * Represents the name of a Gregorian or Julian calendar.\n+  */\n+sealed trait CalendarNameGregorianOrJulian extends CalendarNameV2\n+\n+/**\n+  * Represents the name of the Gregorian calendar.\n+  */\n+case object CalendarNameGregorian extends CalendarNameGregorianOrJulian {\n+    override def toString: String = StringFormatter.CalendarGregorian\n+}\n+\n+/**\n+  * Represents the name of the Julian calendar.\n+  */\n+case object CalendarNameJulian extends CalendarNameGregorianOrJulian {\n+    override def toString: String = StringFormatter.CalendarJulian\n+}\n+\n+/**\n+  * Represents a date as values that are suitable for constructing human-readable representations.\n+  *\n+  * @param calendarName the name of the calendar.\n+  * @param year         the date's year.\n+  * @param maybeMonth   the date's month, if given.\n+  * @param maybeDay     the date's day, if given.\n+  * @param maybeEra     the date's era, if the calendar supports it. An era is required in Gregorian and\n+  *                     Julian calendars.\n+  */\n+case class CalendarDateV2(calendarName: CalendarNameV2, year: Int, maybeMonth: Option[Int], maybeDay: Option[Int], maybeEra: Option[DateEraV2]) {\n+    if (maybeMonth.isEmpty && maybeDay.isDefined) {\n+        throw AssertionException(s\"Invalid date: CalendarDateV2($calendarName, $year, $maybeMonth, $maybeDay, $maybeEra)\")\n+    }\n+\n+    calendarName match {\n+        case _: CalendarNameGregorianOrJulian =>\n+            if (maybeEra.isEmpty) {\n+                throw AssertionException(s\"Era is required in calendar $calendarName\")\n+            }\n+\n+        case _ => ()\n+    }\n+\n+    /**\n+      * The precision of this date.\n+      */\n+    lazy val precision: DatePrecisionV2 = {\n+        (maybeMonth, maybeDay) match {\n+            case (Some(_), Some(_)) => DatePrecisionDay\n+            case (Some(_), None) => DatePrecisionMonth\n+            case (None, None) => DatePrecisionYear\n+            case _ => throw AssertionException(\"Unreachable code\")\n+        }\n+    }\n+\n+    /**\n+      * Returns this date in Knora API v2 simple format, without the calendar.\n+      */\n+    override def toString: String = {\n+        val eraString = maybeEra match {\n+            case Some(era) => s\"${StringFormatter.EraSeparator}$era\"\n+            case None => \"\"\n+        }\n+\n+        (maybeMonth, maybeDay) match {\n+            case (Some(month), Some(day)) =>\n+                // Day precision: include the year, the month, and the day.\n+                f\"$year%04d${StringFormatter.PrecisionSeparator}$month%02d${StringFormatter.PrecisionSeparator}$day%02d$eraString\"\n+\n+            case (Some(month), None) =>\n+                // Month precision: include the year and the month.\n+                f\"$year%04d${StringFormatter.PrecisionSeparator}$month%02d$eraString\"\n+\n+            case (None, None) =>\n+                // Year precision: just include the year.\n+                f\"$year%04d$eraString\"\n+\n+            case _ => throw AssertionException(\"Unreachable code\")\n+        }\n+    }\n+\n+    /**\n+      * Constructs a [[Calendar]] based on the calendar name and era, to be used in subsequent date conversions.\n+      */\n+    private def makeBaseCalendar: Calendar = {\n+        calendarName match {\n+            // TODO: support calendars other than Gregorian and Julian.\n+\n+            case gregorianOrJulianName: CalendarNameGregorianOrJulian =>\n+                val calendar: GregorianCalendar = new GregorianCalendar(TimeZone.GMT_ZONE, ULocale.ENGLISH)\n+                calendar.setLenient(false) // check for invalid dates\n+                calendar.setGregorianChange(CalendarDateUtilV2.getGregorianCalendarChangeDate(gregorianOrJulianName))\n+\n+                maybeEra match {\n+                    case Some(DateEraCE) => calendar.set(Calendar.ERA, GregorianCalendar.AD)\n+                    case Some(DateEraBCE) => calendar.set(Calendar.ERA, GregorianCalendar.BC)\n+                    case None => throw AssertionException(s\"Unreachable code\")\n+                }\n+\n+                calendar.set(Calendar.HOUR, 0)\n+                calendar.set(Calendar.MINUTE, 0)\n+                calendar.set(Calendar.SECOND, 0)\n+\n+                calendar\n+        }\n+    }\n+\n+    /**\n+      * Converts this [[CalendarDateV2]] to a pair of Julian Day Numbers representing a date range. If the date's\n+      * precision is [[DatePrecisionDay]], the two Julian Day Numbers will be equal. This method validates the date\n+      * using its calendar.\n+      */\n+    def toJulianDayRange: (Int, Int) = {\n+        // Note:\n+        //\n+        // In com.ibm.icu.util.Calendar, JULIAN_DAY demarcates days at local zone midnight, rather than noon GMT:\n+        // http://icu-project.org/apiref/icu4j/com/ibm/icu/util/GregorianCalendar.html#setGregorianChange-java.util.Date-\n+        //\n+        // Month is 0-based.\n+\n+        try {\n+            precision match {\n+                case DatePrecisionYear =>\n+                    // first day of the given year\n+                    val startCalendar: Calendar = makeBaseCalendar\n+                    startCalendar.set(Calendar.YEAR, year)\n+                    startCalendar.set(Calendar.MONTH, startCalendar.getActualMinimum(Calendar.MONTH))\n+                    startCalendar.set(Calendar.DAY_OF_MONTH, startCalendar.getActualMinimum(Calendar.DAY_OF_MONTH))\n+\n+                    // last day of the given year\n+                    val endCalendar: Calendar = makeBaseCalendar\n+                    endCalendar.set(Calendar.YEAR, year)\n+                    endCalendar.set(Calendar.MONTH, endCalendar.getActualMaximum(Calendar.MONTH))\n+                    endCalendar.set(Calendar.DAY_OF_MONTH, endCalendar.getActualMaximum(Calendar.DAY_OF_MONTH))\n+\n+                    (startCalendar.get(Calendar.JULIAN_DAY), endCalendar.get(Calendar.JULIAN_DAY))\n+\n+                case DatePrecisionMonth =>\n+                    // first day of the given month in the given year\n+                    val startCalendar: Calendar = makeBaseCalendar\n+                    startCalendar.set(Calendar.YEAR, year)\n+                    startCalendar.set(Calendar.MONTH, maybeMonth.get - 1)\n+                    startCalendar.set(Calendar.DAY_OF_MONTH, startCalendar.getActualMinimum(Calendar.DAY_OF_MONTH))\n+\n+                    // last day of the given month in the given year\n+                    val endCalendar: Calendar = makeBaseCalendar\n+                    endCalendar.set(Calendar.YEAR, year)\n+                    endCalendar.set(Calendar.MONTH, maybeMonth.get - 1)\n+                    endCalendar.set(Calendar.DAY_OF_MONTH, endCalendar.getActualMaximum(Calendar.DAY_OF_MONTH))\n+\n+                    (startCalendar.get(Calendar.JULIAN_DAY), endCalendar.get(Calendar.JULIAN_DAY))\n+\n+                case DatePrecisionDay =>\n+                    val singleCalendar: Calendar = makeBaseCalendar\n+                    singleCalendar.set(Calendar.YEAR, year)\n+                    singleCalendar.set(Calendar.MONTH, maybeMonth.get - 1)\n+                    singleCalendar.set(Calendar.DAY_OF_MONTH, maybeDay.get)\n+\n+                    (singleCalendar.get(Calendar.JULIAN_DAY), singleCalendar.get(Calendar.JULIAN_DAY))\n+            }\n+\n+        } catch {\n+            case illegalArg: IllegalArgumentException => throw BadRequestException(s\"Invalid date '${this.toString}': $illegalArg\")\n+            case e: Exception => throw BadRequestException(s\"The date '${this.toString}' could not be handled correctly: ${e.getMessage}\")\n+        }\n+    }\n+}\n+\n+object CalendarDateV2 {\n+    /**\n+      * Converts a Julian Day Number to a [[CalendarDateV2]].\n+      *\n+      * @param julianDay the Julian Day Number.\n+      * @param precision the desired precision.\n+      * @param calendarName the name of the calendar to be used.\n+      * @return a [[CalendarDateV2]] with the specified precision and calendar.\n+      */\n+    def fromJulianDayNumber(julianDay: Int, precision: DatePrecisionV2, calendarName: CalendarNameV2): CalendarDateV2 = {\n+        // Convert the Julian Day Number to a com.ibm.icu.util.Calendar.\n+        val (calendar: Calendar, maybeEra: Option[DateEraV2]) = calendarName match {\n+            // TODO: support calendars other than Gregorian and Julian.\n+\n+            case gregorianOrJulianName: CalendarNameGregorianOrJulian =>\n+                val calendar: GregorianCalendar = new GregorianCalendar(TimeZone.GMT_ZONE, ULocale.ENGLISH)\n+                calendar.setGregorianChange(CalendarDateUtilV2.getGregorianCalendarChangeDate(gregorianOrJulianName))\n+                calendar.set(Calendar.JULIAN_DAY, julianDay)\n+\n+                val maybeGregorianEra = calendar.get(Calendar.ERA) match {\n+                    case GregorianCalendar.AD => Some(DateEraCE)\n+                    case GregorianCalendar.BC => Some(DateEraBCE)\n+                    case _ => throw AssertionException(\"Unreachable code\")\n+                }\n+\n+                (calendar, maybeGregorianEra)\n+        }\n+\n+        // Get the year, month, and day from the com.ibm.icu.util.Calendar.\n+        // Note: in com.ibm.icu.util.Calendar, month is 0-based.\n+\n+        val year: Int = calendar.get(Calendar.YEAR)\n+        val month: Int = calendar.get(Calendar.MONTH) + 1\n+        val day: Int = calendar.get(Calendar.DAY_OF_MONTH)\n+\n+        // Return a CalendarDateV2 in the requested precision.\n+        precision match {\n+            case DatePrecisionYear =>\n+                CalendarDateV2(\n+                    calendarName = calendarName,\n+                    year = year,\n+                    maybeMonth = None,\n+                    maybeDay = None,\n+                    maybeEra = maybeEra\n+                )\n+\n+            case DatePrecisionMonth =>\n+                CalendarDateV2(\n+                    calendarName = calendarName,\n+                    year = year,\n+                    maybeMonth = Some(month),\n+                    maybeDay = None,\n+                    maybeEra = maybeEra\n+                )\n+\n+            case DatePrecisionDay =>\n+                CalendarDateV2(\n+                    calendarName = calendarName,\n+                    year = year,\n+                    maybeMonth = Some(month),\n+                    maybeDay = Some(day),\n+                    maybeEra = maybeEra\n+                )\n+        }\n+    }\n+\n+    /**\n+      * Parses a string representing a single date, without the calendar. This method does does not check that the\n+      * date is valid in its calendar; to do that, call `toJulianDayRange` on it.\n+      *\n+      * @param dateStr the string to be parsed.\n+      * @param calendarName the name of the calendar used.\n+      * @return a [[CalendarDateV2]] representing the date.\n+      */\n+    def parse(dateStr: String, calendarName: CalendarNameV2): CalendarDateV2 = {\n+        // Get the era, if provided.\n+\n+        val dateStringSplitByEra: Array[String] = dateStr.split(StringFormatter.EraSeparator)\n+\n+        val maybeEra: Option[DateEraV2] = dateStringSplitByEra.length match {\n+            case 1 =>\n+                // No era given. In the Gregorian or Julian calendar, the era defaults to CE.\n+                calendarName match {\n+                    case _: CalendarNameGregorianOrJulian => Some(DateEraCE)\n+                    case _ => None\n+                }\n+\n+            case 2 =>\n+                // An era was given. Parse it.\n+                val eraStr: String = dateStringSplitByEra(1)\n+                Some(DateEraV2.parse(eraStr, throw BadRequestException(s\"Invalid date era: $eraStr\")))\n+\n+            case _ => throw BadRequestException(s\"Invalid date: $dateStr\")\n+        }\n+\n+        // Get the year, month, and day.\n+\n+        val dateSegments = dateStringSplitByEra(0).split(StringFormatter.PrecisionSeparator)\n+\n+        try {\n+            dateSegments.length match {\n+                case 1 => // year precision\n+                    CalendarDateV2(\n+                        calendarName = calendarName,\n+                        year = dateSegments.head.toInt,\n+                        maybeMonth = None,\n+                        maybeDay = None,\n+                        maybeEra = maybeEra\n+                    )\n+\n+                case 2 => // month precision\n+                    CalendarDateV2(\n+                        calendarName = calendarName,\n+                        year = dateSegments.head.toInt,\n+                        maybeMonth = Some(dateSegments(1).toInt),\n+                        maybeDay = None,\n+                        maybeEra = maybeEra\n+                    )\n+\n+                case 3 => // day precision\n+                    CalendarDateV2(\n+                        calendarName = calendarName,\n+                        year = dateSegments.head.toInt,\n+                        maybeMonth = Some(dateSegments(1).toInt),\n+                        maybeDay = Some(dateSegments(2).toInt),\n+                        maybeEra = maybeEra\n+                    )\n+\n+                case _ => throw BadRequestException(s\"Invalid date: $dateStr\")\n+            }\n+        } catch {\n+            case e: IllegalArgumentException => throw BadRequestException(s\"Invalid date '$dateStr': $e\")\n+        }\n+    }\n+}\n+\n+/**\n+  * Represents a date range consisting of two instances of [[CalendarDateV2]]. Both instances must have the same\n+  * calendar.\n+  *\n+  * @param startCalendarDate the start of the range.\n+  * @param endCalendarDate the end of the range.\n+  */\n+case class CalendarDateRangeV2(startCalendarDate: CalendarDateV2, endCalendarDate: CalendarDateV2) {\n+    if (startCalendarDate.calendarName != endCalendarDate.calendarName) {","path":"webapi/src/main/scala/org/knora/webapi/util/date/CalendarDateUtilV2.scala","position":443,"original_position":443,"commit_id":"0a4bf0e8cd5c0d60e7eb6000c36671ed55d44e0e","original_commit_id":"0a4bf0e8cd5c0d60e7eb6000c36671ed55d44e0e","user":{"login":"benjamingeer","id":558389,"node_id":"MDQ6VXNlcjU1ODM4OQ==","avatar_url":"https://avatars0.githubusercontent.com/u/558389?v=4","gravatar_id":"","url":"https://api.github.com/users/benjamingeer","html_url":"https://github.com/benjamingeer","followers_url":"https://api.github.com/users/benjamingeer/followers","following_url":"https://api.github.com/users/benjamingeer/following{/other_user}","gists_url":"https://api.github.com/users/benjamingeer/gists{/gist_id}","starred_url":"https://api.github.com/users/benjamingeer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/benjamingeer/subscriptions","organizations_url":"https://api.github.com/users/benjamingeer/orgs","repos_url":"https://api.github.com/users/benjamingeer/repos","events_url":"https://api.github.com/users/benjamingeer/events{/privacy}","received_events_url":"https://api.github.com/users/benjamingeer/received_events","type":"User","site_admin":false},"body":"Yes, good point.","created_at":"2018-10-12T09:32:57Z","updated_at":"2018-10-12T09:32:57Z","html_url":"https://github.com/dhlab-basel/Knora/pull/1006#discussion_r224726549","pull_request_url":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/dhlab-basel/Knora/pulls/comments/224726549"},"html":{"href":"https://github.com/dhlab-basel/Knora/pull/1006#discussion_r224726549"},"pull_request":{"href":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006"}},"in_reply_to_id":224687667},"pull_request":{"url":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006","id":221833839,"node_id":"MDExOlB1bGxSZXF1ZXN0MjIxODMzODM5","html_url":"https://github.com/dhlab-basel/Knora/pull/1006","diff_url":"https://github.com/dhlab-basel/Knora/pull/1006.diff","patch_url":"https://github.com/dhlab-basel/Knora/pull/1006.patch","issue_url":"https://api.github.com/repos/dhlab-basel/Knora/issues/1006","number":1006,"state":"open","locked":false,"title":"Rewrite date processing in API v2","user":{"login":"benjamingeer","id":558389,"node_id":"MDQ6VXNlcjU1ODM4OQ==","avatar_url":"https://avatars0.githubusercontent.com/u/558389?v=4","gravatar_id":"","url":"https://api.github.com/users/benjamingeer","html_url":"https://github.com/benjamingeer","followers_url":"https://api.github.com/users/benjamingeer/followers","following_url":"https://api.github.com/users/benjamingeer/following{/other_user}","gists_url":"https://api.github.com/users/benjamingeer/gists{/gist_id}","starred_url":"https://api.github.com/users/benjamingeer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/benjamingeer/subscriptions","organizations_url":"https://api.github.com/users/benjamingeer/orgs","repos_url":"https://api.github.com/users/benjamingeer/repos","events_url":"https://api.github.com/users/benjamingeer/events{/privacy}","received_events_url":"https://api.github.com/users/benjamingeer/received_events","type":"User","site_admin":false},"body":"This rewrites date processing in API v2 to use [ICU4J](http://site.icu-project.org/), which supports many different calendars as well as Julian Day Numbers. This PR doesn't include support for calendars other than Gregorian and Julian, but now it should be much easier to add them. It removes the remaining dependencies of API v2 code on API v1 code, and eliminates the need to convert dates to strings when converting them between JSON-LD and Julian Day Numbers.\r\n\r\n- [x] Add new date processing classes in `org.knora.webapi.util.date.CalendarDateUtilV2`.\r\n- [x] Change existing v2 code to use the new classes.\r\n- [x] Add tests to check that everything works as before.\r\n\r\nResolves #803.\r\nResolves #928.\r\n","created_at":"2018-10-10T15:25:08Z","updated_at":"2018-10-12T09:32:57Z","closed_at":null,"merged_at":null,"merge_commit_sha":"3ff894306728768a619b9675a4564bf636a44c7d","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":742124427,"node_id":"MDU6TGFiZWw3NDIxMjQ0Mjc=","url":"https://api.github.com/repos/dhlab-basel/Knora/labels/API%20v2","name":"API v2","color":"403491","default":false},{"id":294389789,"node_id":"MDU6TGFiZWwyOTQzODk3ODk=","url":"https://api.github.com/repos/dhlab-basel/Knora/labels/enhancement","name":"enhancement","color":"84b6eb","default":true},{"id":430843599,"node_id":"MDU6TGFiZWw0MzA4NDM1OTk=","url":"https://api.github.com/repos/dhlab-basel/Knora/labels/ready-for-review","name":"ready-for-review","color":"e6e6e6","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006/commits","review_comments_url":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006/comments","review_comment_url":"https://api.github.com/repos/dhlab-basel/Knora/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dhlab-basel/Knora/issues/1006/comments","statuses_url":"https://api.github.com/repos/dhlab-basel/Knora/statuses/0a4bf0e8cd5c0d60e7eb6000c36671ed55d44e0e","head":{"label":"dhlab-basel:wip/v2-refactor-date-util","ref":"wip/v2-refactor-date-util","sha":"0a4bf0e8cd5c0d60e7eb6000c36671ed55d44e0e","user":{"login":"dhlab-basel","id":16879914,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE2ODc5OTE0","avatar_url":"https://avatars0.githubusercontent.com/u/16879914?v=4","gravatar_id":"","url":"https://api.github.com/users/dhlab-basel","html_url":"https://github.com/dhlab-basel","followers_url":"https://api.github.com/users/dhlab-basel/followers","following_url":"https://api.github.com/users/dhlab-basel/following{/other_user}","gists_url":"https://api.github.com/users/dhlab-basel/gists{/gist_id}","starred_url":"https://api.github.com/users/dhlab-basel/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dhlab-basel/subscriptions","organizations_url":"https://api.github.com/users/dhlab-basel/orgs","repos_url":"https://api.github.com/users/dhlab-basel/repos","events_url":"https://api.github.com/users/dhlab-basel/events{/privacy}","received_events_url":"https://api.github.com/users/dhlab-basel/received_events","type":"Organization","site_admin":false},"repo":{"id":47264649,"node_id":"MDEwOlJlcG9zaXRvcnk0NzI2NDY0OQ==","name":"Knora","full_name":"dhlab-basel/Knora","private":false,"owner":{"login":"dhlab-basel","id":16879914,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE2ODc5OTE0","avatar_url":"https://avatars0.githubusercontent.com/u/16879914?v=4","gravatar_id":"","url":"https://api.github.com/users/dhlab-basel","html_url":"https://github.com/dhlab-basel","followers_url":"https://api.github.com/users/dhlab-basel/followers","following_url":"https://api.github.com/users/dhlab-basel/following{/other_user}","gists_url":"https://api.github.com/users/dhlab-basel/gists{/gist_id}","starred_url":"https://api.github.com/users/dhlab-basel/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dhlab-basel/subscriptions","organizations_url":"https://api.github.com/users/dhlab-basel/orgs","repos_url":"https://api.github.com/users/dhlab-basel/repos","events_url":"https://api.github.com/users/dhlab-basel/events{/privacy}","received_events_url":"https://api.github.com/users/dhlab-basel/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/dhlab-basel/Knora","description":"Knowledge Organization, Representation, and Annotation","fork":false,"url":"https://api.github.com/repos/dhlab-basel/Knora","forks_url":"https://api.github.com/repos/dhlab-basel/Knora/forks","keys_url":"https://api.github.com/repos/dhlab-basel/Knora/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dhlab-basel/Knora/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dhlab-basel/Knora/teams","hooks_url":"https://api.github.com/repos/dhlab-basel/Knora/hooks","issue_events_url":"https://api.github.com/repos/dhlab-basel/Knora/issues/events{/number}","events_url":"https://api.github.com/repos/dhlab-basel/Knora/events","assignees_url":"https://api.github.com/repos/dhlab-basel/Knora/assignees{/user}","branches_url":"https://api.github.com/repos/dhlab-basel/Knora/branches{/branch}","tags_url":"https://api.github.com/repos/dhlab-basel/Knora/tags","blobs_url":"https://api.github.com/repos/dhlab-basel/Knora/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dhlab-basel/Knora/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dhlab-basel/Knora/git/refs{/sha}","trees_url":"https://api.github.com/repos/dhlab-basel/Knora/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dhlab-basel/Knora/statuses/{sha}","languages_url":"https://api.github.com/repos/dhlab-basel/Knora/languages","stargazers_url":"https://api.github.com/repos/dhlab-basel/Knora/stargazers","contributors_url":"https://api.github.com/repos/dhlab-basel/Knora/contributors","subscribers_url":"https://api.github.com/repos/dhlab-basel/Knora/subscribers","subscription_url":"https://api.github.com/repos/dhlab-basel/Knora/subscription","commits_url":"https://api.github.com/repos/dhlab-basel/Knora/commits{/sha}","git_commits_url":"https://api.github.com/repos/dhlab-basel/Knora/git/commits{/sha}","comments_url":"https://api.github.com/repos/dhlab-basel/Knora/comments{/number}","issue_comment_url":"https://api.github.com/repos/dhlab-basel/Knora/issues/comments{/number}","contents_url":"https://api.github.com/repos/dhlab-basel/Knora/contents/{+path}","compare_url":"https://api.github.com/repos/dhlab-basel/Knora/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dhlab-basel/Knora/merges","archive_url":"https://api.github.com/repos/dhlab-basel/Knora/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dhlab-basel/Knora/downloads","issues_url":"https://api.github.com/repos/dhlab-basel/Knora/issues{/number}","pulls_url":"https://api.github.com/repos/dhlab-basel/Knora/pulls{/number}","milestones_url":"https://api.github.com/repos/dhlab-basel/Knora/milestones{/number}","notifications_url":"https://api.github.com/repos/dhlab-basel/Knora/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dhlab-basel/Knora/labels{/name}","releases_url":"https://api.github.com/repos/dhlab-basel/Knora/releases{/id}","deployments_url":"https://api.github.com/repos/dhlab-basel/Knora/deployments","created_at":"2015-12-02T14:00:26Z","updated_at":"2018-10-05T14:01:03Z","pushed_at":"2018-10-11T08:00:15Z","git_url":"git://github.com/dhlab-basel/Knora.git","ssh_url":"git@github.com:dhlab-basel/Knora.git","clone_url":"https://github.com/dhlab-basel/Knora.git","svn_url":"https://github.com/dhlab-basel/Knora","homepage":"http://knora.org","size":412266,"stargazers_count":31,"watchers_count":31,"language":"Scala","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":11,"mirror_url":null,"archived":false,"open_issues_count":237,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":11,"open_issues":237,"watchers":31,"default_branch":"develop"}},"base":{"label":"dhlab-basel:develop","ref":"develop","sha":"93c210dc2585956b03a8accd16344b756884dd6c","user":{"login":"dhlab-basel","id":16879914,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE2ODc5OTE0","avatar_url":"https://avatars0.githubusercontent.com/u/16879914?v=4","gravatar_id":"","url":"https://api.github.com/users/dhlab-basel","html_url":"https://github.com/dhlab-basel","followers_url":"https://api.github.com/users/dhlab-basel/followers","following_url":"https://api.github.com/users/dhlab-basel/following{/other_user}","gists_url":"https://api.github.com/users/dhlab-basel/gists{/gist_id}","starred_url":"https://api.github.com/users/dhlab-basel/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dhlab-basel/subscriptions","organizations_url":"https://api.github.com/users/dhlab-basel/orgs","repos_url":"https://api.github.com/users/dhlab-basel/repos","events_url":"https://api.github.com/users/dhlab-basel/events{/privacy}","received_events_url":"https://api.github.com/users/dhlab-basel/received_events","type":"Organization","site_admin":false},"repo":{"id":47264649,"node_id":"MDEwOlJlcG9zaXRvcnk0NzI2NDY0OQ==","name":"Knora","full_name":"dhlab-basel/Knora","private":false,"owner":{"login":"dhlab-basel","id":16879914,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE2ODc5OTE0","avatar_url":"https://avatars0.githubusercontent.com/u/16879914?v=4","gravatar_id":"","url":"https://api.github.com/users/dhlab-basel","html_url":"https://github.com/dhlab-basel","followers_url":"https://api.github.com/users/dhlab-basel/followers","following_url":"https://api.github.com/users/dhlab-basel/following{/other_user}","gists_url":"https://api.github.com/users/dhlab-basel/gists{/gist_id}","starred_url":"https://api.github.com/users/dhlab-basel/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dhlab-basel/subscriptions","organizations_url":"https://api.github.com/users/dhlab-basel/orgs","repos_url":"https://api.github.com/users/dhlab-basel/repos","events_url":"https://api.github.com/users/dhlab-basel/events{/privacy}","received_events_url":"https://api.github.com/users/dhlab-basel/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/dhlab-basel/Knora","description":"Knowledge Organization, Representation, and Annotation","fork":false,"url":"https://api.github.com/repos/dhlab-basel/Knora","forks_url":"https://api.github.com/repos/dhlab-basel/Knora/forks","keys_url":"https://api.github.com/repos/dhlab-basel/Knora/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dhlab-basel/Knora/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dhlab-basel/Knora/teams","hooks_url":"https://api.github.com/repos/dhlab-basel/Knora/hooks","issue_events_url":"https://api.github.com/repos/dhlab-basel/Knora/issues/events{/number}","events_url":"https://api.github.com/repos/dhlab-basel/Knora/events","assignees_url":"https://api.github.com/repos/dhlab-basel/Knora/assignees{/user}","branches_url":"https://api.github.com/repos/dhlab-basel/Knora/branches{/branch}","tags_url":"https://api.github.com/repos/dhlab-basel/Knora/tags","blobs_url":"https://api.github.com/repos/dhlab-basel/Knora/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dhlab-basel/Knora/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dhlab-basel/Knora/git/refs{/sha}","trees_url":"https://api.github.com/repos/dhlab-basel/Knora/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dhlab-basel/Knora/statuses/{sha}","languages_url":"https://api.github.com/repos/dhlab-basel/Knora/languages","stargazers_url":"https://api.github.com/repos/dhlab-basel/Knora/stargazers","contributors_url":"https://api.github.com/repos/dhlab-basel/Knora/contributors","subscribers_url":"https://api.github.com/repos/dhlab-basel/Knora/subscribers","subscription_url":"https://api.github.com/repos/dhlab-basel/Knora/subscription","commits_url":"https://api.github.com/repos/dhlab-basel/Knora/commits{/sha}","git_commits_url":"https://api.github.com/repos/dhlab-basel/Knora/git/commits{/sha}","comments_url":"https://api.github.com/repos/dhlab-basel/Knora/comments{/number}","issue_comment_url":"https://api.github.com/repos/dhlab-basel/Knora/issues/comments{/number}","contents_url":"https://api.github.com/repos/dhlab-basel/Knora/contents/{+path}","compare_url":"https://api.github.com/repos/dhlab-basel/Knora/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dhlab-basel/Knora/merges","archive_url":"https://api.github.com/repos/dhlab-basel/Knora/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dhlab-basel/Knora/downloads","issues_url":"https://api.github.com/repos/dhlab-basel/Knora/issues{/number}","pulls_url":"https://api.github.com/repos/dhlab-basel/Knora/pulls{/number}","milestones_url":"https://api.github.com/repos/dhlab-basel/Knora/milestones{/number}","notifications_url":"https://api.github.com/repos/dhlab-basel/Knora/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dhlab-basel/Knora/labels{/name}","releases_url":"https://api.github.com/repos/dhlab-basel/Knora/releases{/id}","deployments_url":"https://api.github.com/repos/dhlab-basel/Knora/deployments","created_at":"2015-12-02T14:00:26Z","updated_at":"2018-10-05T14:01:03Z","pushed_at":"2018-10-11T08:00:15Z","git_url":"git://github.com/dhlab-basel/Knora.git","ssh_url":"git@github.com:dhlab-basel/Knora.git","clone_url":"https://github.com/dhlab-basel/Knora.git","svn_url":"https://github.com/dhlab-basel/Knora","homepage":"http://knora.org","size":412266,"stargazers_count":31,"watchers_count":31,"language":"Scala","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":11,"mirror_url":null,"archived":false,"open_issues_count":237,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":11,"open_issues":237,"watchers":31,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006"},"html":{"href":"https://github.com/dhlab-basel/Knora/pull/1006"},"issue":{"href":"https://api.github.com/repos/dhlab-basel/Knora/issues/1006"},"comments":{"href":"https://api.github.com/repos/dhlab-basel/Knora/issues/1006/comments"},"review_comments":{"href":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006/comments"},"review_comment":{"href":"https://api.github.com/repos/dhlab-basel/Knora/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dhlab-basel/Knora/pulls/1006/commits"},"statuses":{"href":"https://api.github.com/repos/dhlab-basel/Knora/statuses/0a4bf0e8cd5c0d60e7eb6000c36671ed55d44e0e"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"benjamingeer"}}