├── .gitattributes
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
├── sonatype.sbt
└── src
├── main
├── resources
│ └── dependencies
│ │ └── halbrowser
│ │ ├── MIT-LICENSE.txt
│ │ ├── README.adoc
│ │ ├── browser.html
│ │ ├── js
│ │ ├── hal.js
│ │ └── hal
│ │ │ ├── browser.js
│ │ │ ├── http
│ │ │ └── client.js
│ │ │ ├── resource.js
│ │ │ └── views
│ │ │ ├── browser.js
│ │ │ ├── documentation.js
│ │ │ ├── embedded_resource.js
│ │ │ ├── embedded_resources.js
│ │ │ ├── explorer.js
│ │ │ ├── inspector.js
│ │ │ ├── links.js
│ │ │ ├── location_bar.js
│ │ │ ├── navigation.js
│ │ │ ├── non_safe_request_dialog.js
│ │ │ ├── properties.js
│ │ │ ├── query_uri_dialog.js
│ │ │ ├── request_headers.js
│ │ │ ├── resource.js
│ │ │ ├── response.js
│ │ │ ├── response_body.js
│ │ │ └── response_headers.js
│ │ ├── styles.css
│ │ └── vendor
│ │ ├── css
│ │ ├── bootstrap-responsive.css
│ │ └── bootstrap.css
│ │ ├── img
│ │ ├── ajax-loader.gif
│ │ ├── glyphicons-halflings-white.png
│ │ └── glyphicons-halflings.png
│ │ └── js
│ │ ├── URI.min.js
│ │ ├── backbone.js
│ │ ├── bootstrap.js
│ │ ├── jquery-1.10.2.js
│ │ ├── jquery-1.10.2.min.js
│ │ ├── jquery-1.10.2.min.map
│ │ ├── underscore.js
│ │ └── uritemplates.js
└── scala
│ └── io
│ └── pileworx
│ └── akka
│ └── http
│ └── rest
│ └── hal
│ ├── Hal.scala
│ ├── HalBrowserService.scala
│ ├── Href.scala
│ └── Relations.scala
└── test
└── scala
└── io
└── pileworx
└── akka
└── http
└── rest
└── hal
├── HalBrowserServiceSpec.scala
├── HalSpec.scala
└── HrefSpec.scala
/.gitattributes:
--------------------------------------------------------------------------------
1 | src/main/resources/dependencies/* linguist-vendored
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | target/
3 | .worksheet
4 | *.sc
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala:
3 | - 2.12.8
4 | jdk:
5 | - openjdk11
6 |
7 | stages:
8 | - crosscompile
9 | - test
10 |
11 | jobs:
12 | include:
13 | - stage: crosscompile
14 | script: sbt +clean +test
15 | - stage: test
16 | script: sbt clean coverage test coverageReport coverageAggregate codacyCoverage
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016-2019 Pileworx.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | akka-http-hal
2 | =============
3 |
4 | HAL Specification support library for akka-http.
5 |
6 | Licensed under the Apache 2 license.
7 |
8 | [](https://travis-ci.org/pileworx/akka-http-hal)
9 | [](https://www.codacy.com/app/Pileworx/akka-http-hal?utm_source=github.com&utm_medium=referral&utm_content=pileworx/akka-http-hal&utm_campaign=Badge_Grade)
10 | [](https://www.codacy.com/app/Pileworx/akka-http-hal?utm_source=github.com&utm_medium=referral&utm_content=pileworx/akka-http-hal&utm_campaign=Badge_Coverage)
11 |
12 | Getting Started
13 | ---------------
14 |
15 | Installation:
16 | ```scala
17 | libraryDependencies += "io.pileworx" %% "akka-http-hal" % "1.2.5"
18 | ```
19 | Support for Scala 2.11, 2.12, 2.13.
20 |
21 | Usage
22 | -----
23 | Create your marshaller:
24 | ```scala
25 | trait FooProtocol extends DefaultJsonProtocol {
26 | implicit val fooFormat = jsonFormat3(FooDto)
27 | }
28 | ```
29 |
30 | You can import a collection of IANA Relations (Self, Next, etc):
31 | ```scala
32 | import io.pileworx.akka.http.rest.hal.Relations._
33 | ```
34 |
35 | Create a resource adapter:
36 | ```scala
37 | trait FooAdapter extends FooProtocol {
38 | def fooLink(rel: String, id: String) = rel -> Link(href = s"/foos/$id")
39 | def foosLink(rel: String) = rel -> Link(href = "/foos")
40 |
41 | def newResource(id: String): JsValue = {
42 | ResourceBuilder(
43 | withLinks = Some(Map(
44 | fooLink(Self, id),
45 | foosLink(Up)
46 | ))
47 | ).build
48 | }
49 |
50 | def notFoundResource: JsValue = {
51 | ResourceBuilder(
52 | withLinks = Some(Map(contactsLink(Up)))
53 | ).build
54 | }
55 |
56 | def toResources(foos: Seq[FooDto]): JsValue = {
57 | ResourceBuilder(
58 | withEmbedded = Some(Map(
59 | "foos" -> foos.map(f => toResource(f))
60 | )),
61 | withLinks = Some(Map(foosLink(Self)))
62 | ).build
63 | }
64 |
65 | def toResource(foo: FooDto): JsValue = {
66 | ResourceBuilder(
67 | withData = Some(foo.toJson),
68 | withLinks = Some(Map(
69 | fooLink(Self, foo.id),
70 | foosLink(Up)
71 | ))
72 | ).build
73 | }
74 | }
75 | ```
76 | Create your routes:
77 | ```scala
78 | trait FooRestPort extends FooAdapter {
79 |
80 | val fooService = new FooService with FooComponent
81 |
82 | val fooRoutes = path("foos") {
83 | get {
84 | complete {
85 | fooService.getAll.map(f => toResources(f))
86 | }
87 | } ~
88 | post {
89 | entity(as[CreateFooCommand]) { newFoo =>
90 | complete {
91 | Created -> fooService.add(newFoo).map(id => newResource(id))
92 | }
93 | }
94 | }
95 | } ~
96 | pathPrefix("foos" / Segment) { id =>
97 | get {
98 | complete {
99 | fooService.getById(id).map {
100 | case Some(f) => Marshal(toResource(f)).to[HttpResponse]
101 | case _ => Marshal(NotFound -> notFoundResource).to[HttpResponse]
102 | }
103 | }
104 | }
105 | }
106 | }
107 | ```
108 |
109 | Curies Support
110 | --------------
111 |
112 | Curies are supported in two ways.
113 |
114 | The first is per resource:
115 | ```scala
116 | ResourceBuilder(
117 | withCuries = Some(Seq(
118 | Curie(name = "ts", href = "http://typesafe.com/{rel}")
119 | ))).build
120 | ```
121 | The second, and most likely more common way, is to set them globally:
122 | ```scala
123 | ResourceBuilder.curies(Seq(
124 | Curie(name = "ts", href = "http://typesafe.com/{rel}"),
125 | Curie(name = "akka", href = "http://akka.io/{rel}")
126 | ))
127 | ```
128 | Note: If you mix global and resource based curies they will be combined. Currently we do not check for duplicate entries.
129 |
130 | For the links pointing to a curie, just prefix the key with the curie name and colon (ex "ts:info"). If a colon is found in a key, we do not alter the href by adding X-Forwarded data or the request host/port.
131 |
132 | Array of Links Support
133 | ----------------------
134 | If you require an array of links:
135 | ```json
136 | {
137 | "_links": {
138 | "multiple_links": [
139 | {
140 | "href": "http://www.test.com?foo=bar",
141 | "name": "one"
142 | },
143 | {
144 | "href": "http://www.test.com?bar=baz",
145 | "name": "two"
146 | }
147 | ]
148 | }
149 | }
150 | ```
151 | This can be achieved by using the Links class which accepts a Sequence of Link:
152 |
153 | ```scala
154 | Map(
155 | "multiple_links" -> Links(Seq(
156 | Link(href = url, name = Some("one")),
157 | Link(href = url, name = Some("two"))
158 | )))
159 | ```
160 |
161 | HttpRequest Support
162 | -------------------
163 |
164 | By default the HAL links will not include the host or port.
165 |
166 | If you would like host, port, or path prefix included, provide the HttpRequest.
167 |
168 | ```scala
169 | def toResource(foo: FooDto, req: HttpRequest): JsValue = {
170 | ResourceBuilder(
171 | withRequest = req,
172 | withData = Some(foo.toJson),
173 | withLinks = Some(Map(
174 | fooLink("self", foo.id),
175 | foosLink("parent")
176 | ))
177 | ).build
178 | }
179 | ```
180 |
181 | This will produce a link with either the current host's information OR construct the links based on
182 | the X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port, and X-Forwarded-Prefix headers.
183 |
184 | HAL Browser
185 | -----------
186 | To expose HAL Browser from your API add the halBrowser route.
187 |
188 | ```scala
189 | val routes = otherRoutes ~ halBrowserRoutes
190 | ```
191 |
192 | The browser will be available at /halbrowser.
193 |
194 | TODO
195 | -----------
196 | Find more contributors (hint).
197 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | lazy val scala213 = "2.13.1"
2 | lazy val scala212 = "2.12.10"
3 | lazy val scala211 = "2.11.12"
4 | lazy val supportedScalaVersions = List(scala213, scala212, scala211)
5 |
6 | name := """akka-http-hal"""
7 |
8 | version := "1.2.5"
9 |
10 | organization := "io.pileworx"
11 |
12 | scalaVersion := scala212
13 |
14 | scalacOptions := Seq("-feature", "-deprecation", "-encoding", "utf8")
15 |
16 | publishTo := Some(
17 | if (isSnapshot.value)
18 | Opts.resolver.sonatypeSnapshots
19 | else
20 | Opts.resolver.sonatypeStaging
21 | )
22 |
23 | credentials += Credentials(
24 | "Sonatype Nexus Repository Manager",
25 | "oss.sonatype.org",
26 | sys.env.getOrElse("SONATYPE_USER", ""),
27 | sys.env.getOrElse("SONATYPE_PASSWORD", ""))
28 |
29 | crossScalaVersions := supportedScalaVersions
30 |
31 | libraryDependencies ++= {
32 | val akkaV = "2.5.23"
33 | val akkaHttpV = "10.1.8"
34 | val scalatestV = "3.0.8"
35 |
36 | Seq(
37 | "com.typesafe.akka" %% "akka-http-core" % akkaHttpV,
38 | "com.typesafe.akka" %% "akka-stream" % akkaV,
39 | "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpV,
40 |
41 | "com.typesafe.akka" %% "akka-testkit" % akkaV % "test",
42 | "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV % "test",
43 | "org.scalatest" %% "scalatest" % scalatestV % "test"
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.3.2
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0")
2 | addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "2.3")
3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.5")
4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1")
--------------------------------------------------------------------------------
/sonatype.sbt:
--------------------------------------------------------------------------------
1 | publishMavenStyle := true
2 |
3 | licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0"))
4 |
5 | homepage := Some(url("https://pileworx.io"))
6 |
7 | scmInfo := Some(
8 | ScmInfo(
9 | url("https://github.com/pileworx/akka-http-hal"),
10 | "git@github.com/pileworx/akka-http-hal.git"))
11 |
12 | developers := List(
13 | Developer(
14 | "marcuslange",
15 | "Marcus Lange",
16 | "marcus.lange@gmail.com",
17 | url("https://github.com/marcuslange")))
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/MIT-LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Mike Kelly, http://stateless.co/
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/README.adoc:
--------------------------------------------------------------------------------
1 | = HAL-browser
2 |
3 | An API browser for the hal+json media type
4 |
5 | == Example Usage
6 |
7 | Here is an example of a hal+json API using the browser:
8 |
9 | http://haltalk.herokuapp.com/explorer/browser.html[http://haltalk.herokuapp.com/explorer/browser.html]
10 |
11 | == About HAL
12 |
13 | HAL is a format based on json that establishes conventions for
14 | representing links. For example:
15 |
16 | [source,javascript]
17 | ----
18 | {
19 | "_links": {
20 | "self": { "href": "/orders" },
21 | "next": { "href": "/orders?page=2" }
22 | }
23 | }
24 | ----
25 |
26 | More detail about HAL can be found at
27 | http://stateless.co/hal_specification.html[http://stateless.co/hal_specification.html].
28 |
29 | == Customizing the POST form
30 |
31 | By default, the HAL Browser can't assume there is any metadata. When you click on the non-GET request button (to create a new resource), the user must enter the JSON document to submit. If your service includes metadata you can access, it's possible to plugin a custom view that makes use of it.
32 |
33 | . Define your custom view.
34 | +
35 | Here is an example that leverages Spring Data REST's JSON Schema metadata found at */{entity}/schema*.
36 | +
37 | [source,javascript]
38 | ----
39 | var CustomPostForm = Backbone.View.extend({
40 | initialize: function (opts) {
41 | this.href = opts.href.split('{')[0];
42 | this.vent = opts.vent;
43 | _.bindAll(this, 'createNewResource');
44 | },
45 |
46 | events: {
47 | 'submit form': 'createNewResource'
48 | },
49 |
50 | className: 'modal fade',
51 |
52 | createNewResource: function (e) {
53 | e.preventDefault();
54 |
55 | var self = this;
56 |
57 | var data = {}
58 | Object.keys(this.schema.properties).forEach(function(property) {
59 | if (!("format" in self.schema.properties[property])) {
60 | data[property] = self.$('input[name=' + property + ']').val();
61 | }
62 | });
63 |
64 | var opts = {
65 | url: this.$('.url').val(),
66 | headers: HAL.parseHeaders(this.$('.headers').val()),
67 | method: this.$('.method').val(),
68 | data: JSON.stringify(data)
69 | };
70 |
71 | var request = HAL.client.request(opts);
72 | request.done(function (response) {
73 | self.vent.trigger('response', {resource: response, jqxhr: jqxhr});
74 | }).fail(function (response) {
75 | self.vent.trigger('fail-response', {jqxhr: jqxhr});
76 | }).always(function () {
77 | self.vent.trigger('response-headers', {jqxhr: jqxhr});
78 | window.location.hash = 'NON-GET:' + opts.url;
79 | });
80 |
81 | this.$el.modal('hide');
82 | },
83 |
84 | render: function (opts) {
85 | var headers = HAL.client.getHeaders();
86 | var headersString = '';
87 |
88 | _.each(headers, function (value, name) {
89 | headersString += name + ': ' + value + '\n';
90 | });
91 |
92 | var request = HAL.client.request({
93 | url: this.href + '/schema',
94 | method: 'GET'
95 | });
96 |
97 | var self = this;
98 | request.done(function (schema) {
99 | self.schema = schema;
100 | self.$el.html(self.template({
101 | href: self.href,
102 | schema: self.schema,
103 | user_defined_headers: headersString}));
104 | self.$el.modal();
105 | });
106 |
107 | return this;
108 | },
109 | template: _.template($('#dynamic-request-template').html())
110 | });
111 | ----
112 | +
113 | . Register it by assigning to `HAL.customPostForm`
114 | +
115 | [source,javascript]
116 | ----
117 | HAL.customPostForm = CustomPostForm;
118 | ----
119 | +
120 | . Load your custom JavaScript component and define your custom HTML template.
121 | +
122 | [source,html,indent=0]
123 | ----
124 |
152 | ----
153 |
154 | NOTE: To load a custom JavaScript module AND a custom HTML template, you will probably need to create a customized version of `browser.html`.
155 |
156 | NOTE: The HAL Browser uses a global `HAL` object, so there is no need to deal with JavaScript packages.
157 |
158 | == Usage Instructions
159 |
160 | All you should need to do is copy the files into your webroot.
161 | It is OK to put it in a subdirectory; it does not need to be in the root.
162 |
163 | All the JS and CSS dependencies come included in the vendor directory.
164 |
165 | == TODO
166 |
167 | * Provide feedback to user when there are issues with response (missing
168 | self link, wrong media type identifier)
169 | * Give 'self' and 'curies' links special treatment
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The HAL Browser
5 |
6 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
34 |
35 |
36 |
37 |
46 |
47 |
111 |
112 |
116 |
117 |
121 |
122 |
137 |
138 |
142 |
143 |
164 |
165 |
166 |
196 |
197 |
200 |
201 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
256 |
257 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var urlRegex = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
3 |
4 | function isCurie(string) {
5 | return string.split(':').length > 1;
6 | };
7 |
8 | var HAL = {
9 | Models: {},
10 | Views: {},
11 | Http: {},
12 | currentDocument: {},
13 | jsonIndent: 2,
14 | isUrl: function(str) {
15 | return str.match(urlRegex) || isCurie(str);
16 | },
17 | isFollowableHeader: function(headerName) {
18 | return headerName === 'Location' || headerName === 'Content-Location';
19 | },
20 | truncateIfUrl: function(str) {
21 | var replaceRegex = /(http|https):\/\/([^\/]*)\//;
22 | return str.replace(replaceRegex, '.../');
23 | },
24 | normalizeUrl: function(rel) {
25 | var cur = location.hash.slice(1);
26 | var uri = new URI(rel)
27 | var norm = uri.absoluteTo(cur);
28 |
29 | return norm
30 | },
31 | buildUrl: function(rel) {
32 | if (!HAL.currentDocument._links) {
33 | return rel;
34 | }
35 | if (!rel.match(urlRegex) && isCurie(rel) && HAL.currentDocument._links.curies) {
36 | var parts = rel.split(':');
37 | var curies = HAL.currentDocument._links.curies;
38 | for (var i=0; i 1) {
60 | var name = parts.shift().trim();
61 | var value = parts.join(':').trim();
62 | headers[name] = value;
63 | }
64 | });
65 | return headers;
66 | },
67 | customPostForm: undefined
68 | };
69 |
70 | window.HAL = HAL;
71 | })();
72 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/browser.js:
--------------------------------------------------------------------------------
1 | HAL.Browser = Backbone.Router.extend({
2 | initialize: function(opts) {
3 | opts = opts || {};
4 |
5 | var vent = _.extend({}, Backbone.Events),
6 | $container = opts.container || $('#browser');
7 |
8 | this.entryPoint = opts.entryPoint || '/';
9 |
10 | // TODO: don't hang currentDoc off namespace
11 | vent.bind('response', function(e) {
12 | window.HAL.currentDocument = e.resource || {};
13 | });
14 |
15 | vent.bind('location-go', _.bind(this.loadUrl, this));
16 |
17 | HAL.client = new HAL.Http.Client({ vent: vent });
18 |
19 | var browser = new HAL.Views.Browser({ vent: vent, entryPoint: this.entryPoint });
20 | browser.render()
21 |
22 | $container.html(browser.el);
23 | vent.trigger('app:loaded');
24 |
25 | if (window.location.hash === '') {
26 | window.location.hash = this.entryPoint;
27 | }
28 |
29 | if(location.hash.slice(1,9) === 'NON-GET:') {
30 | new HAL.Views.NonSafeRequestDialog({
31 | href: location.hash.slice(9),
32 | vent: vent
33 | }).render({});
34 | }
35 | },
36 |
37 | routes: {
38 | '*url': 'resourceRoute'
39 | },
40 |
41 | loadUrl: function(url) {
42 | if (this.getHash() === url) {
43 | HAL.client.get(url);
44 | } else {
45 | window.location.hash = url;
46 | }
47 | },
48 |
49 | getHash: function() {
50 | return window.location.hash.slice(1);
51 | },
52 |
53 | resourceRoute: function() {
54 | url = location.hash.slice(1);
55 | console.log('target url changed to: ' + url);
56 | if (url.slice(0,8) !== 'NON-GET:') {
57 | HAL.client.get(url);
58 | }
59 | }
60 | });
61 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/http/client.js:
--------------------------------------------------------------------------------
1 | HAL.Http.Client = function(opts) {
2 | this.vent = opts.vent;
3 | this.defaultHeaders = { 'Accept': 'application/hal+json, application/json, */*; q=0.01' };
4 | this.headers = this.defaultHeaders;
5 | };
6 |
7 | HAL.Http.Client.prototype.get = function(url) {
8 | var self = this;
9 | this.vent.trigger('location-change', { url: url });
10 | var jqxhr = $.ajax({
11 | url: url,
12 | dataType: 'json',
13 | xhrFields: {
14 | withCredentials: true
15 | },
16 | headers: this.headers,
17 | success: function(resource, textStatus, jqXHR) {
18 | self.vent.trigger('response', {
19 | resource: resource,
20 | jqxhr: jqXHR,
21 | headers: jqXHR.getAllResponseHeaders()
22 | });
23 | }
24 | }).error(function() {
25 | self.vent.trigger('fail-response', { jqxhr: jqxhr });
26 | });
27 | };
28 |
29 | HAL.Http.Client.prototype.request = function(opts) {
30 | var self = this;
31 | opts.dataType = 'json';
32 | opts.xhrFields = opts.xhrFields || {};
33 | opts.xhrFields.withCredentials = opts.xhrFields.withCredentials || true;
34 | self.vent.trigger('location-change', { url: opts.url });
35 | return jqxhr = $.ajax(opts);
36 | };
37 |
38 | HAL.Http.Client.prototype.updateHeaders = function(headers) {
39 | this.headers = headers;
40 | };
41 |
42 | HAL.Http.Client.prototype.getHeaders = function() {
43 | return this.headers;
44 | };
45 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/resource.js:
--------------------------------------------------------------------------------
1 | HAL.Models.Resource = Backbone.Model.extend({
2 | initialize: function(representation) {
3 | representation = representation || {};
4 | this.links = representation._links;
5 | this.title = representation.title;
6 | if(representation._embedded !== undefined) {
7 | this.embeddedResources = this.buildEmbeddedResources(representation._embedded);
8 | }
9 | this.set(representation);
10 | this.unset('_embedded', { silent: true });
11 | this.unset('_links', { silent: true });
12 | },
13 |
14 | buildEmbeddedResources: function(embeddedResources) {
15 | var result = {};
16 | _.each(embeddedResources, function(obj, rel) {
17 | if($.isArray(obj)) {
18 | var arr = [];
19 | _.each(obj, function(resource, i) {
20 | var newResource = new HAL.Models.Resource(resource);
21 | newResource.identifier = rel + '[' + i + ']';
22 | newResource.embed_rel = rel;
23 | arr.push(newResource);
24 | });
25 | result[rel] = arr;
26 | } else {
27 | var newResource = new HAL.Models.Resource(obj);
28 | newResource.identifier = rel;
29 | newResource.embed_rel = rel;
30 | result[rel] = newResource;
31 | }
32 | });
33 | return result;
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/browser.js:
--------------------------------------------------------------------------------
1 | HAL.Views.Browser = Backbone.View.extend({
2 | initialize: function(opts) {
3 | var self = this;
4 | this.vent = opts.vent;
5 | this.entryPoint = opts.entryPoint;
6 | this.explorerView = new HAL.Views.Explorer({ vent: this.vent });
7 | this.inspectorView = new HAL.Views.Inspector({ vent: this.vent });
8 | },
9 |
10 | className: 'hal-browser row-fluid',
11 |
12 | render: function() {
13 | this.$el.empty();
14 |
15 | this.inspectorView.render();
16 | this.explorerView.render();
17 |
18 | this.$el.html(this.explorerView.el);
19 | this.$el.append(this.inspectorView.el);
20 |
21 | var entryPoint = this.entryPoint;
22 |
23 | $("#entryPointLink").click(function(event) {
24 | event.preventDefault();
25 | window.location.hash = entryPoint;
26 | });
27 | return this;
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/documentation.js:
--------------------------------------------------------------------------------
1 | HAL.Views.Documenation = Backbone.View.extend({
2 | className: 'documentation',
3 |
4 | render: function(url) {
5 | this.$el.html('');
6 | }
7 | });
8 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/embedded_resource.js:
--------------------------------------------------------------------------------
1 | HAL.Views.EmbeddedResource = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.vent = opts.vent;
4 | this.resource = opts.resource;
5 |
6 | this.propertiesView = new HAL.Views.Properties({});
7 | this.linksView = new HAL.Views.Links({
8 | vent: this.vent
9 | });
10 |
11 | _.bindAll(this, 'onToggleClick');
12 | _.bindAll(this, 'onDoxClick');
13 | },
14 |
15 | events: {
16 | 'click a.accordion-toggle': 'onToggleClick',
17 | 'click span.dox': 'onDoxClick'
18 | },
19 |
20 | className: 'embedded-resource accordion-group',
21 |
22 | onToggleClick: function(e) {
23 | e.preventDefault();
24 | this.$accordionBody.collapse('toggle');
25 | },
26 |
27 | onDoxClick: function(e) {
28 | e.preventDefault();
29 | this.vent.trigger('show-docs', {
30 | url: $(e.currentTarget).data('href')
31 | });
32 | return false;
33 | },
34 |
35 | render: function() {
36 | this.$el.empty();
37 |
38 | this.propertiesView.render(this.resource.toJSON());
39 | this.linksView.render(this.resource.links);
40 |
41 | this.$el.append(this.template({
42 | resource: this.resource
43 | }));
44 |
45 | var $inner = $('');
46 | $inner.append(this.propertiesView.el);
47 | $inner.append(this.linksView.el);
48 |
49 | this.$accordionBody = $('');
50 | this.$accordionBody.append($inner)
51 |
52 | this.$el.append(this.$accordionBody);
53 | },
54 |
55 | template: _.template($('#embedded-resource-template').html())
56 | });
57 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/embedded_resources.js:
--------------------------------------------------------------------------------
1 | HAL.Views.EmbeddedResources = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.vent = opts.vent;
4 | _.bindAll(this, 'render');
5 | },
6 |
7 | className: 'embedded-resources accordion',
8 |
9 | render: function(resources) {
10 | var self = this,
11 | resourceViews = [],
12 | buildView = function(resource) {
13 | return new HAL.Views.EmbeddedResource({
14 | resource: resource,
15 | vent: self.vent
16 | });
17 | };
18 |
19 | _.each(resources, function(prop) {
20 | if ($.isArray(prop)) {
21 | _.each(prop, function(resource) {
22 | resourceViews.push(buildView(resource));
23 | });
24 | } else {
25 | resourceViews.push(buildView(prop));
26 | }
27 | });
28 |
29 | this.$el.html(this.template());
30 |
31 | _.each(resourceViews, function(view) {
32 | view.render();
33 | self.$el.append(view.el);
34 | });
35 |
36 |
37 | return this;
38 | },
39 |
40 | template: _.template($('#embedded-resources-template').html())
41 | });
42 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/explorer.js:
--------------------------------------------------------------------------------
1 | HAL.Views.Explorer = Backbone.View.extend({
2 | initialize: function(opts) {
3 | var self = this;
4 | this.vent = opts.vent;
5 | this.navigationView = new HAL.Views.Navigation({ vent: this.vent });
6 | this.resourceView = new HAL.Views.Resource({ vent: this.vent });
7 | },
8 |
9 | className: 'explorer span6',
10 |
11 | render: function() {
12 | this.navigationView.render();
13 |
14 | this.$el.html(this.template());
15 |
16 | this.$el.append(this.navigationView.el);
17 | this.$el.append(this.resourceView.el);
18 | },
19 |
20 | template: function() {
21 | return 'Explorer
';
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/inspector.js:
--------------------------------------------------------------------------------
1 | HAL.Views.Inspector = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.vent = opts.vent;
4 |
5 | _.bindAll(this, 'renderDocumentation');
6 | _.bindAll(this, 'renderResponse');
7 |
8 | this.vent.bind('show-docs', this.renderDocumentation);
9 | this.vent.bind('response', this.renderResponse);
10 | },
11 |
12 | className: 'inspector span6',
13 |
14 | render: function() {
15 | this.$el.html(this.template());
16 | },
17 |
18 | renderResponse: function(response) {
19 | var responseView = new HAL.Views.Response({ vent: this.vent });
20 |
21 | this.render();
22 | responseView.render(response);
23 |
24 | this.$el.append(responseView.el);
25 | },
26 |
27 | renderDocumentation: function(e) {
28 | var docView = new HAL.Views.Documenation({ vent: this.vent });
29 |
30 | this.render();
31 | docView.render(e.url);
32 |
33 | this.$el.append(docView.el);
34 | },
35 |
36 | template: function() {
37 | return 'Inspector
';
38 | }
39 | });
40 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/links.js:
--------------------------------------------------------------------------------
1 | HAL.Views.Links = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.vent = opts.vent;
4 | },
5 |
6 | events: {
7 | 'click .follow': 'followLink',
8 | 'click .non-get': 'showNonSafeRequestDialog',
9 | 'click .query': 'showUriQueryDialog',
10 | 'click .dox': 'showDocs'
11 | },
12 |
13 | className: 'links',
14 |
15 | followLink: function(e) {
16 | e.preventDefault();
17 | var $target = $(e.currentTarget);
18 | var uri = $target.attr('href');
19 | window.location.hash = uri;
20 | },
21 |
22 | showUriQueryDialog: function(e) {
23 | e.preventDefault();
24 |
25 | var $target = $(e.currentTarget);
26 | var uri = $target.attr('href');
27 |
28 | new HAL.Views.QueryUriDialog({
29 | href: uri
30 | }).render({});
31 | },
32 |
33 | showNonSafeRequestDialog: function(e) {
34 | e.preventDefault();
35 |
36 | var postForm = (HAL.customPostForm !== undefined) ? HAL.customPostForm : HAL.Views.NonSafeRequestDialog;
37 | var d = new postForm({
38 | href: $(e.currentTarget).attr('href'),
39 | vent: this.vent
40 | }).render({})
41 | },
42 |
43 | showDocs: function(e) {
44 | e.preventDefault();
45 | var $target = $(e.target);
46 | var uri = $target.attr('href') || $target.parent().attr('href');
47 | this.vent.trigger('show-docs', { url: uri });
48 | },
49 |
50 | template: _.template($('#links-template').html()),
51 |
52 | render: function(links) {
53 | this.$el.html(this.template({ links: links }));
54 | }
55 | });
56 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/location_bar.js:
--------------------------------------------------------------------------------
1 | HAL.Views.LocationBar = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.vent = opts.vent;
4 | _.bindAll(this, 'render');
5 | _.bindAll(this, 'onButtonClick');
6 | this.vent.bind('location-change', this.render);
7 | this.vent.bind('location-change', _.bind(this.showSpinner, this));
8 | this.vent.bind('response', _.bind(this.hideSpinner, this));
9 | },
10 |
11 | events: {
12 | 'submit form': 'onButtonClick'
13 | },
14 |
15 | className: 'address',
16 |
17 | render: function(e) {
18 | e = e || { url: '' };
19 | this.$el.html(this.template(e));
20 | },
21 |
22 | onButtonClick: function(e) {
23 | e.preventDefault();
24 | this.vent.trigger('location-go', this.getLocation());
25 | },
26 |
27 | getLocation: function() {
28 | return this.$el.find('input').val();
29 | },
30 |
31 | showSpinner: function() {
32 | this.$el.find('.ajax-loader').addClass('visible');
33 | },
34 |
35 | hideSpinner: function() {
36 | this.$el.find('.ajax-loader').removeClass('visible');
37 | },
38 |
39 | template: _.template($('#location-bar-template').html())
40 | });
41 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/navigation.js:
--------------------------------------------------------------------------------
1 | HAL.Views.Navigation = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.vent = opts.vent;
4 | this.locationBar = new HAL.Views.LocationBar({ vent: this.vent });
5 | this.requestHeadersView = new HAL.Views.RequestHeaders({ vent: this.vent });
6 | },
7 |
8 | className: 'navigation',
9 |
10 | render: function() {
11 | this.$el.empty();
12 |
13 | this.locationBar.render();
14 | this.requestHeadersView.render();
15 |
16 | this.$el.append(this.locationBar.el);
17 | this.$el.append(this.requestHeadersView.el);
18 | }
19 | });
20 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/non_safe_request_dialog.js:
--------------------------------------------------------------------------------
1 | HAL.Views.NonSafeRequestDialog = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.href = opts.href;
4 | this.vent = opts.vent;
5 | this.uriTemplate = uritemplate(this.href);
6 | _.bindAll(this, 'submitQuery');
7 | },
8 |
9 | events: {
10 | 'submit form': 'submitQuery'
11 | },
12 |
13 | className: 'modal fade',
14 |
15 | submitQuery: function(e) {
16 | e.preventDefault();
17 |
18 | var self = this,
19 | opts = {
20 | url: this.$('.url').val(),
21 | headers: HAL.parseHeaders(this.$('.headers').val()),
22 | method: this.$('.method').val(),
23 | data: this.$('.body').val()
24 | };
25 |
26 | var request = HAL.client.request(opts);
27 | request.done(function(response) {
28 | self.vent.trigger('response', { resource: response, jqxhr: jqxhr });
29 | }).fail(function(response) {
30 | self.vent.trigger('fail-response', { jqxhr: jqxhr });
31 | }).always(function() {
32 | self.vent.trigger('response-headers', { jqxhr: jqxhr });
33 | window.location.hash = 'NON-GET:' + opts.url;
34 | });
35 |
36 | this.$el.modal('hide');
37 | },
38 |
39 | render: function(opts) {
40 | var headers = HAL.client.getHeaders(),
41 | headersString = '';
42 |
43 | _.each(headers, function(value, name) {
44 | headersString += name + ': ' + value + '\n';
45 | });
46 |
47 | this.$el.html(this.template({ href: this.href, user_defined_headers: headersString }));
48 | this.$el.modal();
49 | return this;
50 | },
51 |
52 | template: _.template($('#non-safe-request-template').html())
53 | });
54 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/properties.js:
--------------------------------------------------------------------------------
1 | HAL.Views.Properties = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.vent = opts.vent;
4 | _.bindAll(this, 'render');
5 | },
6 |
7 | className: 'properties',
8 |
9 | render: function(props) {
10 | this.$el.html(this.template({ properties: props }));
11 | },
12 |
13 | template: _.template($('#properties-template').html())
14 | });
15 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/js/hal/views/query_uri_dialog.js:
--------------------------------------------------------------------------------
1 | HAL.Views.QueryUriDialog = Backbone.View.extend({
2 | initialize: function(opts) {
3 | this.href = opts.href;
4 | this.uriTemplate = uritemplate(this.href);
5 | _.bindAll(this, 'submitQuery');
6 | _.bindAll(this, 'renderPreview');
7 | },
8 |
9 | className: 'modal fade',
10 |
11 | events: {
12 | 'submit form': 'submitQuery',
13 | 'keyup textarea': 'renderPreview',
14 | 'change textarea': 'renderPreview'
15 | },
16 |
17 | submitQuery: function(e) {
18 | e.preventDefault();
19 | var input;
20 | try {
21 | input = JSON.parse(this.$('textarea').val());
22 | } catch(err) {
23 | input = {};
24 | }
25 | this.$el.modal('hide');
26 | window.location.hash = this.uriTemplate.expand(this.cleanInput(input));
27 | },
28 |
29 | renderPreview: function(e) {
30 | var input, result;
31 | try {
32 | input = JSON.parse($(e.target).val());
33 | result = this.uriTemplate.expand(this.cleanInput(input));
34 | } catch (err) {
35 | result = 'Invalid json input';
36 | }
37 | this.$('.preview').text(result);
38 | },
39 |
40 | extractExpressionNames: function (template) {
41 | var names = [];
42 | for (var i=0; i li {
454 | margin-left: 30px;
455 | }
456 | .row-fluid .thumbnails {
457 | margin-left: 0;
458 | }
459 | }
460 |
461 | @media (min-width: 768px) and (max-width: 979px) {
462 | .row {
463 | margin-left: -20px;
464 | *zoom: 1;
465 | }
466 | .row:before,
467 | .row:after {
468 | display: table;
469 | line-height: 0;
470 | content: "";
471 | }
472 | .row:after {
473 | clear: both;
474 | }
475 | [class*="span"] {
476 | float: left;
477 | min-height: 1px;
478 | margin-left: 20px;
479 | }
480 | .container,
481 | .navbar-static-top .container,
482 | .navbar-fixed-top .container,
483 | .navbar-fixed-bottom .container {
484 | width: 724px;
485 | }
486 | .span12 {
487 | width: 724px;
488 | }
489 | .span11 {
490 | width: 662px;
491 | }
492 | .span10 {
493 | width: 600px;
494 | }
495 | .span9 {
496 | width: 538px;
497 | }
498 | .span8 {
499 | width: 476px;
500 | }
501 | .span7 {
502 | width: 414px;
503 | }
504 | .span6 {
505 | width: 352px;
506 | }
507 | .span5 {
508 | width: 290px;
509 | }
510 | .span4 {
511 | width: 228px;
512 | }
513 | .span3 {
514 | width: 166px;
515 | }
516 | .span2 {
517 | width: 104px;
518 | }
519 | .span1 {
520 | width: 42px;
521 | }
522 | .offset12 {
523 | margin-left: 764px;
524 | }
525 | .offset11 {
526 | margin-left: 702px;
527 | }
528 | .offset10 {
529 | margin-left: 640px;
530 | }
531 | .offset9 {
532 | margin-left: 578px;
533 | }
534 | .offset8 {
535 | margin-left: 516px;
536 | }
537 | .offset7 {
538 | margin-left: 454px;
539 | }
540 | .offset6 {
541 | margin-left: 392px;
542 | }
543 | .offset5 {
544 | margin-left: 330px;
545 | }
546 | .offset4 {
547 | margin-left: 268px;
548 | }
549 | .offset3 {
550 | margin-left: 206px;
551 | }
552 | .offset2 {
553 | margin-left: 144px;
554 | }
555 | .offset1 {
556 | margin-left: 82px;
557 | }
558 | .row-fluid {
559 | width: 100%;
560 | *zoom: 1;
561 | }
562 | .row-fluid:before,
563 | .row-fluid:after {
564 | display: table;
565 | line-height: 0;
566 | content: "";
567 | }
568 | .row-fluid:after {
569 | clear: both;
570 | }
571 | .row-fluid [class*="span"] {
572 | display: block;
573 | float: left;
574 | width: 100%;
575 | min-height: 30px;
576 | margin-left: 2.7624309392265194%;
577 | *margin-left: 2.709239449864817%;
578 | -webkit-box-sizing: border-box;
579 | -moz-box-sizing: border-box;
580 | box-sizing: border-box;
581 | }
582 | .row-fluid [class*="span"]:first-child {
583 | margin-left: 0;
584 | }
585 | .row-fluid .controls-row [class*="span"] + [class*="span"] {
586 | margin-left: 2.7624309392265194%;
587 | }
588 | .row-fluid .span12 {
589 | width: 100%;
590 | *width: 99.94680851063829%;
591 | }
592 | .row-fluid .span11 {
593 | width: 91.43646408839778%;
594 | *width: 91.38327259903608%;
595 | }
596 | .row-fluid .span10 {
597 | width: 82.87292817679558%;
598 | *width: 82.81973668743387%;
599 | }
600 | .row-fluid .span9 {
601 | width: 74.30939226519337%;
602 | *width: 74.25620077583166%;
603 | }
604 | .row-fluid .span8 {
605 | width: 65.74585635359117%;
606 | *width: 65.69266486422946%;
607 | }
608 | .row-fluid .span7 {
609 | width: 57.18232044198895%;
610 | *width: 57.12912895262725%;
611 | }
612 | .row-fluid .span6 {
613 | width: 48.61878453038674%;
614 | *width: 48.56559304102504%;
615 | }
616 | .row-fluid .span5 {
617 | width: 40.05524861878453%;
618 | *width: 40.00205712942283%;
619 | }
620 | .row-fluid .span4 {
621 | width: 31.491712707182323%;
622 | *width: 31.43852121782062%;
623 | }
624 | .row-fluid .span3 {
625 | width: 22.92817679558011%;
626 | *width: 22.87498530621841%;
627 | }
628 | .row-fluid .span2 {
629 | width: 14.3646408839779%;
630 | *width: 14.311449394616199%;
631 | }
632 | .row-fluid .span1 {
633 | width: 5.801104972375691%;
634 | *width: 5.747913483013988%;
635 | }
636 | .row-fluid .offset12 {
637 | margin-left: 105.52486187845304%;
638 | *margin-left: 105.41847889972962%;
639 | }
640 | .row-fluid .offset12:first-child {
641 | margin-left: 102.76243093922652%;
642 | *margin-left: 102.6560479605031%;
643 | }
644 | .row-fluid .offset11 {
645 | margin-left: 96.96132596685082%;
646 | *margin-left: 96.8549429881274%;
647 | }
648 | .row-fluid .offset11:first-child {
649 | margin-left: 94.1988950276243%;
650 | *margin-left: 94.09251204890089%;
651 | }
652 | .row-fluid .offset10 {
653 | margin-left: 88.39779005524862%;
654 | *margin-left: 88.2914070765252%;
655 | }
656 | .row-fluid .offset10:first-child {
657 | margin-left: 85.6353591160221%;
658 | *margin-left: 85.52897613729868%;
659 | }
660 | .row-fluid .offset9 {
661 | margin-left: 79.8342541436464%;
662 | *margin-left: 79.72787116492299%;
663 | }
664 | .row-fluid .offset9:first-child {
665 | margin-left: 77.07182320441989%;
666 | *margin-left: 76.96544022569647%;
667 | }
668 | .row-fluid .offset8 {
669 | margin-left: 71.2707182320442%;
670 | *margin-left: 71.16433525332079%;
671 | }
672 | .row-fluid .offset8:first-child {
673 | margin-left: 68.50828729281768%;
674 | *margin-left: 68.40190431409427%;
675 | }
676 | .row-fluid .offset7 {
677 | margin-left: 62.70718232044199%;
678 | *margin-left: 62.600799341718584%;
679 | }
680 | .row-fluid .offset7:first-child {
681 | margin-left: 59.94475138121547%;
682 | *margin-left: 59.838368402492065%;
683 | }
684 | .row-fluid .offset6 {
685 | margin-left: 54.14364640883978%;
686 | *margin-left: 54.037263430116376%;
687 | }
688 | .row-fluid .offset6:first-child {
689 | margin-left: 51.38121546961326%;
690 | *margin-left: 51.27483249088986%;
691 | }
692 | .row-fluid .offset5 {
693 | margin-left: 45.58011049723757%;
694 | *margin-left: 45.47372751851417%;
695 | }
696 | .row-fluid .offset5:first-child {
697 | margin-left: 42.81767955801105%;
698 | *margin-left: 42.71129657928765%;
699 | }
700 | .row-fluid .offset4 {
701 | margin-left: 37.01657458563536%;
702 | *margin-left: 36.91019160691196%;
703 | }
704 | .row-fluid .offset4:first-child {
705 | margin-left: 34.25414364640884%;
706 | *margin-left: 34.14776066768544%;
707 | }
708 | .row-fluid .offset3 {
709 | margin-left: 28.45303867403315%;
710 | *margin-left: 28.346655695309746%;
711 | }
712 | .row-fluid .offset3:first-child {
713 | margin-left: 25.69060773480663%;
714 | *margin-left: 25.584224756083227%;
715 | }
716 | .row-fluid .offset2 {
717 | margin-left: 19.88950276243094%;
718 | *margin-left: 19.783119783707537%;
719 | }
720 | .row-fluid .offset2:first-child {
721 | margin-left: 17.12707182320442%;
722 | *margin-left: 17.02068884448102%;
723 | }
724 | .row-fluid .offset1 {
725 | margin-left: 11.32596685082873%;
726 | *margin-left: 11.219583872105325%;
727 | }
728 | .row-fluid .offset1:first-child {
729 | margin-left: 8.56353591160221%;
730 | *margin-left: 8.457152932878806%;
731 | }
732 | input,
733 | textarea,
734 | .uneditable-input {
735 | margin-left: 0;
736 | }
737 | .controls-row [class*="span"] + [class*="span"] {
738 | margin-left: 20px;
739 | }
740 | input.span12,
741 | textarea.span12,
742 | .uneditable-input.span12 {
743 | width: 710px;
744 | }
745 | input.span11,
746 | textarea.span11,
747 | .uneditable-input.span11 {
748 | width: 648px;
749 | }
750 | input.span10,
751 | textarea.span10,
752 | .uneditable-input.span10 {
753 | width: 586px;
754 | }
755 | input.span9,
756 | textarea.span9,
757 | .uneditable-input.span9 {
758 | width: 524px;
759 | }
760 | input.span8,
761 | textarea.span8,
762 | .uneditable-input.span8 {
763 | width: 462px;
764 | }
765 | input.span7,
766 | textarea.span7,
767 | .uneditable-input.span7 {
768 | width: 400px;
769 | }
770 | input.span6,
771 | textarea.span6,
772 | .uneditable-input.span6 {
773 | width: 338px;
774 | }
775 | input.span5,
776 | textarea.span5,
777 | .uneditable-input.span5 {
778 | width: 276px;
779 | }
780 | input.span4,
781 | textarea.span4,
782 | .uneditable-input.span4 {
783 | width: 214px;
784 | }
785 | input.span3,
786 | textarea.span3,
787 | .uneditable-input.span3 {
788 | width: 152px;
789 | }
790 | input.span2,
791 | textarea.span2,
792 | .uneditable-input.span2 {
793 | width: 90px;
794 | }
795 | input.span1,
796 | textarea.span1,
797 | .uneditable-input.span1 {
798 | width: 28px;
799 | }
800 | }
801 |
802 | @media (max-width: 767px) {
803 | body {
804 | padding-right: 20px;
805 | padding-left: 20px;
806 | }
807 | .navbar-fixed-top,
808 | .navbar-fixed-bottom,
809 | .navbar-static-top {
810 | margin-right: -20px;
811 | margin-left: -20px;
812 | }
813 | .container-fluid {
814 | padding: 0;
815 | }
816 | .dl-horizontal dt {
817 | float: none;
818 | width: auto;
819 | clear: none;
820 | text-align: left;
821 | }
822 | .dl-horizontal dd {
823 | margin-left: 0;
824 | }
825 | .container {
826 | width: auto;
827 | }
828 | .row-fluid {
829 | width: 100%;
830 | }
831 | .row,
832 | .thumbnails {
833 | margin-left: 0;
834 | }
835 | .thumbnails > li {
836 | float: none;
837 | margin-left: 0;
838 | }
839 | [class*="span"],
840 | .uneditable-input[class*="span"],
841 | .row-fluid [class*="span"] {
842 | display: block;
843 | float: none;
844 | width: 100%;
845 | margin-left: 0;
846 | -webkit-box-sizing: border-box;
847 | -moz-box-sizing: border-box;
848 | box-sizing: border-box;
849 | }
850 | .span12,
851 | .row-fluid .span12 {
852 | width: 100%;
853 | -webkit-box-sizing: border-box;
854 | -moz-box-sizing: border-box;
855 | box-sizing: border-box;
856 | }
857 | .row-fluid [class*="offset"]:first-child {
858 | margin-left: 0;
859 | }
860 | .input-large,
861 | .input-xlarge,
862 | .input-xxlarge,
863 | input[class*="span"],
864 | select[class*="span"],
865 | textarea[class*="span"],
866 | .uneditable-input {
867 | display: block;
868 | width: 100%;
869 | min-height: 30px;
870 | -webkit-box-sizing: border-box;
871 | -moz-box-sizing: border-box;
872 | box-sizing: border-box;
873 | }
874 | .input-prepend input,
875 | .input-append input,
876 | .input-prepend input[class*="span"],
877 | .input-append input[class*="span"] {
878 | display: inline-block;
879 | width: auto;
880 | }
881 | .controls-row [class*="span"] + [class*="span"] {
882 | margin-left: 0;
883 | }
884 | .modal {
885 | position: fixed;
886 | top: 20px;
887 | right: 20px;
888 | left: 20px;
889 | width: auto;
890 | margin: 0;
891 | }
892 | .modal.fade {
893 | top: -100px;
894 | }
895 | .modal.fade.in {
896 | top: 20px;
897 | }
898 | }
899 |
900 | @media (max-width: 480px) {
901 | .nav-collapse {
902 | -webkit-transform: translate3d(0, 0, 0);
903 | }
904 | .page-header h1 small {
905 | display: block;
906 | line-height: 20px;
907 | }
908 | input[type="checkbox"],
909 | input[type="radio"] {
910 | border: 1px solid #ccc;
911 | }
912 | .form-horizontal .control-label {
913 | float: none;
914 | width: auto;
915 | padding-top: 0;
916 | text-align: left;
917 | }
918 | .form-horizontal .controls {
919 | margin-left: 0;
920 | }
921 | .form-horizontal .control-list {
922 | padding-top: 0;
923 | }
924 | .form-horizontal .form-actions {
925 | padding-right: 10px;
926 | padding-left: 10px;
927 | }
928 | .media .pull-left,
929 | .media .pull-right {
930 | display: block;
931 | float: none;
932 | margin-bottom: 10px;
933 | }
934 | .media-object {
935 | margin-right: 0;
936 | margin-left: 0;
937 | }
938 | .modal {
939 | top: 10px;
940 | right: 10px;
941 | left: 10px;
942 | }
943 | .modal-header .close {
944 | padding: 10px;
945 | margin: -10px;
946 | }
947 | .carousel-caption {
948 | position: static;
949 | }
950 | }
951 |
952 | @media (max-width: 979px) {
953 | body {
954 | padding-top: 0;
955 | }
956 | .navbar-fixed-top,
957 | .navbar-fixed-bottom {
958 | position: static;
959 | }
960 | .navbar-fixed-top {
961 | margin-bottom: 20px;
962 | }
963 | .navbar-fixed-bottom {
964 | margin-top: 20px;
965 | }
966 | .navbar-fixed-top .navbar-inner,
967 | .navbar-fixed-bottom .navbar-inner {
968 | padding: 5px;
969 | }
970 | .navbar .container {
971 | width: auto;
972 | padding: 0;
973 | }
974 | .navbar .brand {
975 | padding-right: 10px;
976 | padding-left: 10px;
977 | margin: 0 0 0 -5px;
978 | }
979 | .nav-collapse {
980 | clear: both;
981 | }
982 | .nav-collapse .nav {
983 | float: none;
984 | margin: 0 0 10px;
985 | }
986 | .nav-collapse .nav > li {
987 | float: none;
988 | }
989 | .nav-collapse .nav > li > a {
990 | margin-bottom: 2px;
991 | }
992 | .nav-collapse .nav > .divider-vertical {
993 | display: none;
994 | }
995 | .nav-collapse .nav .nav-header {
996 | color: #777777;
997 | text-shadow: none;
998 | }
999 | .nav-collapse .nav > li > a,
1000 | .nav-collapse .dropdown-menu a {
1001 | padding: 9px 15px;
1002 | font-weight: bold;
1003 | color: #777777;
1004 | -webkit-border-radius: 3px;
1005 | -moz-border-radius: 3px;
1006 | border-radius: 3px;
1007 | }
1008 | .nav-collapse .btn {
1009 | padding: 4px 10px 4px;
1010 | font-weight: normal;
1011 | -webkit-border-radius: 4px;
1012 | -moz-border-radius: 4px;
1013 | border-radius: 4px;
1014 | }
1015 | .nav-collapse .dropdown-menu li + li a {
1016 | margin-bottom: 2px;
1017 | }
1018 | .nav-collapse .nav > li > a:hover,
1019 | .nav-collapse .nav > li > a:focus,
1020 | .nav-collapse .dropdown-menu a:hover,
1021 | .nav-collapse .dropdown-menu a:focus {
1022 | background-color: #f2f2f2;
1023 | }
1024 | .navbar-inverse .nav-collapse .nav > li > a,
1025 | .navbar-inverse .nav-collapse .dropdown-menu a {
1026 | color: #999999;
1027 | }
1028 | .navbar-inverse .nav-collapse .nav > li > a:hover,
1029 | .navbar-inverse .nav-collapse .nav > li > a:focus,
1030 | .navbar-inverse .nav-collapse .dropdown-menu a:hover,
1031 | .navbar-inverse .nav-collapse .dropdown-menu a:focus {
1032 | background-color: #111111;
1033 | }
1034 | .nav-collapse.in .btn-group {
1035 | padding: 0;
1036 | margin-top: 5px;
1037 | }
1038 | .nav-collapse .dropdown-menu {
1039 | position: static;
1040 | top: auto;
1041 | left: auto;
1042 | display: none;
1043 | float: none;
1044 | max-width: none;
1045 | padding: 0;
1046 | margin: 0 15px;
1047 | background-color: transparent;
1048 | border: none;
1049 | -webkit-border-radius: 0;
1050 | -moz-border-radius: 0;
1051 | border-radius: 0;
1052 | -webkit-box-shadow: none;
1053 | -moz-box-shadow: none;
1054 | box-shadow: none;
1055 | }
1056 | .nav-collapse .open > .dropdown-menu {
1057 | display: block;
1058 | }
1059 | .nav-collapse .dropdown-menu:before,
1060 | .nav-collapse .dropdown-menu:after {
1061 | display: none;
1062 | }
1063 | .nav-collapse .dropdown-menu .divider {
1064 | display: none;
1065 | }
1066 | .nav-collapse .nav > li > .dropdown-menu:before,
1067 | .nav-collapse .nav > li > .dropdown-menu:after {
1068 | display: none;
1069 | }
1070 | .nav-collapse .navbar-form,
1071 | .nav-collapse .navbar-search {
1072 | float: none;
1073 | padding: 10px 15px;
1074 | margin: 10px 0;
1075 | border-top: 1px solid #f2f2f2;
1076 | border-bottom: 1px solid #f2f2f2;
1077 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
1078 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
1079 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
1080 | }
1081 | .navbar-inverse .nav-collapse .navbar-form,
1082 | .navbar-inverse .nav-collapse .navbar-search {
1083 | border-top-color: #111111;
1084 | border-bottom-color: #111111;
1085 | }
1086 | .navbar .nav-collapse .nav.pull-right {
1087 | float: none;
1088 | margin-left: 0;
1089 | }
1090 | .nav-collapse,
1091 | .nav-collapse.collapse {
1092 | height: 0;
1093 | overflow: hidden;
1094 | }
1095 | .navbar .btn-navbar {
1096 | display: block;
1097 | }
1098 | .navbar-static .navbar-inner {
1099 | padding-right: 10px;
1100 | padding-left: 10px;
1101 | }
1102 | }
1103 |
1104 | @media (min-width: 980px) {
1105 | .nav-collapse.collapse {
1106 | height: auto !important;
1107 | overflow: visible !important;
1108 | }
1109 | }
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/vendor/img/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pileworx/akka-http-hal/ed5b07626fe382e48ccbff57460a89ad025775ce/src/main/resources/dependencies/halbrowser/vendor/img/ajax-loader.gif
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/vendor/img/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pileworx/akka-http-hal/ed5b07626fe382e48ccbff57460a89ad025775ce/src/main/resources/dependencies/halbrowser/vendor/img/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/vendor/img/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pileworx/akka-http-hal/ed5b07626fe382e48ccbff57460a89ad025775ce/src/main/resources/dependencies/halbrowser/vendor/img/glyphicons-halflings.png
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/vendor/js/URI.min.js:
--------------------------------------------------------------------------------
1 | /*! URI.js v1.14.1 http://medialize.github.io/URI.js/ */
2 | /* build contains: IPv6.js, punycode.js, SecondLevelDomains.js, URI.js, URITemplate.js */
3 | (function(f,l){"object"===typeof exports?module.exports=l():"function"===typeof define&&define.amd?define(l):f.IPv6=l(f)})(this,function(f){var l=f&&f.IPv6;return{best:function(g){g=g.toLowerCase().split(":");var m=g.length,b=8;""===g[0]&&""===g[1]&&""===g[2]?(g.shift(),g.shift()):""===g[0]&&""===g[1]?g.shift():""===g[m-1]&&""===g[m-2]&&g.pop();m=g.length;-1!==g[m-1].indexOf(".")&&(b=7);var k;for(k=0;kf;f++)if("0"===m[0]&&1f&&(m=h,f=l)):"0"===g[k]&&(r=!0,h=k,l=1);l>f&&(m=h,f=l);1=c&&h>>10&1023|55296),b=56320|b&1023);return e+=A(b)}).join("")}function y(b,
6 | e){return b+22+75*(26>b)-((0!=e)<<5)}function p(b,e,h){var a=0;b=h?q(b/700):b>>1;for(b+=q(b/e);455w&&(w=0);for(x=0;x=h&&l("invalid-input");f=b.charCodeAt(w++);f=10>f-48?f-22:26>f-65?f-65:26>f-97?f-97:36;(36<=f||f>q((2147483647-c)/a))&&l("overflow");c+=f*a;m=
7 | g<=t?1:g>=t+26?26:g-t;if(fq(2147483647/f)&&l("overflow");a*=f}a=e.length+1;t=p(c-x,a,0==x);q(c/a)>2147483647-d&&l("overflow");d+=q(c/a);c%=a;e.splice(c++,0,d)}return k(e)}function r(e){var h,g,a,c,d,t,w,x,f,m=[],r,k,n;e=b(e);r=e.length;h=128;g=0;d=72;for(t=0;tf&&m.push(A(f));for((a=c=m.length)&&m.push("-");a=h&&fq((2147483647-g)/k)&&l("overflow");g+=(w-h)*k;h=w;for(t=0;t=d+26?26:w-d;if(x= 0x80 (not a basic code point)",
9 | "invalid-input":"Invalid input"},q=Math.floor,A=String.fromCharCode,D;s={version:"1.2.3",ucs2:{decode:b,encode:k},decode:h,encode:r,toASCII:function(b){return m(b,function(b){return e.test(b)?"xn--"+r(b):b})},toUnicode:function(b){return m(b,function(b){return n.test(b)?h(b.slice(4).toLowerCase()):b})}};if("function"==typeof define&&"object"==typeof define.amd&&define.amd)define(function(){return s});else if(B&&!B.nodeType)if(C)C.exports=s;else for(D in s)s.hasOwnProperty(D)&&(B[D]=s[D]);else f.punycode=
10 | s})(this);
11 | (function(f,l){"object"===typeof exports?module.exports=l():"function"===typeof define&&define.amd?define(l):f.SecondLevelDomains=l(f)})(this,function(f){var l=f&&f.SecondLevelDomains,g={list:{ac:" com gov mil net org ",ae:" ac co gov mil name net org pro sch ",af:" com edu gov net org ",al:" com edu gov mil net org ",ao:" co ed gv it og pb ",ar:" com edu gob gov int mil net org tur ",at:" ac co gv or ",au:" asn com csiro edu gov id net org ",ba:" co com edu gov mil net org rs unbi unmo unsa untz unze ",bb:" biz co com edu gov info net org store tv ",
12 | bh:" biz cc com edu gov info net org ",bn:" com edu gov net org ",bo:" com edu gob gov int mil net org tv ",br:" adm adv agr am arq art ato b bio blog bmd cim cng cnt com coop ecn edu eng esp etc eti far flog fm fnd fot fst g12 ggf gov imb ind inf jor jus lel mat med mil mus net nom not ntr odo org ppg pro psc psi qsl rec slg srv tmp trd tur tv vet vlog wiki zlg ",bs:" com edu gov net org ",bz:" du et om ov rg ",ca:" ab bc mb nb nf nl ns nt nu on pe qc sk yk ",ck:" biz co edu gen gov info net org ",
13 | cn:" ac ah bj com cq edu fj gd gov gs gx gz ha hb he hi hl hn jl js jx ln mil net nm nx org qh sc sd sh sn sx tj tw xj xz yn zj ",co:" com edu gov mil net nom org ",cr:" ac c co ed fi go or sa ",cy:" ac biz com ekloges gov ltd name net org parliament press pro tm ","do":" art com edu gob gov mil net org sld web ",dz:" art asso com edu gov net org pol ",ec:" com edu fin gov info med mil net org pro ",eg:" com edu eun gov mil name net org sci ",er:" com edu gov ind mil net org rochest w ",es:" com edu gob nom org ",
14 | et:" biz com edu gov info name net org ",fj:" ac biz com info mil name net org pro ",fk:" ac co gov net nom org ",fr:" asso com f gouv nom prd presse tm ",gg:" co net org ",gh:" com edu gov mil org ",gn:" ac com gov net org ",gr:" com edu gov mil net org ",gt:" com edu gob ind mil net org ",gu:" com edu gov net org ",hk:" com edu gov idv net org ",hu:" 2000 agrar bolt casino city co erotica erotika film forum games hotel info ingatlan jogasz konyvelo lakas media news org priv reklam sex shop sport suli szex tm tozsde utazas video ",
15 | id:" ac co go mil net or sch web ",il:" ac co gov idf k12 muni net org ","in":" ac co edu ernet firm gen gov i ind mil net nic org res ",iq:" com edu gov i mil net org ",ir:" ac co dnssec gov i id net org sch ",it:" edu gov ",je:" co net org ",jo:" com edu gov mil name net org sch ",jp:" ac ad co ed go gr lg ne or ",ke:" ac co go info me mobi ne or sc ",kh:" com edu gov mil net org per ",ki:" biz com de edu gov info mob net org tel ",km:" asso com coop edu gouv k medecin mil nom notaires pharmaciens presse tm veterinaire ",
16 | kn:" edu gov net org ",kr:" ac busan chungbuk chungnam co daegu daejeon es gangwon go gwangju gyeongbuk gyeonggi gyeongnam hs incheon jeju jeonbuk jeonnam k kg mil ms ne or pe re sc seoul ulsan ",kw:" com edu gov net org ",ky:" com edu gov net org ",kz:" com edu gov mil net org ",lb:" com edu gov net org ",lk:" assn com edu gov grp hotel int ltd net ngo org sch soc web ",lr:" com edu gov net org ",lv:" asn com conf edu gov id mil net org ",ly:" com edu gov id med net org plc sch ",ma:" ac co gov m net org press ",
17 | mc:" asso tm ",me:" ac co edu gov its net org priv ",mg:" com edu gov mil nom org prd tm ",mk:" com edu gov inf name net org pro ",ml:" com edu gov net org presse ",mn:" edu gov org ",mo:" com edu gov net org ",mt:" com edu gov net org ",mv:" aero biz com coop edu gov info int mil museum name net org pro ",mw:" ac co com coop edu gov int museum net org ",mx:" com edu gob net org ",my:" com edu gov mil name net org sch ",nf:" arts com firm info net other per rec store web ",ng:" biz com edu gov mil mobi name net org sch ",
18 | ni:" ac co com edu gob mil net nom org ",np:" com edu gov mil net org ",nr:" biz com edu gov info net org ",om:" ac biz co com edu gov med mil museum net org pro sch ",pe:" com edu gob mil net nom org sld ",ph:" com edu gov i mil net ngo org ",pk:" biz com edu fam gob gok gon gop gos gov net org web ",pl:" art bialystok biz com edu gda gdansk gorzow gov info katowice krakow lodz lublin mil net ngo olsztyn org poznan pwr radom slupsk szczecin torun warszawa waw wroc wroclaw zgora ",pr:" ac biz com edu est gov info isla name net org pro prof ",
19 | ps:" com edu gov net org plo sec ",pw:" belau co ed go ne or ",ro:" arts com firm info nom nt org rec store tm www ",rs:" ac co edu gov in org ",sb:" com edu gov net org ",sc:" com edu gov net org ",sh:" co com edu gov net nom org ",sl:" com edu gov net org ",st:" co com consulado edu embaixada gov mil net org principe saotome store ",sv:" com edu gob org red ",sz:" ac co org ",tr:" av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ",tt:" aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ",
20 | tw:" club com ebiz edu game gov idv mil net org ",mu:" ac co com gov net or org ",mz:" ac co edu gov org ",na:" co com ",nz:" ac co cri geek gen govt health iwi maori mil net org parliament school ",pa:" abo ac com edu gob ing med net nom org sld ",pt:" com edu gov int net nome org publ ",py:" com edu gov mil net org ",qa:" com edu gov mil net org ",re:" asso com nom ",ru:" ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ",
21 | rw:" ac co com edu gouv gov int mil net ",sa:" com edu gov med net org pub sch ",sd:" com edu gov info med net org tv ",se:" a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ",sg:" com edu gov idn net org per ",sn:" art com edu gouv org perso univ ",sy:" com edu gov mil net news org ",th:" ac co go in mi net or ",tj:" ac biz co com edu go gov info int mil name net nic org test web ",tn:" agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ",
22 | tz:" ac co go ne or ",ua:" biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ",ug:" ac co go ne or org sc ",uk:" ac bl british-library co cym gov govt icnet jet lea ltd me mil mod national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ",
23 | us:" dni fed isa kids nsn ",uy:" com edu gub mil net org ",ve:" co com edu gob info mil net org web ",vi:" co com k12 net org ",vn:" ac biz com edu gov health info int name net org pro ",ye:" co com gov ltd me net org plc ",yu:" ac co edu gov org ",za:" ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ",zm:" ac co com edu gov net org sch "},has:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1)return!1;
24 | var k=f.lastIndexOf(".",b-1);if(0>=k||k>=b-1)return!1;var l=g.list[f.slice(b+1)];return l?0<=l.indexOf(" "+f.slice(k+1,b)+" "):!1},is:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1||0<=f.lastIndexOf(".",b-1))return!1;var k=g.list[f.slice(b+1)];return k?0<=k.indexOf(" "+f.slice(0,b)+" "):!1},get:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1)return null;var k=f.lastIndexOf(".",b-1);if(0>=k||k>=b-1)return null;var l=g.list[f.slice(b+1)];return!l||0>l.indexOf(" "+f.slice(k+
25 | 1,b)+" ")?null:f.slice(k+1)},noConflict:function(){f.SecondLevelDomains===this&&(f.SecondLevelDomains=l);return this}};return g});
26 | (function(f,l){"object"===typeof exports?module.exports=l(require("./punycode"),require("./IPv6"),require("./SecondLevelDomains")):"function"===typeof define&&define.amd?define(["./punycode","./IPv6","./SecondLevelDomains"],l):f.URI=l(f.punycode,f.IPv6,f.SecondLevelDomains,f)})(this,function(f,l,g,m){function b(a,c){if(!(this instanceof b))return new b(a,c);void 0===a&&(a="undefined"!==typeof location?location.href+"":"");this.href(a);return void 0!==c?this.absoluteTo(c):this}function k(a){return a.replace(/([.*+?^=!:${}()|[\]\/\\])/g,
27 | "\\$1")}function y(a){return void 0===a?"Undefined":String(Object.prototype.toString.call(a)).slice(8,-1)}function p(a){return"Array"===y(a)}function h(a,c){var d,b;if(p(c)){d=0;for(b=c.length;d]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))/ig;b.findUri={start:/\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,end:/[\s\r\n]|$/,trim:/[`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u201e\u2018\u2019]+$/};b.defaultPorts={http:"80",https:"443",ftp:"21",gopher:"70",ws:"80",wss:"443"};b.invalid_hostname_characters=
31 | /[^a-zA-Z0-9\.-]/;b.domAttributes={a:"href",blockquote:"cite",link:"href",base:"href",script:"src",form:"action",img:"src",area:"href",iframe:"src",embed:"src",source:"src",track:"src",input:"src",audio:"src",video:"src"};b.getDomAttribute=function(a){if(a&&a.nodeName){var c=a.nodeName.toLowerCase();return"input"===c&&"image"!==a.type?void 0:b.domAttributes[c]}};b.encode=C;b.decode=decodeURIComponent;b.iso8859=function(){b.encode=escape;b.decode=unescape};b.unicode=function(){b.encode=C;b.decode=
32 | decodeURIComponent};b.characters={pathname:{encode:{expression:/%(24|26|2B|2C|3B|3D|3A|40)/ig,map:{"%24":"$","%26":"&","%2B":"+","%2C":",","%3B":";","%3D":"=","%3A":":","%40":"@"}},decode:{expression:/[\/\?#]/g,map:{"/":"%2F","?":"%3F","#":"%23"}}},reserved:{encode:{expression:/%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,map:{"%3A":":","%2F":"/","%3F":"?","%23":"#","%5B":"[","%5D":"]","%40":"@","%21":"!","%24":"$","%26":"&","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",",
33 | "%3B":";","%3D":"="}}}};b.encodeQuery=function(a,c){var d=b.encode(a+"");void 0===c&&(c=b.escapeQuerySpace);return c?d.replace(/%20/g,"+"):d};b.decodeQuery=function(a,c){a+="";void 0===c&&(c=b.escapeQuerySpace);try{return b.decode(c?a.replace(/\+/g,"%20"):a)}catch(d){return a}};b.recodePath=function(a){a=(a+"").split("/");for(var c=0,d=a.length;cb)return a.charAt(0)===c.charAt(0)&&"/"===a.charAt(0)?"/":"";if("/"!==a.charAt(b)||"/"!==c.charAt(b))b=a.substring(0,b).lastIndexOf("/");return a.substring(0,b+1)};b.withinString=function(a,c,d){d||(d={});var e=d.start||b.findUri.start,f=d.end||b.findUri.end,h=d.trim||b.findUri.trim,g=/[a-z0-9-]=["']?$/i;for(e.lastIndex=0;;){var r=e.exec(a);if(!r)break;r=r.index;if(d.ignoreHtml){var k=
44 | a.slice(Math.max(r-3,0),r);if(k&&g.test(k))continue}var k=r+a.slice(r).search(f),m=a.slice(r,k).replace(h,"");d.ignore&&d.ignore.test(m)||(k=r+m.length,m=c(m,r,k,a),a=a.slice(0,r)+m+a.slice(k),e.lastIndex=r+m.length)}e.lastIndex=0;return a};b.ensureValidHostname=function(a){if(a.match(b.invalid_hostname_characters)){if(!f)throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-] and Punycode.js is not available');if(f.toASCII(a).match(b.invalid_hostname_characters))throw new TypeError('Hostname "'+
45 | a+'" contains characters other than [A-Z0-9.-]');}};b.noConflict=function(a){if(a)return a={URI:this.noConflict()},m.URITemplate&&"function"===typeof m.URITemplate.noConflict&&(a.URITemplate=m.URITemplate.noConflict()),m.IPv6&&"function"===typeof m.IPv6.noConflict&&(a.IPv6=m.IPv6.noConflict()),m.SecondLevelDomains&&"function"===typeof m.SecondLevelDomains.noConflict&&(a.SecondLevelDomains=m.SecondLevelDomains.noConflict()),a;m.URI===this&&(m.URI=n);return this};e.build=function(a){if(!0===a)this._deferred_build=
46 | !0;else if(void 0===a||this._deferred_build)this._string=b.build(this._parts),this._deferred_build=!1;return this};e.clone=function(){return new b(this)};e.valueOf=e.toString=function(){return this.build(!1)._string};e.protocol=z("protocol");e.username=z("username");e.password=z("password");e.hostname=z("hostname");e.port=z("port");e.query=s("query","?");e.fragment=s("fragment","#");e.search=function(a,c){var d=this.query(a,c);return"string"===typeof d&&d.length?"?"+d:d};e.hash=function(a,c){var d=
47 | this.fragment(a,c);return"string"===typeof d&&d.length?"#"+d:d};e.pathname=function(a,c){if(void 0===a||!0===a){var d=this._parts.path||(this._parts.hostname?"/":"");return a?b.decodePath(d):d}this._parts.path=a?b.recodePath(a):"/";this.build(!c);return this};e.path=e.pathname;e.href=function(a,c){var d;if(void 0===a)return this.toString();this._string="";this._parts=b._parts();var e=a instanceof b,f="object"===typeof a&&(a.hostname||a.path||a.pathname);a.nodeName&&(f=b.getDomAttribute(a),a=a[f]||
48 | "",f=!1);!e&&f&&void 0!==a.pathname&&(a=a.toString());if("string"===typeof a||a instanceof String)this._parts=b.parse(String(a),this._parts);else if(e||f)for(d in e=e?a._parts:a,e)u.call(this._parts,d)&&(this._parts[d]=e[d]);else throw new TypeError("invalid input");this.build(!c);return this};e.is=function(a){var c=!1,d=!1,e=!1,f=!1,h=!1,r=!1,k=!1,m=!this._parts.urn;this._parts.hostname&&(m=!1,d=b.ip4_expression.test(this._parts.hostname),e=b.ip6_expression.test(this._parts.hostname),c=d||e,h=(f=
49 | !c)&&g&&g.has(this._parts.hostname),r=f&&b.idn_expression.test(this._parts.hostname),k=f&&b.punycode_expression.test(this._parts.hostname));switch(a.toLowerCase()){case "relative":return m;case "absolute":return!m;case "domain":case "name":return f;case "sld":return h;case "ip":return c;case "ip4":case "ipv4":case "inet4":return d;case "ip6":case "ipv6":case "inet6":return e;case "idn":return r;case "url":return!this._parts.urn;case "urn":return!!this._parts.urn;case "punycode":return k}return null};
50 | var D=e.protocol,E=e.port,F=e.hostname;e.protocol=function(a,c){if(void 0!==a&&a&&(a=a.replace(/:(\/\/)?$/,""),!a.match(b.protocol_expression)))throw new TypeError('Protocol "'+a+"\" contains characters other than [A-Z0-9.+-] or doesn't start with [A-Z]");return D.call(this,a,c)};e.scheme=e.protocol;e.port=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a&&(0===a&&(a=null),a&&(a+="",":"===a.charAt(0)&&(a=a.substring(1)),a.match(/[^0-9]/))))throw new TypeError('Port "'+a+'" contains characters other than [0-9]');
51 | return E.call(this,a,c)};e.hostname=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a){var d={};b.parseHost(a,d);a=d.hostname}return F.call(this,a,c)};e.host=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?b.buildHost(this._parts):"";b.parseHost(a,this._parts);this.build(!c);return this};e.authority=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?b.buildAuthority(this._parts):
52 | "";b.parseAuthority(a,this._parts);this.build(!c);return this};e.userinfo=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){if(!this._parts.username)return"";var d=b.buildUserinfo(this._parts);return d.substring(0,d.length-1)}"@"!==a[a.length-1]&&(a+="@");b.parseUserinfo(a,this._parts);this.build(!c);return this};e.resource=function(a,c){var d;if(void 0===a)return this.path()+this.search()+this.hash();d=b.parse(a);this._parts.path=d.path;this._parts.query=d.query;this._parts.fragment=
53 | d.fragment;this.build(!c);return this};e.subdomain=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d=this._parts.hostname.length-this.domain().length-1;return this._parts.hostname.substring(0,d)||""}d=this._parts.hostname.length-this.domain().length;d=this._parts.hostname.substring(0,d);d=new RegExp("^"+k(d));a&&"."!==a.charAt(a.length-1)&&(a+=".");a&&b.ensureValidHostname(a);this._parts.hostname=this._parts.hostname.replace(d,
54 | a);this.build(!c);return this};e.domain=function(a,c){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(c=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d=this._parts.hostname.match(/\./g);if(d&&2>d.length)return this._parts.hostname;d=this._parts.hostname.length-this.tld(c).length-1;d=this._parts.hostname.lastIndexOf(".",d-1)+1;return this._parts.hostname.substring(d)||""}if(!a)throw new TypeError("cannot set domain empty");b.ensureValidHostname(a);
55 | !this._parts.hostname||this.is("IP")?this._parts.hostname=a:(d=new RegExp(k(this.domain())+"$"),this._parts.hostname=this._parts.hostname.replace(d,a));this.build(!c);return this};e.tld=function(a,c){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(c=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d=this._parts.hostname.lastIndexOf("."),d=this._parts.hostname.substring(d+1);return!0!==c&&g&&g.list[d.toLowerCase()]?g.get(this._parts.hostname)||d:d}if(a)if(a.match(/[^a-zA-Z0-9-]/))if(g&&
56 | g.is(a))d=new RegExp(k(this.tld())+"$"),this._parts.hostname=this._parts.hostname.replace(d,a);else throw new TypeError('TLD "'+a+'" contains characters other than [A-Z0-9]');else{if(!this._parts.hostname||this.is("IP"))throw new ReferenceError("cannot set TLD on non-domain host");d=new RegExp(k(this.tld())+"$");this._parts.hostname=this._parts.hostname.replace(d,a)}else throw new TypeError("cannot set TLD empty");this.build(!c);return this};e.directory=function(a,c){if(this._parts.urn)return void 0===
57 | a?"":this;if(void 0===a||!0===a){if(!this._parts.path&&!this._parts.hostname)return"";if("/"===this._parts.path)return"/";var d=this._parts.path.length-this.filename().length-1,d=this._parts.path.substring(0,d)||(this._parts.hostname?"/":"");return a?b.decodePath(d):d}d=this._parts.path.length-this.filename().length;d=this._parts.path.substring(0,d);d=new RegExp("^"+k(d));this.is("relative")||(a||(a="/"),"/"!==a.charAt(0)&&(a="/"+a));a&&"/"!==a.charAt(a.length-1)&&(a+="/");a=b.recodePath(a);this._parts.path=
58 | this._parts.path.replace(d,a);this.build(!c);return this};e.filename=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";var d=this._parts.path.lastIndexOf("/"),d=this._parts.path.substring(d+1);return a?b.decodePathSegment(d):d}d=!1;"/"===a.charAt(0)&&(a=a.substring(1));a.match(/\.?\//)&&(d=!0);var e=new RegExp(k(this.filename())+"$");a=b.recodePath(a);this._parts.path=this._parts.path.replace(e,a);d?this.normalizePath(c):
59 | this.build(!c);return this};e.suffix=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";var d=this.filename(),e=d.lastIndexOf(".");if(-1===e)return"";d=d.substring(e+1);d=/^[a-z0-9%]+$/i.test(d)?d:"";return a?b.decodePathSegment(d):d}"."===a.charAt(0)&&(a=a.substring(1));if(d=this.suffix())e=a?new RegExp(k(d)+"$"):new RegExp(k("."+d)+"$");else{if(!a)return this;this._parts.path+="."+b.recodePath(a)}e&&(a=b.recodePath(a),
60 | this._parts.path=this._parts.path.replace(e,a));this.build(!c);return this};e.segment=function(a,c,d){var b=this._parts.urn?":":"/",e=this.path(),f="/"===e.substring(0,1),e=e.split(b);void 0!==a&&"number"!==typeof a&&(d=c,c=a,a=void 0);if(void 0!==a&&"number"!==typeof a)throw Error('Bad segment "'+a+'", must be 0-based integer');f&&e.shift();0>a&&(a=Math.max(e.length+a,0));if(void 0===c)return void 0===a?e:e[a];if(null===a||void 0===e[a])if(p(c)){e=[];a=0;for(var h=c.length;a http://underscorejs.org
5 | // > (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
6 | // > Underscore may be freely distributed under the MIT license.
7 |
8 | // Baseline setup
9 | // --------------
10 | (function() {
11 |
12 | // Establish the root object, `window` in the browser, or `global` on the server.
13 | var root = this;
14 |
15 | // Save the previous value of the `_` variable.
16 | var previousUnderscore = root._;
17 |
18 | // Establish the object that gets returned to break out of a loop iteration.
19 | var breaker = {};
20 |
21 | // Save bytes in the minified (but not gzipped) version:
22 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
23 |
24 | // Create quick reference variables for speed access to core prototypes.
25 | var push = ArrayProto.push,
26 | slice = ArrayProto.slice,
27 | concat = ArrayProto.concat,
28 | toString = ObjProto.toString,
29 | hasOwnProperty = ObjProto.hasOwnProperty;
30 |
31 | // All **ECMAScript 5** native function implementations that we hope to use
32 | // are declared here.
33 | var
34 | nativeForEach = ArrayProto.forEach,
35 | nativeMap = ArrayProto.map,
36 | nativeReduce = ArrayProto.reduce,
37 | nativeReduceRight = ArrayProto.reduceRight,
38 | nativeFilter = ArrayProto.filter,
39 | nativeEvery = ArrayProto.every,
40 | nativeSome = ArrayProto.some,
41 | nativeIndexOf = ArrayProto.indexOf,
42 | nativeLastIndexOf = ArrayProto.lastIndexOf,
43 | nativeIsArray = Array.isArray,
44 | nativeKeys = Object.keys,
45 | nativeBind = FuncProto.bind;
46 |
47 | // Create a safe reference to the Underscore object for use below.
48 | var _ = function(obj) {
49 | if (obj instanceof _) return obj;
50 | if (!(this instanceof _)) return new _(obj);
51 | this._wrapped = obj;
52 | };
53 |
54 | // Export the Underscore object for **Node.js**, with
55 | // backwards-compatibility for the old `require()` API. If we're in
56 | // the browser, add `_` as a global object via a string identifier,
57 | // for Closure Compiler "advanced" mode.
58 | if (typeof exports !== 'undefined') {
59 | if (typeof module !== 'undefined' && module.exports) {
60 | exports = module.exports = _;
61 | }
62 | exports._ = _;
63 | } else {
64 | root._ = _;
65 | }
66 |
67 | // Current version.
68 | _.VERSION = '1.4.4';
69 |
70 | // Collection Functions
71 | // --------------------
72 |
73 | // The cornerstone, an `each` implementation, aka `forEach`.
74 | // Handles objects with the built-in `forEach`, arrays, and raw objects.
75 | // Delegates to **ECMAScript 5**'s native `forEach` if available.
76 | var each = _.each = _.forEach = function(obj, iterator, context) {
77 | if (obj == null) return;
78 | if (nativeForEach && obj.forEach === nativeForEach) {
79 | obj.forEach(iterator, context);
80 | } else if (obj.length === +obj.length) {
81 | for (var i = 0, l = obj.length; i < l; i++) {
82 | if (iterator.call(context, obj[i], i, obj) === breaker) return;
83 | }
84 | } else {
85 | for (var key in obj) {
86 | if (_.has(obj, key)) {
87 | if (iterator.call(context, obj[key], key, obj) === breaker) return;
88 | }
89 | }
90 | }
91 | };
92 |
93 | // Return the results of applying the iterator to each element.
94 | // Delegates to **ECMAScript 5**'s native `map` if available.
95 | _.map = _.collect = function(obj, iterator, context) {
96 | var results = [];
97 | if (obj == null) return results;
98 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
99 | each(obj, function(value, index, list) {
100 | results[results.length] = iterator.call(context, value, index, list);
101 | });
102 | return results;
103 | };
104 |
105 | var reduceError = 'Reduce of empty array with no initial value';
106 |
107 | // **Reduce** builds up a single result from a list of values, aka `inject`,
108 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
109 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
110 | var initial = arguments.length > 2;
111 | if (obj == null) obj = [];
112 | if (nativeReduce && obj.reduce === nativeReduce) {
113 | if (context) iterator = _.bind(iterator, context);
114 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
115 | }
116 | each(obj, function(value, index, list) {
117 | if (!initial) {
118 | memo = value;
119 | initial = true;
120 | } else {
121 | memo = iterator.call(context, memo, value, index, list);
122 | }
123 | });
124 | if (!initial) throw new TypeError(reduceError);
125 | return memo;
126 | };
127 |
128 | // The right-associative version of reduce, also known as `foldr`.
129 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
130 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
131 | var initial = arguments.length > 2;
132 | if (obj == null) obj = [];
133 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
134 | if (context) iterator = _.bind(iterator, context);
135 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
136 | }
137 | var length = obj.length;
138 | if (length !== +length) {
139 | var keys = _.keys(obj);
140 | length = keys.length;
141 | }
142 | each(obj, function(value, index, list) {
143 | index = keys ? keys[--length] : --length;
144 | if (!initial) {
145 | memo = obj[index];
146 | initial = true;
147 | } else {
148 | memo = iterator.call(context, memo, obj[index], index, list);
149 | }
150 | });
151 | if (!initial) throw new TypeError(reduceError);
152 | return memo;
153 | };
154 |
155 | // Return the first value which passes a truth test. Aliased as `detect`.
156 | _.find = _.detect = function(obj, iterator, context) {
157 | var result;
158 | any(obj, function(value, index, list) {
159 | if (iterator.call(context, value, index, list)) {
160 | result = value;
161 | return true;
162 | }
163 | });
164 | return result;
165 | };
166 |
167 | // Return all the elements that pass a truth test.
168 | // Delegates to **ECMAScript 5**'s native `filter` if available.
169 | // Aliased as `select`.
170 | _.filter = _.select = function(obj, iterator, context) {
171 | var results = [];
172 | if (obj == null) return results;
173 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
174 | each(obj, function(value, index, list) {
175 | if (iterator.call(context, value, index, list)) results[results.length] = value;
176 | });
177 | return results;
178 | };
179 |
180 | // Return all the elements for which a truth test fails.
181 | _.reject = function(obj, iterator, context) {
182 | return _.filter(obj, function(value, index, list) {
183 | return !iterator.call(context, value, index, list);
184 | }, context);
185 | };
186 |
187 | // Determine whether all of the elements match a truth test.
188 | // Delegates to **ECMAScript 5**'s native `every` if available.
189 | // Aliased as `all`.
190 | _.every = _.all = function(obj, iterator, context) {
191 | iterator || (iterator = _.identity);
192 | var result = true;
193 | if (obj == null) return result;
194 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
195 | each(obj, function(value, index, list) {
196 | if (!(result = result && iterator.call(context, value, index, list))) return breaker;
197 | });
198 | return !!result;
199 | };
200 |
201 | // Determine if at least one element in the object matches a truth test.
202 | // Delegates to **ECMAScript 5**'s native `some` if available.
203 | // Aliased as `any`.
204 | var any = _.some = _.any = function(obj, iterator, context) {
205 | iterator || (iterator = _.identity);
206 | var result = false;
207 | if (obj == null) return result;
208 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
209 | each(obj, function(value, index, list) {
210 | if (result || (result = iterator.call(context, value, index, list))) return breaker;
211 | });
212 | return !!result;
213 | };
214 |
215 | // Determine if the array or object contains a given value (using `===`).
216 | // Aliased as `include`.
217 | _.contains = _.include = function(obj, target) {
218 | if (obj == null) return false;
219 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
220 | return any(obj, function(value) {
221 | return value === target;
222 | });
223 | };
224 |
225 | // Invoke a method (with arguments) on every item in a collection.
226 | _.invoke = function(obj, method) {
227 | var args = slice.call(arguments, 2);
228 | var isFunc = _.isFunction(method);
229 | return _.map(obj, function(value) {
230 | return (isFunc ? method : value[method]).apply(value, args);
231 | });
232 | };
233 |
234 | // Convenience version of a common use case of `map`: fetching a property.
235 | _.pluck = function(obj, key) {
236 | return _.map(obj, function(value){ return value[key]; });
237 | };
238 |
239 | // Convenience version of a common use case of `filter`: selecting only objects
240 | // containing specific `key:value` pairs.
241 | _.where = function(obj, attrs, first) {
242 | if (_.isEmpty(attrs)) return first ? null : [];
243 | return _[first ? 'find' : 'filter'](obj, function(value) {
244 | for (var key in attrs) {
245 | if (attrs[key] !== value[key]) return false;
246 | }
247 | return true;
248 | });
249 | };
250 |
251 | // Convenience version of a common use case of `find`: getting the first object
252 | // containing specific `key:value` pairs.
253 | _.findWhere = function(obj, attrs) {
254 | return _.where(obj, attrs, true);
255 | };
256 |
257 | // Return the maximum element or (element-based computation).
258 | // Can't optimize arrays of integers longer than 65,535 elements.
259 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797
260 | _.max = function(obj, iterator, context) {
261 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
262 | return Math.max.apply(Math, obj);
263 | }
264 | if (!iterator && _.isEmpty(obj)) return -Infinity;
265 | var result = {computed : -Infinity, value: -Infinity};
266 | each(obj, function(value, index, list) {
267 | var computed = iterator ? iterator.call(context, value, index, list) : value;
268 | computed >= result.computed && (result = {value : value, computed : computed});
269 | });
270 | return result.value;
271 | };
272 |
273 | // Return the minimum element (or element-based computation).
274 | _.min = function(obj, iterator, context) {
275 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
276 | return Math.min.apply(Math, obj);
277 | }
278 | if (!iterator && _.isEmpty(obj)) return Infinity;
279 | var result = {computed : Infinity, value: Infinity};
280 | each(obj, function(value, index, list) {
281 | var computed = iterator ? iterator.call(context, value, index, list) : value;
282 | computed < result.computed && (result = {value : value, computed : computed});
283 | });
284 | return result.value;
285 | };
286 |
287 | // Shuffle an array.
288 | _.shuffle = function(obj) {
289 | var rand;
290 | var index = 0;
291 | var shuffled = [];
292 | each(obj, function(value) {
293 | rand = _.random(index++);
294 | shuffled[index - 1] = shuffled[rand];
295 | shuffled[rand] = value;
296 | });
297 | return shuffled;
298 | };
299 |
300 | // An internal function to generate lookup iterators.
301 | var lookupIterator = function(value) {
302 | return _.isFunction(value) ? value : function(obj){ return obj[value]; };
303 | };
304 |
305 | // Sort the object's values by a criterion produced by an iterator.
306 | _.sortBy = function(obj, value, context) {
307 | var iterator = lookupIterator(value);
308 | return _.pluck(_.map(obj, function(value, index, list) {
309 | return {
310 | value : value,
311 | index : index,
312 | criteria : iterator.call(context, value, index, list)
313 | };
314 | }).sort(function(left, right) {
315 | var a = left.criteria;
316 | var b = right.criteria;
317 | if (a !== b) {
318 | if (a > b || a === void 0) return 1;
319 | if (a < b || b === void 0) return -1;
320 | }
321 | return left.index < right.index ? -1 : 1;
322 | }), 'value');
323 | };
324 |
325 | // An internal function used for aggregate "group by" operations.
326 | var group = function(obj, value, context, behavior) {
327 | var result = {};
328 | var iterator = lookupIterator(value || _.identity);
329 | each(obj, function(value, index) {
330 | var key = iterator.call(context, value, index, obj);
331 | behavior(result, key, value);
332 | });
333 | return result;
334 | };
335 |
336 | // Groups the object's values by a criterion. Pass either a string attribute
337 | // to group by, or a function that returns the criterion.
338 | _.groupBy = function(obj, value, context) {
339 | return group(obj, value, context, function(result, key, value) {
340 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
341 | });
342 | };
343 |
344 | // Counts instances of an object that group by a certain criterion. Pass
345 | // either a string attribute to count by, or a function that returns the
346 | // criterion.
347 | _.countBy = function(obj, value, context) {
348 | return group(obj, value, context, function(result, key) {
349 | if (!_.has(result, key)) result[key] = 0;
350 | result[key]++;
351 | });
352 | };
353 |
354 | // Use a comparator function to figure out the smallest index at which
355 | // an object should be inserted so as to maintain order. Uses binary search.
356 | _.sortedIndex = function(array, obj, iterator, context) {
357 | iterator = iterator == null ? _.identity : lookupIterator(iterator);
358 | var value = iterator.call(context, obj);
359 | var low = 0, high = array.length;
360 | while (low < high) {
361 | var mid = (low + high) >>> 1;
362 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
363 | }
364 | return low;
365 | };
366 |
367 | // Safely convert anything iterable into a real, live array.
368 | _.toArray = function(obj) {
369 | if (!obj) return [];
370 | if (_.isArray(obj)) return slice.call(obj);
371 | if (obj.length === +obj.length) return _.map(obj, _.identity);
372 | return _.values(obj);
373 | };
374 |
375 | // Return the number of elements in an object.
376 | _.size = function(obj) {
377 | if (obj == null) return 0;
378 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
379 | };
380 |
381 | // Array Functions
382 | // ---------------
383 |
384 | // Get the first element of an array. Passing **n** will return the first N
385 | // values in the array. Aliased as `head` and `take`. The **guard** check
386 | // allows it to work with `_.map`.
387 | _.first = _.head = _.take = function(array, n, guard) {
388 | if (array == null) return void 0;
389 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
390 | };
391 |
392 | // Returns everything but the last entry of the array. Especially useful on
393 | // the arguments object. Passing **n** will return all the values in
394 | // the array, excluding the last N. The **guard** check allows it to work with
395 | // `_.map`.
396 | _.initial = function(array, n, guard) {
397 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
398 | };
399 |
400 | // Get the last element of an array. Passing **n** will return the last N
401 | // values in the array. The **guard** check allows it to work with `_.map`.
402 | _.last = function(array, n, guard) {
403 | if (array == null) return void 0;
404 | if ((n != null) && !guard) {
405 | return slice.call(array, Math.max(array.length - n, 0));
406 | } else {
407 | return array[array.length - 1];
408 | }
409 | };
410 |
411 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
412 | // Especially useful on the arguments object. Passing an **n** will return
413 | // the rest N values in the array. The **guard**
414 | // check allows it to work with `_.map`.
415 | _.rest = _.tail = _.drop = function(array, n, guard) {
416 | return slice.call(array, (n == null) || guard ? 1 : n);
417 | };
418 |
419 | // Trim out all falsy values from an array.
420 | _.compact = function(array) {
421 | return _.filter(array, _.identity);
422 | };
423 |
424 | // Internal implementation of a recursive `flatten` function.
425 | var flatten = function(input, shallow, output) {
426 | each(input, function(value) {
427 | if (_.isArray(value)) {
428 | shallow ? push.apply(output, value) : flatten(value, shallow, output);
429 | } else {
430 | output.push(value);
431 | }
432 | });
433 | return output;
434 | };
435 |
436 | // Return a completely flattened version of an array.
437 | _.flatten = function(array, shallow) {
438 | return flatten(array, shallow, []);
439 | };
440 |
441 | // Return a version of the array that does not contain the specified value(s).
442 | _.without = function(array) {
443 | return _.difference(array, slice.call(arguments, 1));
444 | };
445 |
446 | // Produce a duplicate-free version of the array. If the array has already
447 | // been sorted, you have the option of using a faster algorithm.
448 | // Aliased as `unique`.
449 | _.uniq = _.unique = function(array, isSorted, iterator, context) {
450 | if (_.isFunction(isSorted)) {
451 | context = iterator;
452 | iterator = isSorted;
453 | isSorted = false;
454 | }
455 | var initial = iterator ? _.map(array, iterator, context) : array;
456 | var results = [];
457 | var seen = [];
458 | each(initial, function(value, index) {
459 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
460 | seen.push(value);
461 | results.push(array[index]);
462 | }
463 | });
464 | return results;
465 | };
466 |
467 | // Produce an array that contains the union: each distinct element from all of
468 | // the passed-in arrays.
469 | _.union = function() {
470 | return _.uniq(concat.apply(ArrayProto, arguments));
471 | };
472 |
473 | // Produce an array that contains every item shared between all the
474 | // passed-in arrays.
475 | _.intersection = function(array) {
476 | var rest = slice.call(arguments, 1);
477 | return _.filter(_.uniq(array), function(item) {
478 | return _.every(rest, function(other) {
479 | return _.indexOf(other, item) >= 0;
480 | });
481 | });
482 | };
483 |
484 | // Take the difference between one array and a number of other arrays.
485 | // Only the elements present in just the first array will remain.
486 | _.difference = function(array) {
487 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
488 | return _.filter(array, function(value){ return !_.contains(rest, value); });
489 | };
490 |
491 | // Zip together multiple lists into a single array -- elements that share
492 | // an index go together.
493 | _.zip = function() {
494 | var args = slice.call(arguments);
495 | var length = _.max(_.pluck(args, 'length'));
496 | var results = new Array(length);
497 | for (var i = 0; i < length; i++) {
498 | results[i] = _.pluck(args, "" + i);
499 | }
500 | return results;
501 | };
502 |
503 | // Converts lists into objects. Pass either a single array of `[key, value]`
504 | // pairs, or two parallel arrays of the same length -- one of keys, and one of
505 | // the corresponding values.
506 | _.object = function(list, values) {
507 | if (list == null) return {};
508 | var result = {};
509 | for (var i = 0, l = list.length; i < l; i++) {
510 | if (values) {
511 | result[list[i]] = values[i];
512 | } else {
513 | result[list[i][0]] = list[i][1];
514 | }
515 | }
516 | return result;
517 | };
518 |
519 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
520 | // we need this function. Return the position of the first occurrence of an
521 | // item in an array, or -1 if the item is not included in the array.
522 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
523 | // If the array is large and already in sort order, pass `true`
524 | // for **isSorted** to use binary search.
525 | _.indexOf = function(array, item, isSorted) {
526 | if (array == null) return -1;
527 | var i = 0, l = array.length;
528 | if (isSorted) {
529 | if (typeof isSorted == 'number') {
530 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
531 | } else {
532 | i = _.sortedIndex(array, item);
533 | return array[i] === item ? i : -1;
534 | }
535 | }
536 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
537 | for (; i < l; i++) if (array[i] === item) return i;
538 | return -1;
539 | };
540 |
541 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
542 | _.lastIndexOf = function(array, item, from) {
543 | if (array == null) return -1;
544 | var hasIndex = from != null;
545 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
546 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
547 | }
548 | var i = (hasIndex ? from : array.length);
549 | while (i--) if (array[i] === item) return i;
550 | return -1;
551 | };
552 |
553 | // Generate an integer Array containing an arithmetic progression. A port of
554 | // the native Python `range()` function. See
555 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
556 | _.range = function(start, stop, step) {
557 | if (arguments.length <= 1) {
558 | stop = start || 0;
559 | start = 0;
560 | }
561 | step = arguments[2] || 1;
562 |
563 | var len = Math.max(Math.ceil((stop - start) / step), 0);
564 | var idx = 0;
565 | var range = new Array(len);
566 |
567 | while(idx < len) {
568 | range[idx++] = start;
569 | start += step;
570 | }
571 |
572 | return range;
573 | };
574 |
575 | // Function (ahem) Functions
576 | // ------------------
577 |
578 | // Create a function bound to a given object (assigning `this`, and arguments,
579 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
580 | // available.
581 | _.bind = function(func, context) {
582 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
583 | var args = slice.call(arguments, 2);
584 | return function() {
585 | return func.apply(context, args.concat(slice.call(arguments)));
586 | };
587 | };
588 |
589 | // Partially apply a function by creating a version that has had some of its
590 | // arguments pre-filled, without changing its dynamic `this` context.
591 | _.partial = function(func) {
592 | var args = slice.call(arguments, 1);
593 | return function() {
594 | return func.apply(this, args.concat(slice.call(arguments)));
595 | };
596 | };
597 |
598 | // Bind all of an object's methods to that object. Useful for ensuring that
599 | // all callbacks defined on an object belong to it.
600 | _.bindAll = function(obj) {
601 | var funcs = slice.call(arguments, 1);
602 | if (funcs.length === 0) funcs = _.functions(obj);
603 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
604 | return obj;
605 | };
606 |
607 | // Memoize an expensive function by storing its results.
608 | _.memoize = function(func, hasher) {
609 | var memo = {};
610 | hasher || (hasher = _.identity);
611 | return function() {
612 | var key = hasher.apply(this, arguments);
613 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
614 | };
615 | };
616 |
617 | // Delays a function for the given number of milliseconds, and then calls
618 | // it with the arguments supplied.
619 | _.delay = function(func, wait) {
620 | var args = slice.call(arguments, 2);
621 | return setTimeout(function(){ return func.apply(null, args); }, wait);
622 | };
623 |
624 | // Defers a function, scheduling it to run after the current call stack has
625 | // cleared.
626 | _.defer = function(func) {
627 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
628 | };
629 |
630 | // Returns a function, that, when invoked, will only be triggered at most once
631 | // during a given window of time.
632 | _.throttle = function(func, wait) {
633 | var context, args, timeout, result;
634 | var previous = 0;
635 | var later = function() {
636 | previous = new Date;
637 | timeout = null;
638 | result = func.apply(context, args);
639 | };
640 | return function() {
641 | var now = new Date;
642 | var remaining = wait - (now - previous);
643 | context = this;
644 | args = arguments;
645 | if (remaining <= 0) {
646 | clearTimeout(timeout);
647 | timeout = null;
648 | previous = now;
649 | result = func.apply(context, args);
650 | } else if (!timeout) {
651 | timeout = setTimeout(later, remaining);
652 | }
653 | return result;
654 | };
655 | };
656 |
657 | // Returns a function, that, as long as it continues to be invoked, will not
658 | // be triggered. The function will be called after it stops being called for
659 | // N milliseconds. If `immediate` is passed, trigger the function on the
660 | // leading edge, instead of the trailing.
661 | _.debounce = function(func, wait, immediate) {
662 | var timeout, result;
663 | return function() {
664 | var context = this, args = arguments;
665 | var later = function() {
666 | timeout = null;
667 | if (!immediate) result = func.apply(context, args);
668 | };
669 | var callNow = immediate && !timeout;
670 | clearTimeout(timeout);
671 | timeout = setTimeout(later, wait);
672 | if (callNow) result = func.apply(context, args);
673 | return result;
674 | };
675 | };
676 |
677 | // Returns a function that will be executed at most one time, no matter how
678 | // often you call it. Useful for lazy initialization.
679 | _.once = function(func) {
680 | var ran = false, memo;
681 | return function() {
682 | if (ran) return memo;
683 | ran = true;
684 | memo = func.apply(this, arguments);
685 | func = null;
686 | return memo;
687 | };
688 | };
689 |
690 | // Returns the first function passed as an argument to the second,
691 | // allowing you to adjust arguments, run code before and after, and
692 | // conditionally execute the original function.
693 | _.wrap = function(func, wrapper) {
694 | return function() {
695 | var args = [func];
696 | push.apply(args, arguments);
697 | return wrapper.apply(this, args);
698 | };
699 | };
700 |
701 | // Returns a function that is the composition of a list of functions, each
702 | // consuming the return value of the function that follows.
703 | _.compose = function() {
704 | var funcs = arguments;
705 | return function() {
706 | var args = arguments;
707 | for (var i = funcs.length - 1; i >= 0; i--) {
708 | args = [funcs[i].apply(this, args)];
709 | }
710 | return args[0];
711 | };
712 | };
713 |
714 | // Returns a function that will only be executed after being called N times.
715 | _.after = function(times, func) {
716 | if (times <= 0) return func();
717 | return function() {
718 | if (--times < 1) {
719 | return func.apply(this, arguments);
720 | }
721 | };
722 | };
723 |
724 | // Object Functions
725 | // ----------------
726 |
727 | // Retrieve the names of an object's properties.
728 | // Delegates to **ECMAScript 5**'s native `Object.keys`
729 | _.keys = nativeKeys || function(obj) {
730 | if (obj !== Object(obj)) throw new TypeError('Invalid object');
731 | var keys = [];
732 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
733 | return keys;
734 | };
735 |
736 | // Retrieve the values of an object's properties.
737 | _.values = function(obj) {
738 | var values = [];
739 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
740 | return values;
741 | };
742 |
743 | // Convert an object into a list of `[key, value]` pairs.
744 | _.pairs = function(obj) {
745 | var pairs = [];
746 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
747 | return pairs;
748 | };
749 |
750 | // Invert the keys and values of an object. The values must be serializable.
751 | _.invert = function(obj) {
752 | var result = {};
753 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
754 | return result;
755 | };
756 |
757 | // Return a sorted list of the function names available on the object.
758 | // Aliased as `methods`
759 | _.functions = _.methods = function(obj) {
760 | var names = [];
761 | for (var key in obj) {
762 | if (_.isFunction(obj[key])) names.push(key);
763 | }
764 | return names.sort();
765 | };
766 |
767 | // Extend a given object with all the properties in passed-in object(s).
768 | _.extend = function(obj) {
769 | each(slice.call(arguments, 1), function(source) {
770 | if (source) {
771 | for (var prop in source) {
772 | obj[prop] = source[prop];
773 | }
774 | }
775 | });
776 | return obj;
777 | };
778 |
779 | // Return a copy of the object only containing the whitelisted properties.
780 | _.pick = function(obj) {
781 | var copy = {};
782 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
783 | each(keys, function(key) {
784 | if (key in obj) copy[key] = obj[key];
785 | });
786 | return copy;
787 | };
788 |
789 | // Return a copy of the object without the blacklisted properties.
790 | _.omit = function(obj) {
791 | var copy = {};
792 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
793 | for (var key in obj) {
794 | if (!_.contains(keys, key)) copy[key] = obj[key];
795 | }
796 | return copy;
797 | };
798 |
799 | // Fill in a given object with default properties.
800 | _.defaults = function(obj) {
801 | each(slice.call(arguments, 1), function(source) {
802 | if (source) {
803 | for (var prop in source) {
804 | if (obj[prop] == null) obj[prop] = source[prop];
805 | }
806 | }
807 | });
808 | return obj;
809 | };
810 |
811 | // Create a (shallow-cloned) duplicate of an object.
812 | _.clone = function(obj) {
813 | if (!_.isObject(obj)) return obj;
814 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
815 | };
816 |
817 | // Invokes interceptor with the obj, and then returns obj.
818 | // The primary purpose of this method is to "tap into" a method chain, in
819 | // order to perform operations on intermediate results within the chain.
820 | _.tap = function(obj, interceptor) {
821 | interceptor(obj);
822 | return obj;
823 | };
824 |
825 | // Internal recursive comparison function for `isEqual`.
826 | var eq = function(a, b, aStack, bStack) {
827 | // Identical objects are equal. `0 === -0`, but they aren't identical.
828 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
829 | if (a === b) return a !== 0 || 1 / a == 1 / b;
830 | // A strict comparison is necessary because `null == undefined`.
831 | if (a == null || b == null) return a === b;
832 | // Unwrap any wrapped objects.
833 | if (a instanceof _) a = a._wrapped;
834 | if (b instanceof _) b = b._wrapped;
835 | // Compare `[[Class]]` names.
836 | var className = toString.call(a);
837 | if (className != toString.call(b)) return false;
838 | switch (className) {
839 | // Strings, numbers, dates, and booleans are compared by value.
840 | case '[object String]':
841 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
842 | // equivalent to `new String("5")`.
843 | return a == String(b);
844 | case '[object Number]':
845 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
846 | // other numeric values.
847 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
848 | case '[object Date]':
849 | case '[object Boolean]':
850 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their
851 | // millisecond representations. Note that invalid dates with millisecond representations
852 | // of `NaN` are not equivalent.
853 | return +a == +b;
854 | // RegExps are compared by their source patterns and flags.
855 | case '[object RegExp]':
856 | return a.source == b.source &&
857 | a.global == b.global &&
858 | a.multiline == b.multiline &&
859 | a.ignoreCase == b.ignoreCase;
860 | }
861 | if (typeof a != 'object' || typeof b != 'object') return false;
862 | // Assume equality for cyclic structures. The algorithm for detecting cyclic
863 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
864 | var length = aStack.length;
865 | while (length--) {
866 | // Linear search. Performance is inversely proportional to the number of
867 | // unique nested structures.
868 | if (aStack[length] == a) return bStack[length] == b;
869 | }
870 | // Add the first object to the stack of traversed objects.
871 | aStack.push(a);
872 | bStack.push(b);
873 | var size = 0, result = true;
874 | // Recursively compare objects and arrays.
875 | if (className == '[object Array]') {
876 | // Compare array lengths to determine if a deep comparison is necessary.
877 | size = a.length;
878 | result = size == b.length;
879 | if (result) {
880 | // Deep compare the contents, ignoring non-numeric properties.
881 | while (size--) {
882 | if (!(result = eq(a[size], b[size], aStack, bStack))) break;
883 | }
884 | }
885 | } else {
886 | // Objects with different constructors are not equivalent, but `Object`s
887 | // from different frames are.
888 | var aCtor = a.constructor, bCtor = b.constructor;
889 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
890 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
891 | return false;
892 | }
893 | // Deep compare objects.
894 | for (var key in a) {
895 | if (_.has(a, key)) {
896 | // Count the expected number of properties.
897 | size++;
898 | // Deep compare each member.
899 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
900 | }
901 | }
902 | // Ensure that both objects contain the same number of properties.
903 | if (result) {
904 | for (key in b) {
905 | if (_.has(b, key) && !(size--)) break;
906 | }
907 | result = !size;
908 | }
909 | }
910 | // Remove the first object from the stack of traversed objects.
911 | aStack.pop();
912 | bStack.pop();
913 | return result;
914 | };
915 |
916 | // Perform a deep comparison to check if two objects are equal.
917 | _.isEqual = function(a, b) {
918 | return eq(a, b, [], []);
919 | };
920 |
921 | // Is a given array, string, or object empty?
922 | // An "empty" object has no enumerable own-properties.
923 | _.isEmpty = function(obj) {
924 | if (obj == null) return true;
925 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
926 | for (var key in obj) if (_.has(obj, key)) return false;
927 | return true;
928 | };
929 |
930 | // Is a given value a DOM element?
931 | _.isElement = function(obj) {
932 | return !!(obj && obj.nodeType === 1);
933 | };
934 |
935 | // Is a given value an array?
936 | // Delegates to ECMA5's native Array.isArray
937 | _.isArray = nativeIsArray || function(obj) {
938 | return toString.call(obj) == '[object Array]';
939 | };
940 |
941 | // Is a given variable an object?
942 | _.isObject = function(obj) {
943 | return obj === Object(obj);
944 | };
945 |
946 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
947 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
948 | _['is' + name] = function(obj) {
949 | return toString.call(obj) == '[object ' + name + ']';
950 | };
951 | });
952 |
953 | // Define a fallback version of the method in browsers (ahem, IE), where
954 | // there isn't any inspectable "Arguments" type.
955 | if (!_.isArguments(arguments)) {
956 | _.isArguments = function(obj) {
957 | return !!(obj && _.has(obj, 'callee'));
958 | };
959 | }
960 |
961 | // Optimize `isFunction` if appropriate.
962 | if (typeof (/./) !== 'function') {
963 | _.isFunction = function(obj) {
964 | return typeof obj === 'function';
965 | };
966 | }
967 |
968 | // Is a given object a finite number?
969 | _.isFinite = function(obj) {
970 | return isFinite(obj) && !isNaN(parseFloat(obj));
971 | };
972 |
973 | // Is the given value `NaN`? (NaN is the only number which does not equal itself).
974 | _.isNaN = function(obj) {
975 | return _.isNumber(obj) && obj != +obj;
976 | };
977 |
978 | // Is a given value a boolean?
979 | _.isBoolean = function(obj) {
980 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
981 | };
982 |
983 | // Is a given value equal to null?
984 | _.isNull = function(obj) {
985 | return obj === null;
986 | };
987 |
988 | // Is a given variable undefined?
989 | _.isUndefined = function(obj) {
990 | return obj === void 0;
991 | };
992 |
993 | // Shortcut function for checking if an object has a given property directly
994 | // on itself (in other words, not on a prototype).
995 | _.has = function(obj, key) {
996 | return hasOwnProperty.call(obj, key);
997 | };
998 |
999 | // Utility Functions
1000 | // -----------------
1001 |
1002 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
1003 | // previous owner. Returns a reference to the Underscore object.
1004 | _.noConflict = function() {
1005 | root._ = previousUnderscore;
1006 | return this;
1007 | };
1008 |
1009 | // Keep the identity function around for default iterators.
1010 | _.identity = function(value) {
1011 | return value;
1012 | };
1013 |
1014 | // Run a function **n** times.
1015 | _.times = function(n, iterator, context) {
1016 | var accum = Array(n);
1017 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
1018 | return accum;
1019 | };
1020 |
1021 | // Return a random integer between min and max (inclusive).
1022 | _.random = function(min, max) {
1023 | if (max == null) {
1024 | max = min;
1025 | min = 0;
1026 | }
1027 | return min + Math.floor(Math.random() * (max - min + 1));
1028 | };
1029 |
1030 | // List of HTML entities for escaping.
1031 | var entityMap = {
1032 | escape: {
1033 | '&': '&',
1034 | '<': '<',
1035 | '>': '>',
1036 | '"': '"',
1037 | "'": ''',
1038 | '/': '/'
1039 | }
1040 | };
1041 | entityMap.unescape = _.invert(entityMap.escape);
1042 |
1043 | // Regexes containing the keys and values listed immediately above.
1044 | var entityRegexes = {
1045 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
1046 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
1047 | };
1048 |
1049 | // Functions for escaping and unescaping strings to/from HTML interpolation.
1050 | _.each(['escape', 'unescape'], function(method) {
1051 | _[method] = function(string) {
1052 | if (string == null) return '';
1053 | return ('' + string).replace(entityRegexes[method], function(match) {
1054 | return entityMap[method][match];
1055 | });
1056 | };
1057 | });
1058 |
1059 | // If the value of the named property is a function then invoke it;
1060 | // otherwise, return it.
1061 | _.result = function(object, property) {
1062 | if (object == null) return null;
1063 | var value = object[property];
1064 | return _.isFunction(value) ? value.call(object) : value;
1065 | };
1066 |
1067 | // Add your own custom functions to the Underscore object.
1068 | _.mixin = function(obj) {
1069 | each(_.functions(obj), function(name){
1070 | var func = _[name] = obj[name];
1071 | _.prototype[name] = function() {
1072 | var args = [this._wrapped];
1073 | push.apply(args, arguments);
1074 | return result.call(this, func.apply(_, args));
1075 | };
1076 | });
1077 | };
1078 |
1079 | // Generate a unique integer id (unique within the entire client session).
1080 | // Useful for temporary DOM ids.
1081 | var idCounter = 0;
1082 | _.uniqueId = function(prefix) {
1083 | var id = ++idCounter + '';
1084 | return prefix ? prefix + id : id;
1085 | };
1086 |
1087 | // By default, Underscore uses ERB-style template delimiters, change the
1088 | // following template settings to use alternative delimiters.
1089 | _.templateSettings = {
1090 | evaluate : /<%([\s\S]+?)%>/g,
1091 | interpolate : /<%=([\s\S]+?)%>/g,
1092 | escape : /<%-([\s\S]+?)%>/g
1093 | };
1094 |
1095 | // When customizing `templateSettings`, if you don't want to define an
1096 | // interpolation, evaluation or escaping regex, we need one that is
1097 | // guaranteed not to match.
1098 | var noMatch = /(.)^/;
1099 |
1100 | // Certain characters need to be escaped so that they can be put into a
1101 | // string literal.
1102 | var escapes = {
1103 | "'": "'",
1104 | '\\': '\\',
1105 | '\r': 'r',
1106 | '\n': 'n',
1107 | '\t': 't',
1108 | '\u2028': 'u2028',
1109 | '\u2029': 'u2029'
1110 | };
1111 |
1112 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
1113 |
1114 | // JavaScript micro-templating, similar to John Resig's implementation.
1115 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
1116 | // and correctly escapes quotes within interpolated code.
1117 | _.template = function(text, data, settings) {
1118 | var render;
1119 | settings = _.defaults({}, settings, _.templateSettings);
1120 |
1121 | // Combine delimiters into one regular expression via alternation.
1122 | var matcher = new RegExp([
1123 | (settings.escape || noMatch).source,
1124 | (settings.interpolate || noMatch).source,
1125 | (settings.evaluate || noMatch).source
1126 | ].join('|') + '|$', 'g');
1127 |
1128 | // Compile the template source, escaping string literals appropriately.
1129 | var index = 0;
1130 | var source = "__p+='";
1131 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
1132 | source += text.slice(index, offset)
1133 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
1134 |
1135 | if (escape) {
1136 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
1137 | }
1138 | if (interpolate) {
1139 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
1140 | }
1141 | if (evaluate) {
1142 | source += "';\n" + evaluate + "\n__p+='";
1143 | }
1144 | index = offset + match.length;
1145 | return match;
1146 | });
1147 | source += "';\n";
1148 |
1149 | // If a variable is not specified, place data values in local scope.
1150 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
1151 |
1152 | source = "var __t,__p='',__j=Array.prototype.join," +
1153 | "print=function(){__p+=__j.call(arguments,'');};\n" +
1154 | source + "return __p;\n";
1155 |
1156 | try {
1157 | render = new Function(settings.variable || 'obj', '_', source);
1158 | } catch (e) {
1159 | e.source = source;
1160 | throw e;
1161 | }
1162 |
1163 | if (data) return render(data, _);
1164 | var template = function(data) {
1165 | return render.call(this, data, _);
1166 | };
1167 |
1168 | // Provide the compiled function source as a convenience for precompilation.
1169 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
1170 |
1171 | return template;
1172 | };
1173 |
1174 | // Add a "chain" function, which will delegate to the wrapper.
1175 | _.chain = function(obj) {
1176 | return _(obj).chain();
1177 | };
1178 |
1179 | // OOP
1180 | // ---------------
1181 | // If Underscore is called as a function, it returns a wrapped object that
1182 | // can be used OO-style. This wrapper holds altered versions of all the
1183 | // underscore functions. Wrapped objects may be chained.
1184 |
1185 | // Helper function to continue chaining intermediate results.
1186 | var result = function(obj) {
1187 | return this._chain ? _(obj).chain() : obj;
1188 | };
1189 |
1190 | // Add all of the Underscore functions to the wrapper object.
1191 | _.mixin(_);
1192 |
1193 | // Add all mutator Array functions to the wrapper.
1194 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1195 | var method = ArrayProto[name];
1196 | _.prototype[name] = function() {
1197 | var obj = this._wrapped;
1198 | method.apply(obj, arguments);
1199 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
1200 | return result.call(this, obj);
1201 | };
1202 | });
1203 |
1204 | // Add all accessor Array functions to the wrapper.
1205 | each(['concat', 'join', 'slice'], function(name) {
1206 | var method = ArrayProto[name];
1207 | _.prototype[name] = function() {
1208 | return result.call(this, method.apply(this._wrapped, arguments));
1209 | };
1210 | });
1211 |
1212 | _.extend(_.prototype, {
1213 |
1214 | // Start chaining a wrapped Underscore object.
1215 | chain: function() {
1216 | this._chain = true;
1217 | return this;
1218 | },
1219 |
1220 | // Extracts the result from a wrapped and chained object.
1221 | value: function() {
1222 | return this._wrapped;
1223 | }
1224 |
1225 | });
1226 |
1227 | }).call(this);
1228 |
--------------------------------------------------------------------------------
/src/main/resources/dependencies/halbrowser/vendor/js/uritemplates.js:
--------------------------------------------------------------------------------
1 | /*
2 | UriTemplates Template Processor - Version: @VERSION - Dated: @DATE
3 | (c) marc.portier@gmail.com - 2011-2012
4 | Licensed under ALPv2
5 | */
6 |
7 | ;
8 | var uritemplate = (function() {
9 |
10 | // Below are the functions we originally used from jQuery.
11 | // The implementations below are often more naive then what is inside jquery, but they suffice for our needs.
12 |
13 | function isFunction(fn) {
14 | return typeof fn == 'function';
15 | }
16 |
17 | function isEmptyObject (obj) {
18 | for(var name in obj){
19 | return false;
20 | }
21 | return true;
22 | }
23 |
24 | function extend(base, newprops) {
25 | for (var name in newprops) {
26 | base[name] = newprops[name];
27 | }
28 | return base;
29 | }
30 |
31 | /**
32 | * Create a runtime cache around retrieved values from the context.
33 | * This allows for dynamic (function) results to be kept the same for multiple
34 | * occuring expansions within one template.
35 | * Note: Uses key-value tupples to be able to cache null values as well.
36 | */
37 | //TODO move this into prep-processing
38 | function CachingContext(context) {
39 | this.raw = context;
40 | this.cache = {};
41 | }
42 | CachingContext.prototype.get = function(key) {
43 | var val = this.lookupRaw(key);
44 | var result = val;
45 |
46 | if (isFunction(val)) { // check function-result-cache
47 | var tupple = this.cache[key];
48 | if (tupple !== null && tupple !== undefined) {
49 | result = tupple.val;
50 | } else {
51 | result = val(this.raw);
52 | this.cache[key] = {key: key, val: result};
53 | // NOTE: by storing tupples we make sure a null return is validly consistent too in expansions
54 | }
55 | }
56 | return result;
57 | };
58 |
59 | CachingContext.prototype.lookupRaw = function(key) {
60 | return CachingContext.lookup(this, this.raw, key);
61 | };
62 |
63 | CachingContext.lookup = function(me, context, key) {
64 | var result = context[key];
65 | if (result !== undefined) {
66 | return result;
67 | } else {
68 | var keyparts = key.split('.');
69 | var i = 0, keysplits = keyparts.length - 1;
70 | for (i = 0; i 0 ? "=" : "") + val;
125 | }
126 |
127 | function addNamed(name, key, val, noName) {
128 | noName = noName || false;
129 | if (noName) { name = ""; }
130 |
131 | if (!key || key.length === 0) {
132 | key = name;
133 | }
134 | return key + (key.length > 0 ? "=" : "") + val;
135 | }
136 |
137 | function addLabeled(name, key, val, noName) {
138 | noName = noName || false;
139 | if (noName) { name = ""; }
140 |
141 | if (!key || key.length === 0) {
142 | key = name;
143 | }
144 | return key + (key.length > 0 && val ? "=" : "") + val;
145 | }
146 |
147 |
148 | var simpleConf = {
149 | prefix : "", joiner : ",", encode : encodeNormal, builder : addUnNamed
150 | };
151 | var reservedConf = {
152 | prefix : "", joiner : ",", encode : encodeReserved, builder : addUnNamed
153 | };
154 | var fragmentConf = {
155 | prefix : "#", joiner : ",", encode : encodeReserved, builder : addUnNamed
156 | };
157 | var pathParamConf = {
158 | prefix : ";", joiner : ";", encode : encodeNormal, builder : addLabeled
159 | };
160 | var formParamConf = {
161 | prefix : "?", joiner : "&", encode : encodeNormal, builder : addNamed
162 | };
163 | var formContinueConf = {
164 | prefix : "&", joiner : "&", encode : encodeNormal, builder : addNamed
165 | };
166 | var pathHierarchyConf = {
167 | prefix : "/", joiner : "/", encode : encodeNormal, builder : addUnNamed
168 | };
169 | var labelConf = {
170 | prefix : ".", joiner : ".", encode : encodeNormal, builder : addUnNamed
171 | };
172 |
173 |
174 | function Expression(conf, vars ) {
175 | extend(this, conf);
176 | this.vars = vars;
177 | }
178 |
179 | Expression.build = function(ops, vars) {
180 | var conf;
181 | switch(ops) {
182 | case '' : conf = simpleConf; break;
183 | case '+' : conf = reservedConf; break;
184 | case '#' : conf = fragmentConf; break;
185 | case ';' : conf = pathParamConf; break;
186 | case '?' : conf = formParamConf; break;
187 | case '&' : conf = formContinueConf; break;
188 | case '/' : conf = pathHierarchyConf; break;
189 | case '.' : conf = labelConf; break;
190 | default : throw "Unexpected operator: '"+ops+"'";
191 | }
192 | return new Expression(conf, vars);
193 | };
194 |
195 | Expression.prototype.expand = function(context) {
196 | var joiner = this.prefix;
197 | var nextjoiner = this.joiner;
198 | var buildSegment = this.builder;
199 | var res = "";
200 | var i = 0, cnt = this.vars.length;
201 |
202 | for (i = 0 ; i< cnt; i++) {
203 | var varspec = this.vars[i];
204 | varspec.addValues(context, this.encode, function(key, val, noName) {
205 | var segm = buildSegment(varspec.name, key, val, noName);
206 | if (segm !== null && segm !== undefined) {
207 | res += joiner + segm;
208 | joiner = nextjoiner;
209 | }
210 | });
211 | }
212 | return res;
213 | };
214 |
215 |
216 |
217 | var UNBOUND = {};
218 |
219 | /**
220 | * Helper class to help grow a string of (possibly encoded) parts until limit is reached
221 | */
222 | function Buffer(limit) {
223 | this.str = "";
224 | if (limit === UNBOUND) {
225 | this.appender = Buffer.UnboundAppend;
226 | } else {
227 | this.len = 0;
228 | this.limit = limit;
229 | this.appender = Buffer.BoundAppend;
230 | }
231 | }
232 |
233 | Buffer.prototype.append = function(part, encoder) {
234 | return this.appender(this, part, encoder);
235 | };
236 |
237 | Buffer.UnboundAppend = function(me, part, encoder) {
238 | part = encoder ? encoder(part) : part;
239 | me.str += part;
240 | return me;
241 | };
242 |
243 | Buffer.BoundAppend = function(me, part, encoder) {
244 | part = part.substring(0, me.limit - me.len);
245 | me.len += part.length;
246 |
247 | part = encoder ? encoder(part) : part;
248 | me.str += part;
249 | return me;
250 | };
251 |
252 |
253 | function arrayToString(arr, encoder, maxLength) {
254 | var buffer = new Buffer(maxLength);
255 | var joiner = "";
256 |
257 | var i = 0, cnt = arr.length;
258 | for (i=0; i makeHal(data)
43 | case None => makeHal(JsObject())
44 | }
45 |
46 | private[this] def makeHal(jsValue:JsValue):JsValue = jsValue match {
47 | case jsonObj:JsObject => addEmbedded(addLinks(jsonObj))
48 | case _ => jsValue
49 | }
50 |
51 | private[this] def addLinks(jsObject:JsObject):JsObject = combinedLinks match {
52 | case Some(links) => JsObject(jsObject.fields + ("_links" -> links.map {
53 | case (key, value:Link) =>
54 | (key, value.copy(href = curieHref(key, value)).toJson)
55 | case(key, value:Links) =>
56 | val links:Seq[Link] = value.links.map(v => v.copy(href = curieHref(key, v)))
57 | (key , links.toJson)
58 | case (key, value:Seq[Curie]) => (key, value.toJson)
59 | case (_,_) => throw new HalException("Failed to create Links. Invalid key/value supplied.")
60 | }.toJson))
61 | case _ => jsObject
62 | }
63 |
64 | private[this] def addEmbedded(jsObject:JsObject):JsObject = withEmbedded match {
65 | case Some(embedded) => JsObject(jsObject.fields + ("_embedded" -> embedded.toJson))
66 | case _ => jsObject
67 | }
68 |
69 | private[this] def combinedLinks: Option[Map[String, Any]] = {
70 | val combinedLinks = links ++ combinedCuries
71 | if (combinedLinks.isEmpty) None else Some(combinedLinks)
72 | }
73 |
74 | private[this] def links:Map[String, LinkT] = withLinks match {
75 | case Some(links) => links
76 | case _ => Map()
77 | }
78 |
79 | private[this] def combinedCuries:Map[String, Seq[Curie]] = {
80 | val curies:Seq[Curie] = globalCuries ++ addCuries
81 | if (curies.isEmpty) Map() else Map(("curies", curies))
82 | }
83 |
84 | private[this] def addCuries: Seq[Curie] = withCuries match {
85 | case Some(curies) => curies
86 | case _ => Seq[Curie]()
87 | }
88 |
89 | private[this] def globalCuries: Seq[Curie] = ResourceBuilder.globalCuries match {
90 | case Some(curies) => curies
91 | case _ => Seq[Curie]()
92 | }
93 |
94 | private[this] def curied(key: String) = key.contains(":")
95 |
96 | private[this] def curieHref(key: String, value: Link) = s"${if (!curied(key)) Href.make(withRequest) else ""}${value.href}"
97 | }
98 |
99 | /** Base trait for Link or a Collection of related Links
100 | */
101 | trait LinkT
102 |
103 | /** HAL Link
104 | *
105 | * @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5
106 | * @constructor Creates a HAL Link instance.
107 | * @param href URI to referenced resource.
108 | * @param templated Hint that the href is a Template URI.
109 | * @param type Hint for the media type of the referenced resource.
110 | * @param deprecation Hints that the Resource is deprecated. Value is a URI to more information on the deprecation.
111 | * @param name A secondary key for the resource.
112 | * @param profile URI to a profile of the resource conforming to https://tools.ietf.org/html/rfc6906.
113 | * @param title A string intended for labeling the resource.
114 | * @param hreflang A string representing the language of the resource.
115 | */
116 | case class Link(
117 | href:String,
118 | templated:Option[Boolean] = None,
119 | `type`:Option[String] = None,
120 | deprecation:Option[Boolean] = None,
121 | name:Option[String] = None,
122 | profile:Option[String] = None,
123 | title:Option[String] = None,
124 | hreflang:Option[String] = None
125 | ) extends LinkT
126 |
127 | /** Links collection for nesting links.
128 | *
129 | * @constructor creates a collection of Links.
130 | * @param links Seq of Links to use in builder.
131 | */
132 | case class Links(
133 | links:Seq[Link]
134 | ) extends LinkT
135 |
136 | /** Curie support class
137 | *
138 | * @constructor creates a curie instance
139 | * @param name Name of the curie.
140 | * @param href URI for curie.
141 | * @param templated Hint that curie is a Template URI. Default is true.
142 | */
143 | case class Curie(
144 | name:String,
145 | href:String,
146 | templated:Boolean = true
147 | )
148 | /** HAL specific exception
149 | *
150 | * @constructor creates a curie instance
151 | * @param msg Reason for failure.
152 | */
153 | class HalException(msg: String) extends Exception(msg)
--------------------------------------------------------------------------------
/src/main/scala/io/pileworx/akka/http/rest/hal/HalBrowserService.scala:
--------------------------------------------------------------------------------
1 | package io.pileworx.akka.http.rest.hal
2 |
3 | import akka.http.scaladsl.server.Directives._
4 | import akka.http.scaladsl.server.directives.ContentTypeResolver.Default
5 | import akka.http.scaladsl.server.Route
6 |
7 | /** akka-http Route to expose a HAL Browser
8 | * Mix in this trait with ~ halBrowserRoutes to expose a HAL Browser at /halbrowser
9 | */
10 | trait HalBrowserService {
11 | val halBrowser = "halbrowser"
12 | val halBrowserRoutes: Route =
13 | path(halBrowser) {
14 | getFromResource("dependencies/halbrowser/browser.html")
15 | } ~
16 | path(halBrowser / RemainingPath) { rpath =>
17 | getFromResource(s"dependencies/halbrowser/$rpath")
18 | }
19 | }
--------------------------------------------------------------------------------
/src/main/scala/io/pileworx/akka/http/rest/hal/Href.scala:
--------------------------------------------------------------------------------
1 | package io.pileworx.akka.http.rest.hal
2 |
3 | import akka.http.scaladsl.model.{HttpHeader, HttpRequest}
4 |
5 | /** Builds the href for links
6 | *
7 | * If the X-Forwarded headers are available it constructs the URI based on the headers
8 | * If the Request is available with out X-Forwarded the request URI parts are used.
9 | * If the Request is not available, no changes are made to the URI
10 | */
11 | object Href {
12 | /** Makes the href URI if optional HTTP Request is available
13 | *
14 | * @param maybeRequest Optional HTTP Request
15 | * @return An adjusted URI
16 | */
17 | def make(maybeRequest:Option[HttpRequest]):String = maybeRequest match {
18 | case Some(req) => if (containsForwarded(req)) ForwardedBuilder(req).build else UrlBuilder(req).build
19 | case None => ""
20 | }
21 |
22 | private[this] def containsForwarded(req:HttpRequest) = {
23 | req.headers.exists(xf => xf.name.contains("X-Forwarded"))
24 | }
25 | }
26 |
27 | object ForwardedBuilder {
28 | private val XForwardedProto = "X-Forwarded-Proto"
29 | private val XForwardedHost = "X-Forwarded-Host"
30 | private val XForwardedPort = "X-Forwarded-Port"
31 | private val XForwardedPrefix = "X-Forwarded-Prefix"
32 | private val portSeparator = ":"
33 | }
34 |
35 | /** Builder to construct URI from X-Forwarded headers
36 | *
37 | * @param req The current HTTP Request
38 | */
39 | case class ForwardedBuilder(req:HttpRequest) {
40 | private[this] val withProto:Option[String] = req.headers.collectFirst {
41 | case h:HttpHeader if h.name.equalsIgnoreCase(ForwardedBuilder.XForwardedProto) => h.value
42 | }
43 |
44 | private[this] val withHost:Option[String] = {
45 | val xForwarded = req.headers.collectFirst {
46 | case h:HttpHeader if h.name.equalsIgnoreCase(ForwardedBuilder.XForwardedHost) => stripPort(h.value)
47 | }
48 | val hostHeader = if (req.uri.authority.host.address.nonEmpty) Some(req.uri.authority.host.address) else None
49 | if (xForwarded.isInstanceOf[Some[String]]) xForwarded else hostHeader
50 | }
51 |
52 | private[this] val withPort:Option[String] = req.headers.collectFirst {
53 | case h:HttpHeader if h.name.equalsIgnoreCase(ForwardedBuilder.XForwardedPort) => h.value
54 | }
55 |
56 | private[this] val withPrefix:Option[String] = req.headers.collectFirst {
57 | case h:HttpHeader if h.name.equalsIgnoreCase(ForwardedBuilder.XForwardedPrefix) => h.value
58 | }
59 |
60 | /** Builds the URI
61 | *
62 | * @return The altered URI with X-Forwarded parts
63 | */
64 | def build: String = withProto match {
65 | case Some(xfp) => addHost(s"$xfp://")
66 | case _ => withHost match {
67 | case Some(_) => addHost("http://")
68 | case _ => addHost("")
69 | }
70 | }
71 |
72 | private[this] def addHost(protocol:String) = withHost match {
73 | case Some(xfh) => addPort(s"$protocol$xfh")
74 | case _ => addPort(protocol)
75 | }
76 |
77 | private[this] def addPort(host:String) = withPort match {
78 | case Some(xfp) => addPrefix(s"$host:$xfp")
79 | case _ => addPrefix(host)
80 | }
81 |
82 | private[this] def addPrefix(port:String) = withPrefix match {
83 | case Some(xfp) => s"$port/$xfp"
84 | case _ => port
85 | }
86 |
87 | private[this] def stripPort(hostname:String) = {
88 | if(hostname.contains(ForwardedBuilder.portSeparator))
89 | hostname.split(ForwardedBuilder.portSeparator)(0)
90 | else
91 | hostname
92 | }
93 | }
94 |
95 | /** Builder to construct URI from HTTP Request URI parts
96 | *
97 | * @param req The current HTTP Request
98 | */
99 | case class UrlBuilder(req:HttpRequest) {
100 | private[this] val proto:String = req.uri.scheme
101 | private[this] val host:String = req.uri.authority.host.address
102 | private[this] val port:Int = req.uri.authority.port
103 |
104 | /** Builds the URI
105 | *
106 | * @return The altered URI with HTTP Request URI parts
107 | */
108 | def build: String = s"$proto://$host:$port"
109 | }
--------------------------------------------------------------------------------
/src/main/scala/io/pileworx/akka/http/rest/hal/Relations.scala:
--------------------------------------------------------------------------------
1 | package io.pileworx.akka.http.rest.hal
2 |
3 | object Relations {
4 | /**
5 | * @see { @link https://tools.ietf.org/html/rfc6903}
6 | */
7 | val About: String = "about"
8 |
9 | /**
10 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-alternate}
11 | */
12 | val Alternate: String = "alternate"
13 |
14 | /**
15 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
16 | */
17 | val Appendix: String = "appendix"
18 |
19 | /**
20 | * @see { @link https://www.w3.org/TR/2011/WD-html5-20110113/links.html#rel-archives}
21 | */
22 | val Archives: String = "archives"
23 |
24 | /**
25 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-author}
26 | */
27 | val Author: String = "author"
28 |
29 | /**
30 | * @see { @link https://tools.ietf.org/html/rfc7725}
31 | */
32 | val BlockedBy: String = "blocked-by"
33 |
34 | /**
35 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-bookmark}
36 | */
37 | val Bookmark: String = "bookmark"
38 |
39 | /**
40 | * @see { @link https://tools.ietf.org/html/rfc6596}
41 | */
42 | val Canonical: String = "canonical"
43 |
44 | /**
45 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
46 | */
47 | val Chapter: String = "chapter"
48 |
49 | /**
50 | * @see { @link https://datatracker.ietf.org/doc/draft-vandesompel-citeas/}
51 | */
52 | val CiteAs: String = "cite-as"
53 |
54 | /**
55 | * @see { @link https://tools.ietf.org/html/rfc6573}
56 | */
57 | val Collection: String = "collection"
58 |
59 | /**
60 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
61 | */
62 | val CONTENTS: String = "contents"
63 |
64 | /**
65 | * @see { @link https://tools.ietf.org/html/rfc7991}
66 | */
67 | val ConvertedFrom: String = "convertedFrom"
68 |
69 | /**
70 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
71 | */
72 | val Copyright: String = "copyright"
73 |
74 | /**
75 | * @see { @link https://tools.ietf.org/html/rfc6861}
76 | */
77 | val CreateForm: String = "create-form"
78 |
79 | /**
80 | * @see { @link https://tools.ietf.org/html/rfc5005}
81 | */
82 | val Current: String = "current"
83 |
84 | /**
85 | * @see { @link https://www.w3.org/TR/powder-dr/#assoc-linking}
86 | */
87 | val DescribedBy: String = "describedBy"
88 |
89 | /**
90 | * @see { @link https://tools.ietf.org/html/rfc6892}
91 | */
92 | val Describes: String = "describes"
93 |
94 | /**
95 | * @see { @link https://tools.ietf.org/html/rfc6579}
96 | */
97 | val Disclosure: String = "disclosure"
98 |
99 | /**
100 | * @see { @link https://www.w3.org/TR/resource-hints/}
101 | */
102 | val DnsPrefetch: String = "dns-prefetch"
103 |
104 | /**
105 | * @see { @link https://tools.ietf.org/html/rfc6249}
106 | */
107 | val Duplicate: String = "duplicate"
108 |
109 | /**
110 | * @see { @link https://tools.ietf.org/html/rfc5023}
111 | */
112 | val Edit: String = "edit"
113 |
114 | /**
115 | * @see { @link https://tools.ietf.org/html/rfc6861}
116 | */
117 | val EditForm: String = "edit-form"
118 |
119 | /**
120 | * @see { @link https://tools.ietf.org/html/rfc5023}
121 | */
122 | val EditMedia: String = "edit-media"
123 |
124 | /**
125 | * @see { @link https://tools.ietf.org/html/rfc4287}
126 | */
127 | val Enclosure: String = "enclosure"
128 |
129 | /**
130 | * @see { @link https://tools.ietf.org/html/rfc8288}
131 | */
132 | val First: String = "first"
133 |
134 | /**
135 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
136 | */
137 | val Glossary: String = "glossary"
138 |
139 | /**
140 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-help}
141 | */
142 | val Help: String = "help"
143 |
144 | /**
145 | * @see { @link https://tools.ietf.org/html/rfc6690}
146 | */
147 | val Hosts: String = "hosts"
148 |
149 | /**
150 | * @see { @link https://pubsubhubbub.googlecode.com}
151 | */
152 | val Hub: String = "hub"
153 |
154 | /**
155 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-icon}
156 | */
157 | val Icon: String = "icon"
158 |
159 | /**
160 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
161 | */
162 | val Index: String = "index"
163 |
164 | /**
165 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalAfter section 4.2.21}
166 | */
167 | val IntervalAfter: String = "intervalAfter"
168 |
169 | /**
170 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalBefore section 4.2.22}
171 | */
172 | val IntervalBefore: String = "intervalBefore"
173 |
174 | /**
175 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalContains section 4.2.23}
176 | */
177 | val IntervalContains: String = "intervalContains"
178 |
179 | /**
180 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalDisjoint section 4.2.24}
181 | */
182 | val IntervalDisjoint: String = "intervalDisjoint"
183 |
184 | /**
185 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalDuring section 4.2.25}
186 | */
187 | val IntervalDuring: String = "intervalDuring"
188 |
189 | /**
190 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalEquals section 4.2.26}
191 | */
192 | val IntervalEquals: String = "intervalEquals"
193 |
194 | /**
195 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalFinishedBy section 4.2.27}
196 | */
197 | val IntervalFinishedBy: String = "intervalFinishedBy"
198 |
199 | /**
200 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalFinishes section 4.2.28}
201 | */
202 | val IntervalFinishes: String = "intervalFinishes"
203 |
204 | /**
205 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalIn section 4.2.29}
206 | */
207 | val IntervalIn: String = "intervalIn"
208 |
209 | /**
210 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalMeets section 4.2.30}
211 | */
212 | val IntervalMeets: String = "intervalMeets"
213 |
214 | /**
215 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalMetBy section 4.2.31}
216 | */
217 | val IntervalMetBy: String = "intervalMetBy"
218 |
219 | /**
220 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalOverlappedBy section 4.2.32}
221 | */
222 | val IntervalOverlappedBy: String = "intervalOverlappedBy"
223 |
224 | /**
225 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalOverlaps section 4.2.33}
226 | */
227 | val IntervalOverlaps: String = "intervalOverlaps"
228 |
229 | /**
230 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalStartedBy section 4.2.34}
231 | */
232 | val IntervalStartedBy: String = "intervalStartedBy"
233 |
234 | /**
235 | * @see { @link https://www.w3.org/TR/owl-time/#time:intervalStarts section 4.2.35}
236 | */
237 | val IntervalStarts: String = "intervalStarts"
238 |
239 | /**
240 | * @see { @link https://tools.ietf.org/html/rfc6573}
241 | */
242 | val Item: String = "item"
243 |
244 | /**
245 | * @see { @link https://tools.ietf.org/html/rfc8288}
246 | */
247 | val Last: String = "last"
248 |
249 | /**
250 | * @see { @link https://tools.ietf.org/html/rfc5829}
251 | */
252 | val LatestVersion: String = "latest-version"
253 |
254 | /**
255 | * @see { @link https://tools.ietf.org/html/rfc4946}
256 | */
257 | val License: String = "license"
258 |
259 | /**
260 | * @see { @link https://tools.ietf.org/html/rfc6415}
261 | */
262 | val Lrdd: String = "lrdd"
263 |
264 | /**
265 | * @see { @link https://tools.ietf.org/html/rfc7089}
266 | */
267 | val Memento: String = "memento"
268 |
269 | /**
270 | * @see { @link https://tools.ietf.org/html/rfc5989}
271 | */
272 | val Monitor: String = "monitor"
273 |
274 | /**
275 | * @see { @link https://tools.ietf.org/html/rfc5989}
276 | */
277 | val MonitorGroup: String = "monitor-group"
278 |
279 | /**
280 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-next}
281 | */
282 | val Next: String = "next"
283 |
284 | /**
285 | * @see { @link https://tools.ietf.org/html/rfc5005}
286 | */
287 | val NextArchive: String = "next-archive"
288 |
289 | /**
290 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-nofollow}
291 | */
292 | val NOFOLLOW: String = "nofollow"
293 |
294 | /**
295 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-noreferrer}
296 | */
297 | val NoReferrer: String = "noreferrer"
298 |
299 | /**
300 | * @see { @link https://tools.ietf.org/html/rfc7089}
301 | */
302 | val Original: String = "original"
303 |
304 | /**
305 | * @see { @link https://tools.ietf.org/html/rfc8288}
306 | */
307 | val Payment: String = "payment"
308 |
309 | /**
310 | * @see { @link https://www.hixie.ch/specs/pingback/pingback}
311 | */
312 | val PingBack: String = "pingback"
313 |
314 | /**
315 | * @see { @link https://www.w3.org/TR/resource-hints/}
316 | */
317 | val PreConnect: String = "preconnect"
318 |
319 | /**
320 | * @see { @link https://tools.ietf.org/html/rfc5829}
321 | */
322 | val PredecessorVersion: String = "predecessor-version"
323 |
324 | /**
325 | * @see { @link https://www.w3.org/TR/resource-hints/}
326 | */
327 | val Prefetch: String = "prefetch"
328 |
329 | /**
330 | * @see { @link https://www.w3.org/TR/preload/}
331 | */
332 | val Preload: String = "preload"
333 |
334 | /**
335 | * @see { @link https://www.w3.org/TR/resource-hints/}
336 | */
337 | val PreRender: String = "prerender"
338 |
339 | /**
340 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-prev}
341 | */
342 | val Prev: String = "prev"
343 |
344 | /**
345 | * @see { @link https://tools.ietf.org/html/rfc6903, section 3}
346 | */
347 | val Preview: String = "preview"
348 |
349 | /**
350 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
351 | */
352 | val Previous: String = "previous"
353 |
354 | /**
355 | * @see { @link https://tools.ietf.org/html/rfc5005}
356 | */
357 | val PrevArchive: String = "prev-archive"
358 |
359 | /**
360 | * @see { @link https://tools.ietf.org/html/rfc6903, section 4}
361 | */
362 | val PrivacyPolicy: String = "privacy-policy"
363 |
364 | /**
365 | * @see { @link https://tools.ietf.org/html/rfc6906}
366 | */
367 | val Profile: String = "profile"
368 |
369 | /**
370 | * @see { @link https://tools.ietf.org/html/rfc4287}
371 | */
372 | val Related: String = "related"
373 |
374 | /**
375 | * @see { @link https://tools.ietf.org/html/rfc8040}
376 | */
377 | val RestConf: String = "restconf"
378 |
379 | /**
380 | * @see { @link https://tools.ietf.org/html/rfc4685}
381 | */
382 | val Replies: String = "replies"
383 |
384 | /**
385 | * @see { @link http://www.opensearch.org/Specifications/OpenSearch/1.1}
386 | */
387 | val Search: String = "search"
388 |
389 | /**
390 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
391 | */
392 | val Section: String = "section"
393 |
394 | /**
395 | * @see { @link https://tools.ietf.org/html/rfc4287}
396 | */
397 | val Self: String = "self"
398 |
399 | /**
400 | * @see { @link https://tools.ietf.org/html/rfc5023}
401 | */
402 | val Service: String = "service"
403 |
404 | /**
405 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
406 | */
407 | val Start: String = "start"
408 |
409 | /**
410 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-stylesheet}
411 | */
412 | val Stylesheet: String = "stylesheet"
413 |
414 | /**
415 | * @see { @link https://www.w3.org/TR/1999/REC-html401-19991224}
416 | */
417 | val Subsection: String = "subsection"
418 |
419 | /**
420 | * @see { @link https://tools.ietf.org/html/rfc5829}
421 | */
422 | val SuccessorVersions: String = "successor-versions"
423 |
424 | /**
425 | * @see { @link https://www.w3.org/TR/html5/links.html#link-type-tag}
426 | */
427 | val tag: String = "tag"
428 |
429 | /**
430 | * @see { @link https://tools.ietf.org/html/rfc6903, section 5}
431 | */
432 | val TermsOfService: String = "terms-of-service"
433 |
434 | /**
435 | * @see { @link https://tools.ietf.org/html/rfc7089}
436 | */
437 | val TimeGate: String = "timegate"
438 |
439 | /**
440 | * @see { @link https://tools.ietf.org/html/rfc7089}
441 | */
442 | val TimeMap: String = "timemap"
443 |
444 | /**
445 | * @see { @link https://tools.ietf.org/html/rfc6903, section 6}
446 | */
447 | val Type: String = "type"
448 |
449 | /**
450 | * @see { @link https://tools.ietf.org/html/rfc8288}
451 | */
452 | val Up: String = "up"
453 |
454 | /**
455 | * @see { @link https://tools.ietf.org/html/rfc5829}
456 | */
457 | val VersionHistory: String = "version-history"
458 |
459 | /**
460 | * @see { @link https://tools.ietf.org/html/rfc4287}
461 | */
462 | val Via: String = "via"
463 |
464 | /**
465 | * @see { @link https://www.w3.org/TR/webmention/}
466 | */
467 | val WebMention: String = "webmention"
468 |
469 | /**
470 | * @see { @link https://tools.ietf.org/html/rfc5829}
471 | */
472 | val WorkingCopy: String = "working-copy"
473 |
474 | /**
475 | * @see { @link https://tools.ietf.org/html/rfc5829}
476 | */
477 | val WorkingCopyOf: String = "working-copy-of"
478 | }
--------------------------------------------------------------------------------
/src/test/scala/io/pileworx/akka/http/rest/hal/HalBrowserServiceSpec.scala:
--------------------------------------------------------------------------------
1 | package io.pileworx.akka.http.rest.hal
2 |
3 | import org.scalatest.{ Matchers, WordSpec }
4 | import akka.http.scaladsl.testkit.ScalatestRouteTest
5 |
6 | class HalBrowserServiceSpec extends WordSpec with Matchers with ScalatestRouteTest with HalBrowserService {
7 |
8 | "The HAL Browser service" should {
9 |
10 | "return the landing page from /halbrowser" in {
11 | Get("/halbrowser") ~> halBrowserRoutes ~> check {
12 | responseAs[String] should not be empty
13 | }
14 | }
15 |
16 | "return resources from correct dynamic subpath of /halbrowser/..." in {
17 | Get("/halbrowser/js/hal.js") ~> halBrowserRoutes ~> check {
18 | responseAs[String] should include("var HAL = {")
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/scala/io/pileworx/akka/http/rest/hal/HalSpec.scala:
--------------------------------------------------------------------------------
1 | package io.pileworx.akka.http.rest.hal
2 |
3 | import org.scalatest.{Matchers, WordSpec}
4 | import spray.json._
5 | import io.pileworx.akka.http.rest.hal.Relations._
6 |
7 | trait FakeDataProtocol extends DefaultJsonProtocol {
8 | implicit val fakeDataFormat: RootJsonFormat[FakeData] = jsonFormat2(FakeData)
9 | }
10 |
11 | class HalSpec extends WordSpec with Matchers with FakeDataProtocol {
12 |
13 | val curieKey = "pw"
14 | val curieUrl = "http://pileworx.io/{rel}"
15 | val url = "http://www.test.com"
16 | val data: JsValue = FakeData("one","two").toJson
17 | val links:Map[String, LinkT] = Map(
18 | Self -> Link(href = url),
19 | Up -> Link(href = url)
20 | )
21 | val linksNested:Map[String, LinkT] = Map(
22 | "link_nested" -> Links(Seq(Link(href = url, title = Some("one")),Link(href = url, title = Some("two"))))
23 | )
24 | val embedded: Map[String, Seq[JsValue]] = Map(
25 | "fakesOne" -> Seq(FakeData("one","two").toJson),
26 | "fakesTwo" -> Seq(FakeData("three","four").toJson)
27 | )
28 |
29 | "Resource Builder" should {
30 |
31 | "return a resource with a provided link" in {
32 | val result = ResourceBuilder(
33 | withLinks = Some(links)).build.toString
34 |
35 | result should include(url)
36 | result should include(Self)
37 | result should include(Up)
38 | result should include("_links")
39 | }
40 |
41 | "return a resource with a provided nested links" in {
42 | val result = ResourceBuilder(
43 | withLinks = Some(linksNested)).build.toString
44 |
45 | result should include(url)
46 | result should include("one")
47 | result should include("two")
48 | result should include("_links")
49 | }
50 |
51 | "return a resource with a provided embedded" in {
52 | val result = ResourceBuilder(
53 | withEmbedded = Some(embedded)).build.toString
54 |
55 | result should include("fakesOne")
56 | result should include("fakesTwo")
57 | result should include("one")
58 | result should include("two")
59 | result should include("three")
60 | result should include("four")
61 | result should include("_embedded")
62 | }
63 |
64 | "return a resource with the provided data" in {
65 | val result = ResourceBuilder(
66 | withData = Some(data)).build.toString
67 |
68 | result should include("one")
69 | result should include("two")
70 | }
71 |
72 | "return a resource with the _links property if no links are provided" in {
73 | val result = ResourceBuilder(
74 | withData = Some(data)).build.toString
75 |
76 | result should not include "_links"
77 | }
78 |
79 | "return a resource with the _embedded property if no embedded objects are provided" in {
80 | val result = ResourceBuilder(
81 | withData = Some(data)).build.toString
82 |
83 | result should not include "_embedded"
84 | }
85 |
86 | "return a resource without optional _link properties if unused" in {
87 | val result = ResourceBuilder(
88 | withLinks = links(Link(href = url))).build.toString
89 |
90 | result should not include "templated"
91 | result should not include "type"
92 | result should not include "deprecation"
93 | result should not include "name"
94 | result should not include "profile"
95 | result should not include "title"
96 | result should not include "hreflang"
97 | }
98 |
99 | "return a resource with optional _link property templated if used" in {
100 | val result = ResourceBuilder(
101 | withLinks = links(Link(
102 | href = url,
103 | templated = Some(true)
104 | ))).build.toString
105 |
106 | result should include("templated")
107 | }
108 |
109 | "return a resource with optional _link property type if used" in {
110 | val result = ResourceBuilder(
111 | withLinks = links(Link(
112 | href = url,
113 | `type` = Some("mything")
114 | ))).build.toString
115 |
116 | result should include("type")
117 | }
118 |
119 | "return a resource with optional _link property deprecation if used" in {
120 | val result = ResourceBuilder(
121 | withLinks = links(Link(
122 | href = url,
123 | deprecation = Some(true)
124 | ))).build.toString
125 |
126 | result should include("deprecation")
127 | }
128 |
129 | "return a resource with optional _link property name if used" in {
130 | val result = ResourceBuilder(
131 | withLinks = links(Link(
132 | href = url,
133 | name = Some("thisisme")
134 | ))).build.toString
135 |
136 | result should include("name")
137 | }
138 |
139 | "return a resource with optional _link property profile if used" in {
140 | val result = ResourceBuilder(
141 | withLinks = links(Link(
142 | href = url,
143 | profile = Some("alps")
144 | ))).build.toString
145 |
146 | result should include("profile")
147 | }
148 |
149 | "return a resource with optional _link property title if used" in {
150 | val result = ResourceBuilder(
151 | withLinks = links(Link(
152 | href = url,
153 | title = Some("my thing")
154 | ))).build.toString
155 |
156 | result should include("title")
157 | }
158 |
159 | "return a resource with optional _link property hreflang if used" in {
160 | val result = ResourceBuilder(
161 | withLinks = links(Link(
162 | href = url,
163 | hreflang = Some("en-us")
164 | ))).build.toString
165 |
166 | result should include("hreflang")
167 | }
168 |
169 | "return curies in links if curies are provided" in {
170 | val result = ResourceBuilder(
171 | withCuries = Some(Seq(Curie(
172 | name = curieKey,
173 | href = curieUrl
174 | )))).build.toString
175 |
176 | result should include("_links")
177 | result should include("curies")
178 | result should include(curieKey)
179 | result should include(curieUrl)
180 | }
181 |
182 | "return curies in links if curies are provided globally" in {
183 | val gCurieKey = "ak"
184 | val gCurieUrl = "http://akka.io/{rel}"
185 | ResourceBuilder.curies(Seq(Curie(name = gCurieKey, href = gCurieUrl)))
186 |
187 | val result = ResourceBuilder().build.toString
188 |
189 | result should include("_links")
190 | result should include("curies")
191 | result should include(gCurieKey)
192 | result should include(gCurieUrl)
193 | }
194 |
195 | "return curies in links if curies are provided globally and locally" in {
196 | val gCurieKey = "ts"
197 | val gCurieUrl = "http://typesafe.io/{rel}"
198 | ResourceBuilder.curies(Seq(Curie(name = gCurieKey, href = gCurieUrl)))
199 |
200 | val result = ResourceBuilder(
201 | withCuries = Some(Seq(Curie(
202 | name = curieKey,
203 | href = curieUrl
204 | )))).build.toString
205 |
206 | result should include("_links")
207 | result should include("curies")
208 | result should include(gCurieKey)
209 | result should include(gCurieUrl)
210 | result should include(curieKey)
211 | result should include(curieUrl)
212 | }
213 |
214 | "return links containing curies" in {
215 | val result = ResourceBuilder(
216 | withCuries = Some(Seq(Curie(
217 | name = curieKey,
218 | href = curieUrl
219 | ))),
220 | withLinks = linksWithCurie(Link(
221 | href = url
222 | ))).build.toString
223 |
224 | result should include(""""http://www.test.com"""")
225 | }
226 | }
227 |
228 | def links(link:Link):Option[Map[String,Link]] = Some(Map("self" -> link))
229 | def linksWithCurie(link:Link):Option[Map[String,Link]] = Some(Map(curieKey + ":find" -> link))
230 | }
231 |
232 | case class FakeData(title:String, description:String)
233 |
--------------------------------------------------------------------------------
/src/test/scala/io/pileworx/akka/http/rest/hal/HrefSpec.scala:
--------------------------------------------------------------------------------
1 | package io.pileworx.akka.http.rest.hal
2 |
3 | import akka.http.scaladsl.model.headers.RawHeader
4 | import akka.http.scaladsl.model.{HttpHeader, HttpRequest, Uri}
5 | import org.scalatest.{Matchers, WordSpec}
6 |
7 | import scala.collection.immutable.Seq
8 |
9 |
10 | class HrefSpec extends WordSpec with Matchers {
11 | "Href make with X-Forwarded headers" should {
12 | "return a url containing the http protocol and X-Forwarded-Host" in {
13 | val result = Href.make(forwardedWithHost)
14 | result should be(s"http://$xfHostName")
15 | }
16 |
17 | "return a url containing the X-Forwarded-Proto and X-Forwarded-Host" in {
18 | val result = Href.make(forwardedWithHostAndProto)
19 | result should be(s"$xfProtocol://$xfHostName")
20 | }
21 |
22 | "return a url containing the X-Forwarded-Host and X-Forwarded-Port" in {
23 | val result = Href.make(forwardedWithHostAndPort)
24 | result should be(s"http://$xfHostName:$xfPortNumber")
25 | }
26 |
27 | "return a url containing the X-Forwarded-Host and X-Forwarded-Prefix" in {
28 | val result = Href.make(forwardedWithHostAndPrefix)
29 | result should be(s"http://$xfHostName/$xfPathPrefix")
30 | }
31 |
32 | "return a url containing the X-Forwarded-Prefix and not contain a host if none is available" in {
33 | val result = Href.make(forwardedWithPrefix)
34 | result should be(s"/$xfPathPrefix")
35 | }
36 |
37 | "return a url containing the uri host and X-Forwarded-Port" in {
38 | val result = Href.make(requestWithUriAndForwardedPort)
39 | result should be(s"$uriProto://$uriHost:$xfPortNumber")
40 | }
41 |
42 | "return a url containing the uri host and X-Forwarded-Port without a second port" in {
43 | val result = Href.make(forwardedWithHostPortAndPort)
44 | result should be(s"$uriProto://$xfHostName:$xfPortNumber")
45 | }
46 | }
47 |
48 | "Href make with no request" should {
49 | "return as empty string" in {
50 | val result = Href.make(None)
51 | result shouldBe empty
52 | }
53 | }
54 |
55 | "Href make Uri in request" should {
56 | "return a url containing the protocol, host, and port" in {
57 | val result = Href.make(requestWithUri)
58 | result should be(s"$uriProto://$uriHost:$uriPort")
59 | }
60 | }
61 |
62 | private def requestWithUriAndForwardedPort = {
63 | Some(HttpRequest.apply(uri = uri, headers = Seq(xfPort)))
64 | }
65 |
66 | private def requestWithUri = Some(HttpRequest.apply(uri = uri))
67 |
68 | private def forwardedWithHost = {
69 | val headers: Seq[HttpHeader] = Seq(xfHost)
70 | Some(HttpRequest.apply(uri = Uri./, headers = headers))
71 | }
72 |
73 | private def forwardedWithHostAndProto = {
74 | val headers: Seq[HttpHeader] = Seq(xfHost, xfProto)
75 | Some(HttpRequest.apply(uri = Uri./, headers = headers))
76 | }
77 |
78 | private def forwardedWithHostAndPort = {
79 | val headers: Seq[HttpHeader] = Seq(xfHost, xfPort)
80 | Some(HttpRequest.apply(uri = Uri./, headers = headers))
81 | }
82 |
83 | private def forwardedWithHostPortAndPort = {
84 | val headers: Seq[HttpHeader] = Seq(xfHostWithPort, xfPort)
85 | Some(HttpRequest.apply(uri = Uri./, headers = headers))
86 | }
87 |
88 | private def forwardedWithHostAndPrefix = {
89 | val headers: Seq[HttpHeader] = Seq(xfHost, xfPrefix)
90 | Some(HttpRequest.apply(uri = Uri./, headers = headers))
91 | }
92 |
93 | private def forwardedWithPrefix = {
94 | val headers: Seq[HttpHeader] = Seq(xfPrefix)
95 | Some(HttpRequest.apply(uri = Uri./, headers = headers))
96 | }
97 |
98 | private val xfHostName = "www.myhost.com"
99 | private val xfHost = RawHeader("X-Forwarded-Host", xfHostName)
100 |
101 | private val xfHostNameWithPort = "www.myhost.com:7900"
102 | private val xfHostWithPort = RawHeader("X-Forwarded-Host", xfHostNameWithPort)
103 |
104 | private val xfPortNumber = "9000"
105 | private val xfPort = RawHeader("X-Forwarded-Port", xfPortNumber)
106 |
107 | private val xfProtocol = "https"
108 | private val xfProto = RawHeader("X-Forwarded-Proto", xfProtocol)
109 |
110 | private val xfPathPrefix = "my/prefix"
111 | private val xfPrefix = RawHeader("X-Forwarded-Prefix", xfPathPrefix)
112 |
113 | private val uriHost = "api.myhost.com"
114 | private val uriPort = "9001"
115 | private val uriProto = "http"
116 | private val uriPrefix = "prefix"
117 | private val uri = Uri(s"$uriProto://$uriHost:$uriPort/$uriPrefix")
118 | }
119 |
--------------------------------------------------------------------------------