You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by yu...@apache.org on 2017/10/20 19:43:07 UTC

[22/22] ambari git commit: AMBARI-21955. Update React version to 15.6.2 to get MIT license. (Sanket Shah via yusaku)

AMBARI-21955. Update React version to 15.6.2 to get MIT license. (Sanket Shah via yusaku)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/0c188aea
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/0c188aea
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/0c188aea

Branch: refs/heads/trunk
Commit: 0c188aeae7343c76c2c0471c0b271237d0995d09
Parents: 6df6cbe
Author: Yusaku Sako <yu...@hortonworks.com>
Authored: Fri Oct 20 12:42:13 2017 -0700
Committer: Yusaku Sako <yu...@hortonworks.com>
Committed: Fri Oct 20 12:42:13 2017 -0700

----------------------------------------------------------------------
 .gitignore                                      |     3 +
 contrib/views/storm/pom.xml                     |   103 +-
 .../views/storm/src/main/resources/index.html   |    45 -
 .../js/backbone-paginator.min.js                |  1325 --
 .../main/resources/libs/Backbone/js/Backbone.js |  1920 --
 .../libs/Bootstrap/css/bootstrap-editable.css   |   663 -
 .../libs/Bootstrap/css/bootstrap-slider.min.css |    28 -
 .../libs/Bootstrap/css/bootstrap-switch.min.css |    22 -
 .../resources/libs/Bootstrap/css/bootstrap.css  |  2120 +-
 .../fonts/glyphicons-halflings-regular.svg      |     2 +-
 .../libs/Bootstrap/js/bootstrap-editable.min.js |     7 -
 .../libs/Bootstrap/js/bootstrap-notify.min.js   |     1 -
 .../libs/Bootstrap/js/bootstrap-slider.min.js   |    29 -
 .../libs/Bootstrap/js/bootstrap-switch.min.js   |    22 -
 .../libs/Bootstrap/js/bootstrap.min.js          |     7 -
 .../libs/Font-Awesome/css/font-awesome.min.css  |     4 -
 .../Font-Awesome/fonts/fontawesome-webfont.svg  |    60 +-
 .../resources/libs/Underscore/js/Underscore.js  |  1548 --
 .../resources/libs/bootbox/js/bootbox.min.js    |     6 -
 .../src/main/resources/libs/d3/js/d3-tip.min.js |     1 -
 .../src/main/resources/libs/d3/js/d3.min.js     |     5 -
 .../resources/libs/dagre-d3/dagre-d3.min.js     |    28 -
 .../libs/jQuery/js/jquery-2.2.3.min.js          |     4 -
 .../main/resources/libs/jsx/JSXTransformer.js   | 15201 ------------
 .../storm/src/main/resources/libs/jsx/jsx.js    |    75 -
 .../main/resources/libs/react/js/react-dom.js   |    42 -
 .../libs/react/js/react-with-addons.js          | 20775 -----------------
 .../resources/libs/require-js/js/require.min.js |    36 -
 .../main/resources/libs/require-text/js/text.js |   390 -
 .../scripts/collections/BaseCollection.js       |   197 -
 .../scripts/collections/VNimbusConfigList.js    |    52 -
 .../scripts/collections/VNimbusList.js          |    52 -
 .../scripts/collections/VSupervisorList.js      |    58 +-
 .../scripts/collections/VTopologyConfigList.js  |    49 -
 .../scripts/collections/VTopologyList.js        |    52 -
 .../resources/scripts/components/BarChart.jsx   |   402 -
 .../scripts/components/Breadcrumbs.jsx          |    50 -
 .../main/resources/scripts/components/Modal.jsx |    60 -
 .../scripts/components/RadialChart.jsx          |   127 -
 .../resources/scripts/components/SearchLogs.jsx |    89 -
 .../main/resources/scripts/components/Table.jsx |   101 -
 .../scripts/components/TopologyGraph.jsx        |   199 -
 .../scripts/containers/ClusterSummary.jsx       |   122 -
 .../scripts/containers/NimbusConfigSummary.jsx  |   103 -
 .../scripts/containers/NimbusSummary.jsx        |   139 -
 .../scripts/containers/SupervisorSummary.jsx    |   155 -
 .../containers/TopologyConfiguration.jsx        |    93 -
 .../scripts/containers/TopologyDetailGraph.jsx  |    66 -
 .../scripts/containers/TopologyListing.jsx      |   188 -
 .../storm/src/main/resources/scripts/main.js    |    98 -
 .../main/resources/scripts/models/BaseModel.js  |    83 -
 .../main/resources/scripts/models/VCluster.js   |    37 +-
 .../main/resources/scripts/models/VNimbus.js    |    41 +-
 .../resources/scripts/models/VNimbusConfig.js   |    47 +-
 .../resources/scripts/models/VSupervisor.js     |    44 +-
 .../main/resources/scripts/models/VTopology.js  |    90 -
 .../resources/scripts/models/VTopologyConfig.js |    30 +-
 .../scripts/modules/Table/PageableTable.jsx     |    47 -
 .../scripts/modules/Table/Pagination.jsx        |   161 -
 .../src/main/resources/scripts/router/Router.js |   123 -
 .../src/main/resources/scripts/utils/Globals.js |    20 +-
 .../main/resources/scripts/utils/Overrides.js   |    30 -
 .../src/main/resources/scripts/utils/Utils.js   |   113 -
 .../scripts/views/ComponentDetailView.jsx       |   534 -
 .../main/resources/scripts/views/Dashboard.jsx  |    65 -
 .../src/main/resources/scripts/views/Footer.jsx |    48 -
 .../scripts/views/NimbusSummaryView.jsx         |    65 -
 .../resources/scripts/views/ProfilingView.jsx   |   214 -
 .../resources/scripts/views/RebalanceView.jsx   |   223 -
 .../scripts/views/SupervisorSummaryView.jsx     |    65 -
 .../scripts/views/TopologyDetailView.jsx        |  1039 -
 .../scripts/views/TopologyListingView.jsx       |    65 -
 .../storm/src/main/resources/styles/style.css   |   313 +-
 .../views/storm/src/main/resources/ui/.babelrc  |    25 +
 .../src/main/resources/ui/.eslintignore.js      |     3 +
 .../storm/src/main/resources/ui/.eslintrc.js    |    58 +
 .../ui/app/scripts/components/BarChart.jsx      |   429 +
 .../ui/app/scripts/components/Breadcrumbs.jsx   |    45 +
 .../scripts/components/CommonNotification.jsx   |    69 +
 .../app/scripts/components/CommonPagination.jsx |    56 +
 .../components/CommonSwitchComponent.jsx        |    41 +
 .../scripts/components/CommonWindowPanel.jsx    |    99 +
 .../ui/app/scripts/components/Editable.jsx      |   127 +
 .../ui/app/scripts/components/FSModel.jsx       |   149 +
 .../scripts/components/LogLevelComponent.jsx    |   236 +
 .../ui/app/scripts/components/ProfilingView.jsx |   168 +
 .../ui/app/scripts/components/RadialChart.jsx   |   134 +
 .../scripts/components/RebalanceTopology.jsx    |   152 +
 .../ui/app/scripts/components/SearchLogs.jsx    |    84 +
 .../ui/app/scripts/components/TopologyGraph.jsx |   208 +
 .../ui/app/scripts/containers/BaseContainer.jsx |    50 +
 .../app/scripts/containers/ClusterSummary.jsx   |   125 +
 .../scripts/containers/ComponentDetailView.jsx  |   714 +
 .../ui/app/scripts/containers/Dashboard.jsx     |    52 +
 .../scripts/containers/NimbusConfigSummary.jsx  |   126 +
 .../ui/app/scripts/containers/NimbusSummary.jsx |   150 +
 .../scripts/containers/SupervisorSummary.jsx    |   165 +
 .../scripts/containers/TopologyDetailView.jsx   |   862 +
 .../app/scripts/containers/TopologyListing.jsx  |   222 +
 .../ui/app/scripts/rest/TopologyREST.js         |   118 +
 .../resources/ui/app/scripts/routers/routes.jsx |    68 +
 .../resources/ui/app/scripts/utils/Utils.js     |    51 +
 .../ui/app/styles/css/font-awesome.min.css      |     4 +
 .../resources/ui/app/styles/css/toastr.min.css  |     1 +
 .../resources/ui/config/webpack.config.base.js  |   101 +
 .../ui/config/webpack.config.development.js     |    64 +
 .../ui/config/webpack.config.production.js      |   131 +
 .../storm/src/main/resources/ui/dev-server.js   |   101 +
 .../storm/src/main/resources/ui/index.html      |    47 +
 .../storm/src/main/resources/ui/package.json    |   110 +
 pom.xml                                         |     7 +-
 111 files changed, 7372 insertions(+), 48401 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/0c188aea/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index a40e61a..77aadb7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,6 @@ rebel.xml
 rebel-remote.xml
 out
 createDDL.jdbc
+/contrib/views/storm/src/main/resources/ui/node_modules/
+/contrib/views/storm/src/main/resources/ui/public/
+/contrib/views/storm/src/main/resources/ui/npm-debug.log

http://git-wip-us.apache.org/repos/asf/ambari/blob/0c188aea/contrib/views/storm/pom.xml
----------------------------------------------------------------------
diff --git a/contrib/views/storm/pom.xml b/contrib/views/storm/pom.xml
index cd92658..c424f45 100644
--- a/contrib/views/storm/pom.xml
+++ b/contrib/views/storm/pom.xml
@@ -23,6 +23,7 @@
   </parent>
   <properties>
     <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir>
+    <ui.directory>${basedir}/src/main/resources/ui</ui.directory>
   </properties>
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.apache.ambari.contrib.views</groupId>
@@ -55,14 +56,17 @@
           </execution>
         </executions>
       </plugin>
+
       <plugin>
         <groupId>org.apache.rat</groupId>
         <artifactId>apache-rat-plugin</artifactId>
         <configuration>
           <excludes>
-            <exclude>src/main/resources/libs/**</exclude>
-            <exclude>src/main/resources/styles/**</exclude>
-	    <exclude>src/main/resources/templates/**</exclude>
+            <exclude>src/main/resources/ui/.*</exclude>
+            <exclude>src/main/resources/ui/node_modules/**</exclude>
+            <exclude>src/main/resources/ui/public/**</exclude>
+            <exclude>src/main/resources/ui/app/styles/**</exclude>
+            <exclude>src/main/resources/ui/package.json</exclude>
           </excludes>
         </configuration>
         <executions>
@@ -74,6 +78,52 @@
           </execution>
         </executions>
       </plugin>
+
+      <!-- Building frontend -->
+      <plugin>
+        <groupId>com.github.eirslett</groupId>
+        <artifactId>frontend-maven-plugin</artifactId>
+        <version>1.4</version>
+        <configuration>
+          <workingDirectory>src/main/resources/ui/</workingDirectory>
+          <installDirectory>target</installDirectory>
+          <npmInheritsProxyConfigFromMaven>false</npmInheritsProxyConfigFromMaven>
+        </configuration>
+        <executions>
+          <execution>
+            <id>install node and npm</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>install-node-and-npm</goal>
+            </goals>
+            <configuration>
+              <nodeVersion>v5.6.0</nodeVersion>
+              <npmVersion>3.6.0</npmVersion>
+            </configuration>
+          </execution>
+          <execution>
+            <id>npm install</id>
+            <goals>
+              <goal>npm</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <arguments>install</arguments>
+            </configuration>
+          </execution>
+          <execution>
+            <id>npm run build</id>
+            <goals>
+              <goal>npm</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <arguments>run build</arguments>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
       <plugin>
         <artifactId>maven-dependency-plugin</artifactId>
         <executions>
@@ -98,6 +148,27 @@
         </executions>
       </plugin>
     </plugins>
+    <resources>
+      <resource>
+        <directory>src/main/resources/ui/public</directory>
+        <filtering>false</filtering>
+      </resource>
+
+      <resource>
+        <directory>src/main/resources/WEB-INF</directory>
+        <targetPath>WEB-INF</targetPath>
+        <filtering>false</filtering>
+      </resource>
+
+      <resource>
+        <directory>src/main/resources/</directory>
+        <filtering>false</filtering>
+        <includes>
+          <include>view.xml</include>
+          <include>view.log4j.properties</include>
+        </includes>
+      </resource>
+    </resources>
   </build>
   <dependencies>
     <dependency>
@@ -125,4 +196,30 @@
     </dependency>
 
   </dependencies>
+  <profiles>
+    <profile>
+      <id>windows</id>
+      <activation>
+        <os>
+          <family>win</family>
+        </os>
+      </activation>
+      <properties>
+        <node.executable>node.exe</node.executable>
+        <skip.nodegyp.chmod>true</skip.nodegyp.chmod>
+      </properties>
+    </profile>
+    <profile>
+      <id>linux</id>
+      <activation>
+        <os>
+          <family>unix</family>
+        </os>
+      </activation>
+      <properties>
+        <node.executable>node</node.executable>
+        <skip.nodegyp.chmod>false</skip.nodegyp.chmod>
+      </properties>
+    </profile>
+  </profiles>
 </project>

http://git-wip-us.apache.org/repos/asf/ambari/blob/0c188aea/contrib/views/storm/src/main/resources/index.html
----------------------------------------------------------------------
diff --git a/contrib/views/storm/src/main/resources/index.html b/contrib/views/storm/src/main/resources/index.html
deleted file mode 100644
index df94a76..0000000
--- a/contrib/views/storm/src/main/resources/index.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!doctype html>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements. See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License. Kerberos, LDAP, Custom. Binary/Htt
--->
-<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
-<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
-<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
-<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
-    <head>
-        <meta charset="utf-8">
-        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-        <title>Apache Storm</title>
-        <meta name="description" content="">
-        <meta name="viewport" content="width=device-width">
-        <link href='https://fonts.googleapis.com/css?family=Lato:400,400italic,300italic,300,700,700italic' rel='stylesheet' type='text/css'>
-        <link rel="stylesheet" type="text/css" href="libs/Bootstrap/css/bootstrap.css">
-        <link rel="stylesheet" type="text/css" href="libs/Bootstrap/css/bootstrap-switch.min.css">
-        <link rel="stylesheet" type="text/css" href="libs/Bootstrap/css/bootstrap-editable.css">
-        <link rel="stylesheet" type="text/css" href="libs/Bootstrap/css/bootstrap-slider.min.css">
-        <link rel="stylesheet" type="text/css" href="libs/Font-Awesome/css/font-awesome.min.css">
-        <link rel="stylesheet" type="text/css" href="styles/style.css">
-    </head>
-
-    <body>
-        <div class="loader"></div>
-        <div class="container-fluid">
-            <section id="container"></section>
-            <footer id="footer"></footer>    
-        </div>
-        <script data-main="scripts/main" src="libs/require-js/js/require.min.js"></script>
-    </body>
-</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/0c188aea/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js
----------------------------------------------------------------------
diff --git a/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js b/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js
deleted file mode 100644
index d8ccc65..0000000
--- a/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js
+++ /dev/null
@@ -1,1325 +0,0 @@
-/*
-  backbone.paginator 2.0.0
-  http://github.com/backbone-paginator/backbone.paginator
-
-  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
-  Licensed under the MIT @license.
-*/
-
-(function (factory) {
-
-  // CommonJS
-  if (typeof exports == "object") {
-    module.exports = factory(require("underscore"), require("backbone"));
-  }
-  // AMD
-  else if (typeof define == "function" && define.amd) {
-    define(["underscore", "backbone"], factory);
-  }
-  // Browser
-  else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") {
-    var oldPageableCollection = Backbone.PageableCollection;
-    var PageableCollection = factory(_, Backbone);
-
-    /**
-       __BROWSER ONLY__
-
-       If you already have an object named `PageableCollection` attached to the
-       `Backbone` module, you can use this to return a local reference to this
-       Backbone.PageableCollection class and reset the name
-       Backbone.PageableCollection to its previous definition.
-
-           // The left hand side gives you a reference to this
-           // Backbone.PageableCollection implementation, the right hand side
-           // resets Backbone.PageableCollection to your other
-           // Backbone.PageableCollection.
-           var PageableCollection = Backbone.PageableCollection.noConflict();
-
-       @static
-       @member Backbone.PageableCollection
-       @return {Backbone.PageableCollection}
-    */
-    Backbone.PageableCollection.noConflict = function () {
-      Backbone.PageableCollection = oldPageableCollection;
-      return PageableCollection;
-    };
-  }
-
-}(function (_, Backbone) {
-
-  "use strict";
-
-  var _extend = _.extend;
-  var _omit = _.omit;
-  var _clone = _.clone;
-  var _each = _.each;
-  var _pick = _.pick;
-  var _contains = _.contains;
-  var _isEmpty = _.isEmpty;
-  var _pairs = _.pairs;
-  var _invert = _.invert;
-  var _isArray = _.isArray;
-  var _isFunction = _.isFunction;
-  var _isObject = _.isObject;
-  var _keys = _.keys;
-  var _isUndefined = _.isUndefined;
-  var ceil = Math.ceil;
-  var floor = Math.floor;
-  var max = Math.max;
-
-  var BBColProto = Backbone.Collection.prototype;
-
-  function finiteInt (val, name) {
-    if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) {
-      throw new TypeError("`" + name + "` must be a finite integer");
-    }
-    return val;
-  }
-
-  function queryStringToParams (qs) {
-    var kvp, k, v, ls, params = {}, decode = decodeURIComponent;
-    var kvps = qs.split('&');
-    for (var i = 0, l = kvps.length; i < l; i++) {
-      var param = kvps[i];
-      kvp = param.split('='), k = kvp[0], v = kvp[1] || true;
-      k = decode(k), v = decode(v), ls = params[k];
-      if (_isArray(ls)) ls.push(v);
-      else if (ls) params[k] = [ls, v];
-      else params[k] = v;
-    }
-    return params;
-  }
-
-  // hack to make sure the whatever event handlers for this event is run
-  // before func is, and the event handlers that func will trigger.
-  function runOnceAtLastHandler (col, event, func) {
-    var eventHandlers = col._events[event];
-    if (eventHandlers && eventHandlers.length) {
-      var lastHandler = eventHandlers[eventHandlers.length - 1];
-      var oldCallback = lastHandler.callback;
-      lastHandler.callback = function () {
-        try {
-          oldCallback.apply(this, arguments);
-          func();
-        }
-        catch (e) {
-          throw e;
-        }
-        finally {
-          lastHandler.callback = oldCallback;
-        }
-      };
-    }
-    else func();
-  }
-
-  var PARAM_TRIM_RE = /[\s'"]/g;
-  var URL_TRIM_RE = /[<>\s'"]/g;
-
-  /**
-     Drop-in replacement for Backbone.Collection. Supports server-side and
-     client-side pagination and sorting. Client-side mode also support fully
-     multi-directional synchronization of changes between pages.
-
-     @class Backbone.PageableCollection
-     @extends Backbone.Collection
-  */
-  var PageableCollection = Backbone.PageableCollection = Backbone.Collection.extend({
-
-    /**
-       The container object to store all pagination states.
-
-       You can override the default state by extending this class or specifying
-       them in an `options` hash to the constructor.
-
-       @property {Object} state
-
-       @property {0|1} [state.firstPage=1] The first page index. Set to 0 if
-       your server API uses 0-based indices. You should only override this value
-       during extension, initialization or reset by the server after
-       fetching. This value should be read only at other times.
-
-       @property {number} [state.lastPage=null] The last page index. This value
-       is __read only__ and it's calculated based on whether `firstPage` is 0 or
-       1, during bootstrapping, fetching and resetting. Please don't change this
-       value under any circumstances.
-
-       @property {number} [state.currentPage=null] The current page index. You
-       should only override this value during extension, initialization or reset
-       by the server after fetching. This value should be read only at other
-       times. Can be a 0-based or 1-based index, depending on whether
-       `firstPage` is 0 or 1. If left as default, it will be set to `firstPage`
-       on initialization.
-
-       @property {number} [state.pageSize=25] How many records to show per
-       page. This value is __read only__ after initialization, if you want to
-       change the page size after initialization, you must call #setPageSize.
-
-       @property {number} [state.totalPages=null] How many pages there are. This
-       value is __read only__ and it is calculated from `totalRecords`.
-
-       @property {number} [state.totalRecords=null] How many records there
-       are. This value is __required__ under server mode. This value is optional
-       for client mode as the number will be the same as the number of models
-       during bootstrapping and during fetching, either supplied by the server
-       in the metadata, or calculated from the size of the response.
-
-       @property {string} [state.sortKey=null] The model attribute to use for
-       sorting.
-
-       @property {-1|0|1} [state.order=-1] The order to use for sorting. Specify
-       -1 for ascending order or 1 for descending order. If 0, no client side
-       sorting will be done and the order query parameter will not be sent to
-       the server during a fetch.
-    */
-    state: {
-      firstPage: 1,
-      lastPage: null,
-      currentPage: null,
-      pageSize: 25,
-      totalPages: null,
-      totalRecords: null,
-      sortKey: null,
-      order: -1
-    },
-
-    /**
-       @property {"server"|"client"|"infinite"} [mode="server"] The mode of
-       operations for this collection. `"server"` paginates on the server-side,
-       `"client"` paginates on the client-side and `"infinite"` paginates on the
-       server-side for APIs that do not support `totalRecords`.
-    */
-    mode: "server",
-
-    /**
-       A translation map to convert Backbone.PageableCollection state attributes
-       to the query parameters accepted by your server API.
-
-       You can override the default state by extending this class or specifying
-       them in `options.queryParams` object hash to the constructor.
-
-       @property {Object} queryParams
-       @property {string} [queryParams.currentPage="page"]
-       @property {string} [queryParams.pageSize="per_page"]
-       @property {string} [queryParams.totalPages="total_pages"]
-       @property {string} [queryParams.totalRecords="total_entries"]
-       @property {string} [queryParams.sortKey="sort_by"]
-       @property {string} [queryParams.order="order"]
-       @property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A
-       map for translating a Backbone.PageableCollection#state.order constant to
-       the ones your server API accepts.
-    */
-    queryParams: {
-      currentPage: "page",
-      pageSize: "per_page",
-      totalPages: "total_pages",
-      totalRecords: "total_entries",
-      sortKey: "sort_by",
-      order: "order",
-      directions: {
-        "-1": "asc",
-        "1": "desc"
-      }
-    },
-
-    /**
-       __CLIENT MODE ONLY__
-
-       This collection is the internal storage for the bootstrapped or fetched
-       models. You can use this if you want to operate on all the pages.
-
-       @property {Backbone.Collection} fullCollection
-    */
-
-    /**
-       Given a list of models or model attributues, bootstraps the full
-       collection in client mode or infinite mode, or just the page you want in
-       server mode.
-
-       If you want to initialize a collection to a different state than the
-       default, you can specify them in `options.state`. Any state parameters
-       supplied will be merged with the default. If you want to change the
-       default mapping from #state keys to your server API's query parameter
-       names, you can specifiy an object hash in `option.queryParams`. Likewise,
-       any mapping provided will be merged with the default. Lastly, all
-       Backbone.Collection constructor options are also accepted.
-
-       See:
-
-       - Backbone.PageableCollection#state
-       - Backbone.PageableCollection#queryParams
-       - [Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor)
-
-       @param {Array.<Object>} [models]
-
-       @param {Object} [options]
-
-       @param {function(*, *): number} [options.comparator] If specified, this
-       comparator is set to the current page under server mode, or the #fullCollection
-       otherwise.
-
-       @param {boolean} [options.full] If `false` and either a
-       `options.comparator` or `sortKey` is defined, the comparator is attached
-       to the current page. Default is `true` under client or infinite mode and
-       the comparator will be attached to the #fullCollection.
-
-       @param {Object} [options.state] The state attributes overriding the defaults.
-
-       @param {string} [options.state.sortKey] The model attribute to use for
-       sorting. If specified instead of `options.comparator`, a comparator will
-       be automatically created using this value, and optionally a sorting order
-       specified in `options.state.order`. The comparator is then attached to
-       the new collection instance.
-
-       @param {-1|1} [options.state.order] The order to use for sorting. Specify
-       -1 for ascending order and 1 for descending order.
-
-       @param {Object} [options.queryParam]
-    */
-    constructor: function (models, options) {
-
-      BBColProto.constructor.apply(this, arguments);
-
-      options = options || {};
-
-      var mode = this.mode = options.mode || this.mode || PageableProto.mode;
-
-      var queryParams = _extend({}, PageableProto.queryParams, this.queryParams,
-                                options.queryParams || {});
-
-      queryParams.directions = _extend({},
-                                       PageableProto.queryParams.directions,
-                                       this.queryParams.directions,
-                                       queryParams.directions || {});
-
-      this.queryParams = queryParams;
-
-      var state = this.state = _extend({}, PageableProto.state, this.state,
-                                       options.state || {});
-
-      state.currentPage = state.currentPage == null ?
-        state.firstPage :
-        state.currentPage;
-
-      if (!_isArray(models)) models = models ? [models] : [];
-      models = models.slice();
-
-      if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) {
-        state.totalRecords = models.length;
-      }
-
-      this.switchMode(mode, _extend({fetch: false,
-                                     resetState: false,
-                                     models: models}, options));
-
-      var comparator = options.comparator;
-
-      if (state.sortKey && !comparator) {
-        this.setSorting(state.sortKey, state.order, options);
-      }
-
-      if (mode != "server") {
-        var fullCollection = this.fullCollection;
-
-        if (comparator && options.full) {
-          this.comparator = null;
-          fullCollection.comparator = comparator;
-        }
-
-        if (options.full) fullCollection.sort();
-
-        // make sure the models in the current page and full collection have the
-        // same references
-        if (models && !_isEmpty(models)) {
-          this.reset(models, _extend({silent: true}, options));
-          this.getPage(state.currentPage);
-          models.splice.apply(models, [0, models.length].concat(this.models));
-        }
-      }
-
-      this._initState = _clone(this.state);
-    },
-
-    /**
-       Makes a Backbone.Collection that contains all the pages.
-
-       @private
-       @param {Array.<Object|Backbone.Model>} models
-       @param {Object} options Options for Backbone.Collection constructor.
-       @return {Backbone.Collection}
-    */
-    _makeFullCollection: function (models, options) {
-
-      var properties = ["url", "model", "sync", "comparator"];
-      var thisProto = this.constructor.prototype;
-      var i, length, prop;
-
-      var proto = {};
-      for (i = 0, length = properties.length; i < length; i++) {
-        prop = properties[i];
-        if (!_isUndefined(thisProto[prop])) {
-          proto[prop] = thisProto[prop];
-        }
-      }
-
-      var fullCollection = new (Backbone.Collection.extend(proto))(models, options);
-
-      for (i = 0, length = properties.length; i < length; i++) {
-        prop = properties[i];
-        if (this[prop] !== thisProto[prop]) {
-          fullCollection[prop] = this[prop];
-        }
-      }
-
-      return fullCollection;
-    },
-
-    /**
-       Factory method that returns a Backbone event handler that responses to
-       the `add`, `remove`, `reset`, and the `sort` events. The returned event
-       handler will synchronize the current page collection and the full
-       collection's models.
-
-       @private
-
-       @param {Backbone.PageableCollection} pageCol
-       @param {Backbone.Collection} fullCol
-
-       @return {function(string, Backbone.Model, Backbone.Collection, Object)}
-       Collection event handler
-    */
-    _makeCollectionEventHandler: function (pageCol, fullCol) {
-
-      return function collectionEventHandler (event, model, collection, options) {
-
-        var handlers = pageCol._handlers;
-        _each(_keys(handlers), function (event) {
-          var handler = handlers[event];
-          pageCol.off(event, handler);
-          fullCol.off(event, handler);
-        });
-
-        var state = _clone(pageCol.state);
-        var firstPage = state.firstPage;
-        var currentPage = firstPage === 0 ?
-          state.currentPage :
-          state.currentPage - 1;
-        var pageSize = state.pageSize;
-        var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize;
-
-        if (event == "add") {
-          var pageIndex, fullIndex, addAt, colToAdd, options = options || {};
-          if (collection == fullCol) {
-            fullIndex = fullCol.indexOf(model);
-            if (fullIndex >= pageStart && fullIndex < pageEnd) {
-              colToAdd = pageCol;
-              pageIndex = addAt = fullIndex - pageStart;
-            }
-          }
-          else {
-            pageIndex = pageCol.indexOf(model);
-            fullIndex = pageStart + pageIndex;
-            colToAdd = fullCol;
-            var addAt = !_isUndefined(options.at) ?
-              options.at + pageStart :
-              fullIndex;
-          }
-
-          if (!options.onRemove) {
-            ++state.totalRecords;
-            delete options.onRemove;
-          }
-
-          pageCol.state = pageCol._checkState(state);
-
-          if (colToAdd) {
-            colToAdd.add(model, _extend({}, options || {}, {at: addAt}));
-            var modelToRemove = pageIndex >= pageSize ?
-              model :
-              !_isUndefined(options.at) && addAt < pageEnd && pageCol.length > pageSize ?
-              pageCol.at(pageSize) :
-              null;
-            if (modelToRemove) {
-              runOnceAtLastHandler(collection, event, function () {
-                pageCol.remove(modelToRemove, {onAdd: true});
-              });
-            }
-          }
-        }
-
-        // remove the model from the other collection as well
-        if (event == "remove") {
-          if (!options.onAdd) {
-            // decrement totalRecords and update totalPages and lastPage
-            if (!--state.totalRecords) {
-              state.totalRecords = null;
-              state.totalPages = null;
-            }
-            else {
-              var totalPages = state.totalPages = ceil(state.totalRecords / pageSize);
-              state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages || firstPage;
-              if (state.currentPage > totalPages) state.currentPage = state.lastPage;
-            }
-            pageCol.state = pageCol._checkState(state);
-
-            var nextModel, removedIndex = options.index;
-            if (collection == pageCol) {
-              if (nextModel = fullCol.at(pageEnd)) {
-                runOnceAtLastHandler(pageCol, event, function () {
-                  pageCol.push(nextModel, {onRemove: true});
-                });
-              }
-              else if (!pageCol.length && state.totalRecords) {
-                pageCol.reset(fullCol.models.slice(pageStart - pageSize, pageEnd - pageSize),
-                              _extend({}, options, {parse: false}));
-              }
-              fullCol.remove(model);
-            }
-            else if (removedIndex >= pageStart && removedIndex < pageEnd) {
-              if (nextModel = fullCol.at(pageEnd - 1)) {
-                runOnceAtLastHandler(pageCol, event, function() {
-                  pageCol.push(nextModel, {onRemove: true});
-                });
-              }
-              pageCol.remove(model);
-              if (!pageCol.length && state.totalRecords) {
-                pageCol.reset(fullCol.models.slice(pageStart - pageSize, pageEnd - pageSize),
-                              _extend({}, options, {parse: false}));
-              }
-            }
-          }
-          else delete options.onAdd;
-        }
-
-        if (event == "reset") {
-          options = collection;
-          collection = model;
-
-          // Reset that's not a result of getPage
-          if (collection == pageCol && options.from == null &&
-              options.to == null) {
-            var head = fullCol.models.slice(0, pageStart);
-            var tail = fullCol.models.slice(pageStart + pageCol.models.length);
-            fullCol.reset(head.concat(pageCol.models).concat(tail), options);
-          }
-          else if (collection == fullCol) {
-            if (!(state.totalRecords = fullCol.models.length)) {
-              state.totalRecords = null;
-              state.totalPages = null;
-            }
-            if (pageCol.mode == "client") {
-              state.lastPage = state.currentPage = state.firstPage;
-            }
-            pageCol.state = pageCol._checkState(state);
-            pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
-                          _extend({}, options, {parse: false}));
-          }
-        }
-
-        if (event == "sort") {
-          options = collection;
-          collection = model;
-          if (collection === fullCol) {
-            pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
-                          _extend({}, options, {parse: false}));
-          }
-        }
-
-        _each(_keys(handlers), function (event) {
-          var handler = handlers[event];
-          _each([pageCol, fullCol], function (col) {
-            col.on(event, handler);
-            var callbacks = col._events[event] || [];
-            callbacks.unshift(callbacks.pop());
-          });
-        });
-      };
-    },
-
-    /**
-       Sanity check this collection's pagination states. Only perform checks
-       when all the required pagination state values are defined and not null.
-       If `totalPages` is undefined or null, it is set to `totalRecords` /
-       `pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1
-       when no error occurs.
-
-       @private
-
-       @throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or
-       `firstPage` is not a finite integer.
-
-       @throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out
-       of bounds.
-
-       @return {Object} Returns the `state` object if no error was found.
-    */
-    _checkState: function (state) {
-
-      var mode = this.mode;
-      var links = this.links;
-      var totalRecords = state.totalRecords;
-      var pageSize = state.pageSize;
-      var currentPage = state.currentPage;
-      var firstPage = state.firstPage;
-      var totalPages = state.totalPages;
-
-      if (totalRecords != null && pageSize != null && currentPage != null &&
-          firstPage != null && (mode == "infinite" ? links : true)) {
-
-        totalRecords = finiteInt(totalRecords, "totalRecords");
-        pageSize = finiteInt(pageSize, "pageSize");
-        currentPage = finiteInt(currentPage, "currentPage");
-        firstPage = finiteInt(firstPage, "firstPage");
-
-        if (pageSize < 1) {
-          throw new RangeError("`pageSize` must be >= 1");
-        }
-
-        totalPages = state.totalPages = ceil(totalRecords / pageSize);
-
-        if (firstPage < 0 || firstPage > 1) {
-          throw new RangeError("`firstPage must be 0 or 1`");
-        }
-
-        state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage;
-
-        if (mode == "infinite") {
-          if (!links[currentPage + '']) {
-            throw new RangeError("No link found for page " + currentPage);
-          }
-        }
-        else if (currentPage < firstPage ||
-                 (totalPages > 0 &&
-                  (firstPage ? currentPage > totalPages : currentPage >= totalPages))) {
-          throw new RangeError("`currentPage` must be firstPage <= currentPage " +
-                               (firstPage ? ">" : ">=") +
-                               " totalPages if " + firstPage + "-based. Got " +
-                               currentPage + '.');
-        }
-      }
-
-      return state;
-    },
-
-    /**
-       Change the page size of this collection.
-
-       Under most if not all circumstances, you should call this method to
-       change the page size of a pageable collection because it will keep the
-       pagination state sane. By default, the method will recalculate the
-       current page number to one that will retain the current page's models
-       when increasing the page size. When decreasing the page size, this method
-       will retain the last models to the current page that will fit into the
-       smaller page size.
-
-       If `options.first` is true, changing the page size will also reset the
-       current page back to the first page instead of trying to be smart.
-
-       For server mode operations, changing the page size will trigger a #fetch
-       and subsequently a `reset` event.
-
-       For client mode operations, changing the page size will `reset` the
-       current page by recalculating the current page boundary on the client
-       side.
-
-       If `options.fetch` is true, a fetch can be forced if the collection is in
-       client mode.
-
-       @param {number} pageSize The new page size to set to #state.
-       @param {Object} [options] {@link #fetch} options.
-       @param {boolean} [options.first=false] Reset the current page number to
-       the first page if `true`.
-       @param {boolean} [options.fetch] If `true`, force a fetch in client mode.
-
-       @throws {TypeError} If `pageSize` is not a finite integer.
-       @throws {RangeError} If `pageSize` is less than 1.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this.
-    */
-    setPageSize: function (pageSize, options) {
-      pageSize = finiteInt(pageSize, "pageSize");
-
-      options = options || {first: false};
-
-      var state = this.state;
-      var totalPages = ceil(state.totalRecords / pageSize);
-      var currentPage = totalPages ?
-          max(state.firstPage, floor(totalPages * state.currentPage / state.totalPages)) :
-        state.firstPage;
-
-      state = this.state = this._checkState(_extend({}, state, {
-        pageSize: pageSize,
-        currentPage: options.first ? state.firstPage : currentPage,
-        totalPages: totalPages
-      }));
-
-      return this.getPage(state.currentPage, _omit(options, ["first"]));
-    },
-
-    /**
-       Switching between client, server and infinite mode.
-
-       If switching from client to server mode, the #fullCollection is emptied
-       first and then deleted and a fetch is immediately issued for the current
-       page from the server. Pass `false` to `options.fetch` to skip fetching.
-
-       If switching to infinite mode, and if `options.models` is given for an
-       array of models, #links will be populated with a URL per page, using the
-       default URL for this collection.
-
-       If switching from server to client mode, all of the pages are immediately
-       refetched. If you have too many pages, you can pass `false` to
-       `options.fetch` to skip fetching.
-
-       If switching to any mode from infinite mode, the #links will be deleted.
-
-       @param {"server"|"client"|"infinite"} [mode] The mode to switch to.
-
-       @param {Object} [options]
-
-       @param {boolean} [options.fetch=true] If `false`, no fetching is done.
-
-       @param {boolean} [options.resetState=true] If 'false', the state is not
-       reset, but checked for sanity instead.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this if `options.fetch` is `false`.
-    */
-    switchMode: function (mode, options) {
-
-      if (!_contains(["server", "client", "infinite"], mode)) {
-        throw new TypeError('`mode` must be one of "server", "client" or "infinite"');
-      }
-
-      options = options || {fetch: true, resetState: true};
-
-      var state = this.state = options.resetState ?
-        _clone(this._initState) :
-        this._checkState(_extend({}, this.state));
-
-      this.mode = mode;
-
-      var self = this;
-      var fullCollection = this.fullCollection;
-      var handlers = this._handlers = this._handlers || {}, handler;
-      if (mode != "server" && !fullCollection) {
-        fullCollection = this._makeFullCollection(options.models || [], options);
-        fullCollection.pageableCollection = this;
-        this.fullCollection = fullCollection;
-        var allHandler = this._makeCollectionEventHandler(this, fullCollection);
-        _each(["add", "remove", "reset", "sort"], function (event) {
-          handlers[event] = handler = _.bind(allHandler, {}, event);
-          self.on(event, handler);
-          fullCollection.on(event, handler);
-        });
-        fullCollection.comparator = this._fullComparator;
-      }
-      else if (mode == "server" && fullCollection) {
-        _each(_keys(handlers), function (event) {
-          handler = handlers[event];
-          self.off(event, handler);
-          fullCollection.off(event, handler);
-        });
-        delete this._handlers;
-        this._fullComparator = fullCollection.comparator;
-        delete this.fullCollection;
-      }
-
-      if (mode == "infinite") {
-        var links = this.links = {};
-        var firstPage = state.firstPage;
-        var totalPages = ceil(state.totalRecords / state.pageSize);
-        var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage;
-        for (var i = state.firstPage; i <= lastPage; i++) {
-          links[i] = this.url;
-        }
-      }
-      else if (this.links) delete this.links;
-
-      return options.fetch ?
-        this.fetch(_omit(options, "fetch", "resetState")) :
-        this;
-    },
-
-    /**
-       @return {boolean} `true` if this collection can page backward, `false`
-       otherwise.
-    */
-    hasPreviousPage: function () {
-      var state = this.state;
-      var currentPage = state.currentPage;
-      if (this.mode != "infinite") return currentPage > state.firstPage;
-      return !!this.links[currentPage - 1];
-    },
-
-    /**
-       @return {boolean} `true` if this collection can page forward, `false`
-       otherwise.
-    */
-    hasNextPage: function () {
-      var state = this.state;
-      var currentPage = this.state.currentPage;
-      if (this.mode != "infinite") return currentPage < state.lastPage;
-      return !!this.links[currentPage + 1];
-    },
-
-    /**
-       Fetch the first page in server mode, or reset the current page of this
-       collection to the first page in client or infinite mode.
-
-       @param {Object} options {@link #getPage} options.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this.
-    */
-    getFirstPage: function (options) {
-      return this.getPage("first", options);
-    },
-
-    /**
-       Fetch the previous page in server mode, or reset the current page of this
-       collection to the previous page in client or infinite mode.
-
-       @param {Object} options {@link #getPage} options.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this.
-    */
-    getPreviousPage: function (options) {
-      return this.getPage("prev", options);
-    },
-
-    /**
-       Fetch the next page in server mode, or reset the current page of this
-       collection to the next page in client mode.
-
-       @param {Object} options {@link #getPage} options.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this.
-    */
-    getNextPage: function (options) {
-      return this.getPage("next", options);
-    },
-
-    /**
-       Fetch the last page in server mode, or reset the current page of this
-       collection to the last page in client mode.
-
-       @param {Object} options {@link #getPage} options.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this.
-    */
-    getLastPage: function (options) {
-      return this.getPage("last", options);
-    },
-
-    /**
-       Given a page index, set #state.currentPage to that index. If this
-       collection is in server mode, fetch the page using the updated state,
-       otherwise, reset the current page of this collection to the page
-       specified by `index` in client mode. If `options.fetch` is true, a fetch
-       can be forced in client mode before resetting the current page. Under
-       infinite mode, if the index is less than the current page, a reset is
-       done as in client mode. If the index is greater than the current page
-       number, a fetch is made with the results **appended** to #fullCollection.
-       The current page will then be reset after fetching.
-
-       @param {number|string} index The page index to go to, or the page name to
-       look up from #links in infinite mode.
-       @param {Object} [options] {@link #fetch} options or
-       [reset](http://backbonejs.org/#Collection-reset) options for client mode
-       when `options.fetch` is `false`.
-       @param {boolean} [options.fetch=false] If true, force a {@link #fetch} in
-       client mode.
-
-       @throws {TypeError} If `index` is not a finite integer under server or
-       client mode, or does not yield a URL from #links under infinite mode.
-
-       @throws {RangeError} If `index` is out of bounds.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this.
-    */
-    getPage: function (index, options) {
-
-      var mode = this.mode, fullCollection = this.fullCollection;
-
-      options = options || {fetch: false};
-
-      var state = this.state,
-      firstPage = state.firstPage,
-      currentPage = state.currentPage,
-      lastPage = state.lastPage,
-      pageSize = state.pageSize;
-
-      var pageNum = index;
-      switch (index) {
-        case "first": pageNum = firstPage; break;
-        case "prev": pageNum = currentPage - 1; break;
-        case "next": pageNum = currentPage + 1; break;
-        case "last": pageNum = lastPage; break;
-        default: pageNum = finiteInt(index, "index");
-      }
-
-      this.state = this._checkState(_extend({}, state, {currentPage: pageNum}));
-
-      options.from = currentPage, options.to = pageNum;
-
-      var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize;
-      var pageModels = fullCollection && fullCollection.length ?
-        fullCollection.models.slice(pageStart, pageStart + pageSize) :
-        [];
-      if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) &&
-          !options.fetch) {
-        this.reset(pageModels, _omit(options, "fetch"));
-        return this;
-      }
-
-      if (mode == "infinite") options.url = this.links[pageNum];
-
-      return this.fetch(_omit(options, "fetch"));
-    },
-
-    /**
-       Fetch the page for the provided item offset in server mode, or reset the current page of this
-       collection to the page for the provided item offset in client mode.
-
-       @param {Object} options {@link #getPage} options.
-
-       @chainable
-       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
-       from fetch or this.
-    */
-    getPageByOffset: function (offset, options) {
-      if (offset < 0) {
-        throw new RangeError("`offset must be > 0`");
-      }
-      offset = finiteInt(offset);
-
-      var page = floor(offset / this.state.pageSize);
-      if (this.state.firstPage !== 0) page++;
-      if (page > this.state.lastPage) page = this.state.lastPage;
-      return this.getPage(page, options);
-    },
-
-    /**
-       Overidden to make `getPage` compatible with Zepto.
-
-       @param {string} method
-       @param {Backbone.Model|Backbone.Collection} model
-       @param {Object} [options]
-
-       @return {XMLHttpRequest}
-    */
-    sync: function (method, model, options) {
-      var self = this;
-      if (self.mode == "infinite") {
-        var success = options.success;
-        var currentPage = self.state.currentPage;
-        options.success = function (resp, status, xhr) {
-          var links = self.links;
-          var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options));
-          if (newLinks.first) links[self.state.firstPage] = newLinks.first;
-          if (newLinks.prev) links[currentPage - 1] = newLinks.prev;
-          if (newLinks.next) links[currentPage + 1] = newLinks.next;
-          if (success) success(resp, status, xhr);
-        };
-      }
-
-      return (BBColProto.sync || Backbone.sync).call(self, method, model, options);
-    },
-
-    /**
-       Parse pagination links from the server response. Only valid under
-       infinite mode.
-
-       Given a response body and a XMLHttpRequest object, extract pagination
-       links from them for infinite paging.
-
-       This default implementation parses the RFC 5988 `Link` header and extract
-       3 links from it - `first`, `prev`, `next`. Any subclasses overriding this
-       method __must__ return an object hash having only the keys
-       above. However, simply returning a `next` link or an empty hash if there
-       are no more links should be enough for most implementations.
-
-       @param {*} resp The deserialized response body.
-       @param {Object} [options]
-       @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this
-       response.
-       @return {Object}
-    */
-    parseLinks: function (resp, options) {
-      var links = {};
-      var linkHeader = options.xhr.getResponseHeader("Link");
-      if (linkHeader) {
-        var relations = ["first", "prev", "next"];
-        _each(linkHeader.split(","), function (linkValue) {
-          var linkParts = linkValue.split(";");
-          var url = linkParts[0].replace(URL_TRIM_RE, '');
-          var params = linkParts.slice(1);
-          _each(params, function (param) {
-            var paramParts = param.split("=");
-            var key = paramParts[0].replace(PARAM_TRIM_RE, '');
-            var value = paramParts[1].replace(PARAM_TRIM_RE, '');
-            if (key == "rel" && _contains(relations, value)) links[value] = url;
-          });
-        });
-      }
-
-      return links;
-    },
-
-    /**
-       Parse server response data.
-
-       This default implementation assumes the response data is in one of two
-       structures:
-
-           [
-             {}, // Your new pagination state
-             [{}, ...] // An array of JSON objects
-           ]
-
-       Or,
-
-           [{}] // An array of JSON objects
-
-       The first structure is the preferred form because the pagination states
-       may have been updated on the server side, sending them down again allows
-       this collection to update its states. If the response has a pagination
-       state object, it is checked for errors.
-
-       The second structure is the
-       [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse)
-       default.
-
-       **Note:** this method has been further simplified since 1.1.7. While
-       existing #parse implementations will continue to work, new code is
-       encouraged to override #parseState and #parseRecords instead.
-
-       @param {Object} resp The deserialized response data from the server.
-       @param {Object} the options for the ajax request
-
-       @return {Array.<Object>} An array of model objects
-    */
-    parse: function (resp, options) {
-      var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options);
-      if (newState) this.state = this._checkState(_extend({}, this.state, newState));
-      return this.parseRecords(resp, options);
-    },
-
-    /**
-       Parse server response for server pagination state updates. Not applicable
-       under infinite mode.
-
-       This default implementation first checks whether the response has any
-       state object as documented in #parse. If it exists, a state object is
-       returned by mapping the server state keys to this pageable collection
-       instance's query parameter keys using `queryParams`.
-
-       It is __NOT__ neccessary to return a full state object complete with all
-       the mappings defined in #queryParams. Any state object resulted is merged
-       with a copy of the current pageable collection state and checked for
-       sanity before actually updating. Most of the time, simply providing a new
-       `totalRecords` value is enough to trigger a full pagination state
-       recalculation.
-
-           parseState: function (resp, queryParams, state, options) {
-             return {totalRecords: resp.total_entries};
-           }
-
-       If you want to use header fields use:
-
-           parseState: function (resp, queryParams, state, options) {
-               return {totalRecords: options.xhr.getResponseHeader("X-total")};
-           }
-
-       This method __MUST__ return a new state object instead of directly
-       modifying the #state object. The behavior of directly modifying #state is
-       undefined.
-
-       @param {Object} resp The deserialized response data from the server.
-       @param {Object} queryParams A copy of #queryParams.
-       @param {Object} state A copy of #state.
-       @param {Object} [options] The options passed through from
-       `parse`. (backbone >= 0.9.10 only)
-
-       @return {Object} A new (partial) state object.
-     */
-    parseState: function (resp, queryParams, state, options) {
-      if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
-
-        var newState = _clone(state);
-        var serverState = resp[0];
-
-        _each(_pairs(_omit(queryParams, "directions")), function (kvp) {
-          var k = kvp[0], v = kvp[1];
-          var serverVal = serverState[v];
-          if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v];
-        });
-
-        if (serverState.order) {
-          newState.order = _invert(queryParams.directions)[serverState.order] * 1;
-        }
-
-        return newState;
-      }
-    },
-
-    /**
-       Parse server response for an array of model objects.
-
-       This default implementation first checks whether the response has any
-       state object as documented in #parse. If it exists, the array of model
-       objects is assumed to be the second element, otherwise the entire
-       response is returned directly.
-
-       @param {Object} resp The deserialized response data from the server.
-       @param {Object} [options] The options passed through from the
-       `parse`. (backbone >= 0.9.10 only)
-
-       @return {Array.<Object>} An array of model objects
-     */
-    parseRecords: function (resp, options) {
-      if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
-        return resp[1];
-      }
-
-      return resp;
-    },
-
-    /**
-       Fetch a page from the server in server mode, or all the pages in client
-       mode. Under infinite mode, the current page is refetched by default and
-       then reset.
-
-       The query string is constructed by translating the current pagination
-       state to your server API query parameter using #queryParams. The current
-       page will reset after fetch.
-
-       @param {Object} [options] Accepts all
-       [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
-       options.
-
-       @return {XMLHttpRequest}
-    */
-    fetch: function (options) {
-
-      options = options || {};
-
-      var state = this._checkState(this.state);
-
-      var mode = this.mode;
-
-      if (mode == "infinite" && !options.url) {
-        options.url = this.links[state.currentPage];
-      }
-
-      var data = options.data || {};
-
-      // dedup query params
-      var url = options.url || this.url || "";
-      if (_isFunction(url)) url = url.call(this);
-      var qsi = url.indexOf('?');
-      if (qsi != -1) {
-        _extend(data, queryStringToParams(url.slice(qsi + 1)));
-        url = url.slice(0, qsi);
-      }
-
-      options.url = url;
-      options.data = data;
-
-      // map params except directions
-      var queryParams = this.mode == "client" ?
-        _pick(this.queryParams, "sortKey", "order") :
-        _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
-              "directions");
-
-      var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
-      for (i = 0; i < kvps.length; i++) {
-        kvp = kvps[i], k = kvp[0], v = kvp[1];
-        v = _isFunction(v) ? v.call(thisCopy) : v;
-        if (state[k] != null && v != null) {
-          data[v] = state[k];
-        }
-      }
-
-      // fix up sorting parameters
-      if (state.sortKey && state.order) {
-        var o = _isFunction(queryParams.order) ?
-          queryParams.order.call(thisCopy) :
-          queryParams.order;
-        data[o] = this.queryParams.directions[state.order + ""];
-      }
-      else if (!state.sortKey) delete data[queryParams.order];
-
-      // map extra query parameters
-      var extraKvps = _pairs(_omit(this.queryParams,
-                                   _keys(PageableProto.queryParams)));
-      for (i = 0; i < extraKvps.length; i++) {
-        kvp = extraKvps[i];
-        v = kvp[1];
-        v = _isFunction(v) ? v.call(thisCopy) : v;
-        if (v != null) data[kvp[0]] = v;
-      }
-
-      if (mode != "server") {
-        var self = this, fullCol = this.fullCollection;
-        var success = options.success;
-        options.success = function (col, resp, opts) {
-
-          // make sure the caller's intent is obeyed
-          opts = opts || {};
-          if (_isUndefined(options.silent)) delete opts.silent;
-          else opts.silent = options.silent;
-
-          var models = col.models;
-          if (mode == "client") fullCol.reset(models, opts);
-          else {
-            fullCol.add(models, _extend({at: fullCol.length},
-                                        _extend(opts, {parse: false})));
-            self.trigger("reset", self, opts);
-          }
-
-          if (success) success(col, resp, opts);
-        };
-
-        // silent the first reset from backbone
-        return BBColProto.fetch.call(this, _extend({}, options, {silent: true}));
-      }
-
-      return BBColProto.fetch.call(this, options);
-    },
-
-    /**
-       Convenient method for making a `comparator` sorted by a model attribute
-       identified by `sortKey` and ordered by `order`.
-
-       Like a Backbone.Collection, a Backbone.PageableCollection will maintain
-       the __current page__ in sorted order on the client side if a `comparator`
-       is attached to it. If the collection is in client mode, you can attach a
-       comparator to #fullCollection to have all the pages reflect the global
-       sorting order by specifying an option `full` to `true`. You __must__ call
-       `sort` manually or #fullCollection.sort after calling this method to
-       force a resort.
-
-       While you can use this method to sort the current page in server mode,
-       the sorting order may not reflect the global sorting order due to the
-       additions or removals of the records on the server since the last
-       fetch. If you want the most updated page in a global sorting order, it is
-       recommended that you set #state.sortKey and optionally #state.order, and
-       then call #fetch.
-
-       @protected
-
-       @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
-       @param {number} [order=this.state.order] See `state.order`.
-       @param {(function(Backbone.Model, string): Object) | string} [sortValue] See #setSorting.
-
-       See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
-    */
-    _makeComparator: function (sortKey, order, sortValue) {
-      var state = this.state;
-
-      sortKey = sortKey || state.sortKey;
-      order = order || state.order;
-
-      if (!sortKey || !order) return;
-
-      if (!sortValue) sortValue = function (model, attr) {
-        return model.get(attr);
-      };
-
-      return function (left, right) {
-        var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t;
-        if (order === 1) t = l, l = r, r = t;
-        if (l === r) return 0;
-        else if (l < r) return -1;
-        return 1;
-      };
-    },
-
-    /**
-       Adjusts the sorting for this pageable collection.
-
-       Given a `sortKey` and an `order`, sets `state.sortKey` and
-       `state.order`. A comparator can be applied on the client side to sort in
-       the order defined if `options.side` is `"client"`. By default the
-       comparator is applied to the #fullCollection. Set `options.full` to
-       `false` to apply a comparator to the current page under any mode. Setting
-       `sortKey` to `null` removes the comparator from both the current page and
-       the full collection.
-
-       If a `sortValue` function is given, it will be passed the `(model,
-       sortKey)` arguments and is used to extract a value from the model during
-       comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is
-       used for sorting.
-
-       @chainable
-
-       @param {string} sortKey See `state.sortKey`.
-       @param {number} [order=this.state.order] See `state.order`.
-       @param {Object} [options]
-       @param {"server"|"client"} [options.side] By default, `"client"` if
-       `mode` is `"client"`, `"server"` otherwise.
-       @param {boolean} [options.full=true]
-       @param {(function(Backbone.Model, string): Object) | string} [options.sortValue]
-    */
-    setSorting: function (sortKey, order, options) {
-
-      var state = this.state;
-
-      state.sortKey = sortKey;
-      state.order = order = order || state.order;
-
-      var fullCollection = this.fullCollection;
-
-      var delComp = false, delFullComp = false;
-
-      if (!sortKey) delComp = delFullComp = true;
-
-      var mode = this.mode;
-      options = _extend({side: mode == "client" ? mode : "server", full: true},
-                        options);
-
-      var comparator = this._makeComparator(sortKey, order, options.sortValue);
-
-      var full = options.full, side = options.side;
-
-      if (side == "client") {
-        if (full) {
-          if (fullCollection) fullCollection.comparator = comparator;
-          delComp = true;
-        }
-        else {
-          this.comparator = comparator;
-          delFullComp = true;
-        }
-      }
-      else if (side == "server" && !full) {
-        this.comparator = comparator;
-      }
-
-      if (delComp) this.comparator = null;
-      if (delFullComp && fullCollection) fullCollection.comparator = null;
-
-      return this;
-    }
-
-  });
-
-  var PageableProto = PageableCollection.prototype;
-
-  return PageableCollection;
-
-}));