hyper

Available on ForgeBox Tested With TestBox

Compatible with ColdFusion 11 Compatible with ColdFusion 2016 Compatible with Lucee 5

Master Branch Build Status

A CFML HTTP Builder

Inspiration

Hyper was built after coding several API SDK's for various platforms — S3SDK, cbstripe, and cbgithub, to name a few. I noticed that I spent a lot of time setting up the plumbing for the requests and a wrapper around cfhttp. Each implementation was mostly the same but slightly different. It was additionally frustrating because I really only needed to tweak a few values, usually just the Authorization header. It would be nice to create an HTTP client pre-configured for each of these SDK's. It seemed the perfect fit for a module.

The problem it solves

Hyper exists to provide a fluent builder experience for HTTP requests and responses. It also provides a powerful way to create clients, Bulider objects with pre-configured defaults like a base URL or certain headers.

Requirements

Hyper runs on Adobe ColdFusion 11+ and Lucee 5+.

ColdBox is not required, but mappings are provided for ColdBox users automatically.

HyperBuilder

The component you will most likely inject is the HyperBuilder. This is commonly aliased as hyper.

component {
    property name="hyper" inject="HyperBuilder@Hyper";
}

The HyperBuilder creates new requests. This can be done in one of two ways:

  1. Calling the new method will create a new request with the configured defaults.
  2. Calling any method on HyperRequest on the HyperBuilder instance will create a new request and forward on the method call.

Using the HyperBuilder lets you easily create requests with defaults while also avoiding having to deal with providers directly.

HyperRequest

Though the HyperBuilder is the component you will most likely inject, HyperRequest is the component will you interact with the most. HyperRequest provides a fluent interface to configure your HTTP call.

Example:

hyper.get( "https//api.github.com/users" );

hyper.setMethod( "PUT" )
    .withHeaders( { "Authorization" = "Bearer #token#" } )
    .setUrl( "https://jsonplaceholder.typicode.com/posts/1" )
    .setBody( {
        title: "New Title"
    } )
    .send();

Request Execution

get

Execute a GET request.

NameTypeRequiredDefaultDescription
urlstringfalsenullAn optional URL to set for the request.
queryParamsstructfalsenullAn optional struct of query parameters to set for the request.
post

Execute a POST request.

NameTypeRequiredDefaultDescription
urlstringfalsenullAn optional URL to set for the request.
bodystructfalsenullAn optional body to set for the request.
put

Execute a PUT request.

NameTypeRequiredDefaultDescription
urlstringfalsenullAn optional URL to set for the request.
bodystructfalsenullAn optional body to set for the request.
patch

Execute a PATCH request.

NameTypeRequiredDefaultDescription
urlstringfalsenullAn optional URL to set for the request.
bodystructfalsenullAn optional body to set for the request.
delete

Execute a DELETE request.

NameTypeRequiredDefaultDescription
urlstringfalsenullAn optional URL to set for the request.
bodystructfalsenullAn optional body to set for the request.
send

Send the HTTP request and return a HyperResponse.

NameTypeRequiredDefaultDescription
No arguments

Request Properties

getBaseURL

Gets the base URL for the request.

NameTypeRequiredDefaultDescription
No arguments
setBaseURL

Sets the base URL for the request.

NameTypeRequiredDefaultDescription
valuestringtrueThe base URL for the request, e.g. https://api.github.com/.
getURL

Gets the URL for the request.

NameTypeRequiredDefaultDescription
No arguments
setURL

Sets the URL for the request.

NameTypeRequiredDefaultDescription
valuestringtrueThe URL for the request. It can either be a full url or a URI resource for use with the baseURL. e.g. /repos.
getMethod

Gets the HTTP method for the request.

NameTypeRequiredDefaultDescription
No arguments
setMethod

Sets the HTTP method for the request.

NameTypeRequiredDefaultDescription
valuestringtrueThe HTTP method for the request.
withBasicAuth

Sets the username and password for HTTP Basic Auth.

NameTypeRequiredDefaultDescription
usernamestringtrueThe username for the basic auth.
passwordstringtrueThe password for the basic auth.
getUsername

Gets the username for the request.

NameTypeRequiredDefaultDescription
No arguments
setUsername

Sets the username for the request.

NameTypeRequiredDefaultDescription
valuestringtrueThe username for the request.
getPassword

Gets the password for the request.

NameTypeRequiredDefaultDescription
No arguments
setPassword

Sets the password for the request.

NameTypeRequiredDefaultDescription
valuestringtrueThe password for the request.
getTimeout

Gets the timeout for the request.

NameTypeRequiredDefaultDescription
No arguments
setTimeout

Sets the timeout for the request.

NameTypeRequiredDefaultDescription
valuestringtrueThe timeout for the request.
withoutRedirecting

A convenience method to not follow any redirects.

NameTypeRequiredDefaultDescription
No arguments
getMaximumRedirects

Gets the maximum number of redirects to follow.

NameTypeRequiredDefaultDescription
No arguments
setMaximumRedirects

Sets the maximum number of redirects to follow. A value of * will follow redirects infinitely.

NameTypeRequiredDefaultDescription
valueanytrueThe maximum number of redirects to follow.
getBody

Gets the body for the request.

NameTypeRequiredDefaultDescription
No arguments
setBody

Sets the body for the request. Complex values will be serialized before sending the request.

NameTypeRequiredDefaultDescription
valueanytrueThe body for the request.
getBodyFormat

Gets the body format for the request.

NameTypeRequiredDefaultDescription
No arguments
setBodyFormat

Sets the body format for the request.

NameTypeRequiredDefaultDescription
valueanytrueThe body format for the request.
asJson

A convenience method to set the body format and Content-Type to json.

NameTypeRequiredDefaultDescription
No arguments
asFormFields

A convenience method to set the body format and Content-Type to form fields.

NameTypeRequiredDefaultDescription
No arguments
getReferrer

Gets the referrer for the request, if any.

NameTypeRequiredDefaultDescription
No arguments
getHeaders

Gets the headers for the request.

NameTypeRequiredDefaultDescription
No arguments
setHeaders

Sets the headers for the request.

NameTypeRequiredDefaultDescription
valuestructtrueThe headers for the request.
setHeader

Set a header for the request.

NameTypeRequiredDefaultDescription
namestringtrueThe name of the header.
valuestringtrueThe value of the header.
withHeaders

Add additional headers to the request.

NameTypeRequiredDefaultDescription
headersstructtrueA struct of headers to add to the request.
hasHeader

Check if the request has a header with the given name.

NameTypeRequiredDefaultDescription
namestringtrueThe name of the header to check.
setContentType

A convenience method to set the Content-Type header.

NameTypeRequiredDefaultDescription
typestringtrueThe Content-Type value for the request
setAccept

A convenience method to set the Accept header.

NameTypeRequiredDefaultDescription
typestringtrueThe Accept value for the request
getQueryParams

Gets the query parameters for the request.

NameTypeRequiredDefaultDescription
No arguments
setQueryParams

Sets the query parameters for the request.

NameTypeRequiredDefaultDescription
valuestructtrueThe query parameters for the request.
setQueryParam

Set a query parameter for the request.

NameTypeRequiredDefaultDescription
namestringtrueThe name of the query parameter.
valuestringtrueThe value of the query parameter.
withQueryParams

Add additional query parameters to the request.

NameTypeRequiredDefaultDescription
queryParamsstructtrueA struct of query parameters to add to the request.
hasQueryParam

Check if the request has a query parameter with the given name.

NameTypeRequiredDefaultDescription
namestringtrueThe name of the query parameter to check.
setThrowOnError

Sets the throw on error property for the request. If true, error codes and status will be turned in to exceptions.

NameTypeRequiredDefaultDescription
valuebooleantrueThe value of the throw on error flag.
throwErrors

A convenience method to throw on errors.

NameTypeRequiredDefaultDescription
No arguments
allowErrors

A convenience method to not throw on errors.

NameTypeRequiredDefaultDescription
No arguments
setProperties

Quickly set many request properties using a struct. The key should be the name of one of the properties on the request, e.g. url, headers, method, body.

NameTypeRequiredDefaultDescription
propertiesstructtrueA struct of properties to set. Each property name will be set on the request. Properties that don't exist on the request will throw an error.

HyperResponse

The HyperResponse component is a read-only wrapper to easily grab different information about the response.

getStatusCode

Gets the status code for the response.

NameTypeRequiredDefaultDescription
No arguments
getData

Gets the data for the response.

NameTypeRequiredDefaultDescription
No arguments
getRequest

Gets the HyperRequest instance associated with this response.

NameTypeRequiredDefaultDescription
No arguments
getCharset

Gets the charset value for the response.

NameTypeRequiredDefaultDescription
No arguments
getTimestamp

Gets the timestamp for when this response was recieved.

NameTypeRequiredDefaultDescription
No arguments
json

Returns the data of the request as deserialized JSON.

NameTypeRequiredDefaultDescription
No arguments
isSuccess

Returns true if the request status code is considered successful.

NameTypeRequiredDefaultDescription
No arguments
isRedirect

Returns true if the request status code is considered a redirect.

NameTypeRequiredDefaultDescription
No arguments
isError

Returns true if the request status code is considered either a client error or a server error.

NameTypeRequiredDefaultDescription
No arguments
isClientError

Returns true if the request status code is considered a client error.

NameTypeRequiredDefaultDescription
No arguments
isServerError

Returns true if the request status code is considered a server error.

NameTypeRequiredDefaultDescription
No arguments

Request Defaults

Hyper allows you to configure defaults for your requests. This is particularly useful for reducing boilerplate in your application.

Defaults are set on the HyperBuilder instance. The easiest way to do this is to configure it in WireBox:

// config/WireBox.cfc
component {

    function configure() {
        map( "StarWarsClient" )
            .to( "hyper.models.HyperBuilder" )
            .asSingleton()
            .initWith(
                baseUrl = "https://swapi.co/api"
            );
    }

}

Now, you can inject this pre-configured builder wherever you need in your application:

component {

    property name="StarWarsClient" inject="id";

    function findUser( id ) {
        return StarWarsClient.get( "/people/#id#" );
    }

}

You can even create multiple clients using this approach:

// config/WireBox.cfc
component {

    function configure() {
        map( "SWAPIClient" )
            .to( "hyper.models.HyperBuilder" )
            .asSingleton()
            .initWith(
                baseUrl = "https://swapi.co/api"
            );

        map( "GitHubClient" )
            .to( "hyper.models.HyperBuilder" )
            .asSingleton()
            .initWith(
                baseUrl = "https://api.github.com",
                headers = {
                    "Authorization" = getSetting( "SWAPI_TOKEN" )
                }
            );
    }

}

You can also set or change the defaults by either passing the key / value pairs in to the init method or by calling the appropriate HyperRequest method on the HyperBuilder.defaults property.

var hyper = new Hyper.models.HyperBuilder(
    baseUrl = "https://api.github.com"
);
hyper.defaults.withHeaders( { "Authorization" = token } );

v2.0.2

31 Oct 2018 — 18:20: 32 UTC

fix

  • HyperRequest: Avoid double encoding using cfhttpparam (f25bc86)

v2.0.1

23 Oct 2018 — 21:40: 42 UTC

docs

  • README: Remove Lucee 4.5 support from docs (3abe4f8)

v2.0.0

23 Oct 2018 — 21:27: 14 UTC

BREAKING

  • HyperRequest: Do not include username & password unless they have values (dcd2270)

chore

  • test: Add Adobe 2018 to Travis CI (14506b6)

fix

  • CHANGELOG: Correct changelog for 2.0.0 (a25648e)

v2.0.0

23 Oct 2018 — 19:44: 23 UTC

chore

  • test: Add Adobe 2018 to Travis CI (14506b6)

fix

  • HyperRequest: Do not include username & password unless they have values (dcd2270)

v1.15.0

19 Jul 2018 — 03:24: 45 UTC

feat

  • HyperRequest: Add throw on error flag (bf05152)

v1.14.5

14 May 2018 — 18:35: 29 UTC

fix

  • HyperResponse: Use 504 status code for incomplete responses. (589a6c6)

v1.14.4

20 Apr 2018 — 22:38: 10 UTC

fix

  • HyperRequest: Include equal signs in query param values (abc36be)

24 Jan 2018

other

24 Jan 2018

other

24 Jan 2018

other

23 Jan 2018

chore

  • box.json: Migrate to coldbox-modules organization (c16a9a4)
  • box.json: Update description (1c7f268)
  • release: Enable semantic release (39e9ebd)

doc

  • docblocks: Update the docblock for data in HyperBuilder (a33ef95)

docs

  • README: Fix Travis CI link (d4785e4)
  • HyperRequest: Add missing docblocks (8f6a3b5)
  • README: Add initial documentation (9104e89)
  • docblocks: Update docblocks for HyperBuilder, HyperRequest, and HyperResponse (6eb0087)

feat

  • HyperRequest: Add a when helper to execute conditions without breaking chaining. (b8ec096)
  • HyperRequest: Clear out any configured values of the HyperRequest. (ca13e6f)
  • HyperRequest: Add configurable timeout (929abeb)

fix

  • box.json: Fix line endings (102ee22)
  • box.json: Use * as default version to avoid installation issues (again) (d2b32ff)
  • box.json: Use latest as default version to avoid installation issues (4c56adc)
  • box.json: Use * as default version to avoid installation issues (c0b5294)
  • HyperRequest: Missing semicolon (484fa87)
  • HyperRequest: Fix for Lucee 4 compatibility on LinkedHashMaps and keyArray (a8b64de)
  • HyperRequest: Preserve case in header names (59c3349)
  • HyperRequest: Preserve case for query parameters (617f5db)
  • HyperRequest: Serialize both params from the url and set on the HyperRequest (7e960a0)
  • HyperResponse: Default to UTF-8 charset if none is present in the response. (9587100)
  • HyperRequest: Fix withHeaders and withQueryParams for Lucee compatibility (26d5628)
  • HyperBuilder: Only map HyperBuilder as a singleton in WireBox (e5556f7)

other

  • *: Trigger build (46ae862)
  • *: 1.3.0 (293bdc4)
  • *: 1.3.0 (a9f939b)
  • *: 1.3.0 (a66c31a)
  • *: 1.0.0 (215fb05)
  • *: Docblocks for HyperBuilder (18c4cb2)
  • *: Add PUT, PATCH, and DELETE method shortcuts (f79694b)
  • *: Add basic auth (dc90012)
  • *: Rename makeRequest to send (05041b5)
  • *: Serialize query params (9072a48)
  • *: Add withHeaders method to set multiple headers at once (f4adf3e)
  • *: Pass on any init arguments to builder to the default request (03a05a3)
  • *: Allow defaults to be set on the HyperBuilder that appear on each request (ab034ca)
  • *: Refactor to use internal api instead of external one (9f21e88)
  • *: Add status code detection to the response class (abbdd70)
  • *: Begin README (61cd67c)
  • *: Default engine to the most restrictive engine we support. (54eed06)
  • *: Switch ColdBox to be a devDependency (2871bc6)
  • *: Add in missing coldbox dependency (cefb604)
  • *: Some refactoring and fixes for ACF. (08dc986)
  • *: Allow for POST requests (77a28ee)
  • *: Handle redirects and store the request on the response (c1f6b33)
  • *: It passes through all other methods to the HyperRequest class. (ccfd213)
  • *: Throw an exception when trying to deserialize data to json that is not json (7030d38)
  • *: Make a basic GET request with Hyper! (005edfd)

23 Jan 2018

chore

  • box.json: Migrate to coldbox-modules organization (c16a9a4)
  • box.json: Update description (1c7f268)
  • release: Enable semantic release (39e9ebd)

doc

  • docblocks: Update the docblock for data in HyperBuilder (a33ef95)

docs

  • HyperRequest: Add missing docblocks (8f6a3b5)
  • README: Add initial documentation (9104e89)
  • docblocks: Update docblocks for HyperBuilder, HyperRequest, and HyperResponse (6eb0087)

feat

  • HyperRequest: Add a when helper to execute conditions without breaking chaining. (b8ec096)
  • HyperRequest: Clear out any configured values of the HyperRequest. (ca13e6f)
  • HyperRequest: Add configurable timeout (929abeb)

fix

  • box.json: Use * as default version to avoid installation issues (again) (d2b32ff)
  • box.json: Use latest as default version to avoid installation issues (4c56adc)
  • box.json: Use * as default version to avoid installation issues (c0b5294)
  • HyperRequest: Missing semicolon (484fa87)
  • HyperRequest: Fix for Lucee 4 compatibility on LinkedHashMaps and keyArray (a8b64de)
  • HyperRequest: Preserve case in header names (59c3349)
  • HyperRequest: Preserve case for query parameters (617f5db)
  • HyperRequest: Serialize both params from the url and set on the HyperRequest (7e960a0)
  • HyperResponse: Default to UTF-8 charset if none is present in the response. (9587100)
  • HyperRequest: Fix withHeaders and withQueryParams for Lucee compatibility (26d5628)
  • HyperBuilder: Only map HyperBuilder as a singleton in WireBox (e5556f7)

other

  • *: Trigger build (46ae862)
  • *: 1.3.0 (293bdc4)
  • *: 1.3.0 (a9f939b)
  • *: 1.3.0 (a66c31a)
  • *: 1.0.0 (215fb05)
  • *: Docblocks for HyperBuilder (18c4cb2)
  • *: Add PUT, PATCH, and DELETE method shortcuts (f79694b)
  • *: Add basic auth (dc90012)
  • *: Rename makeRequest to send (05041b5)
  • *: Serialize query params (9072a48)
  • *: Add withHeaders method to set multiple headers at once (f4adf3e)
  • *: Pass on any init arguments to builder to the default request (03a05a3)
  • *: Allow defaults to be set on the HyperBuilder that appear on each request (ab034ca)
  • *: Refactor to use internal api instead of external one (9f21e88)
  • *: Add status code detection to the response class (abbdd70)
  • *: Begin README (61cd67c)
  • *: Default engine to the most restrictive engine we support. (54eed06)
  • *: Switch ColdBox to be a devDependency (2871bc6)
  • *: Add in missing coldbox dependency (cefb604)
  • *: Some refactoring and fixes for ACF. (08dc986)
  • *: Allow for POST requests (77a28ee)
  • *: Handle redirects and store the request on the response (c1f6b33)
  • *: It passes through all other methods to the HyperRequest class. (ccfd213)
  • *: Throw an exception when trying to deserialize data to json that is not json (7030d38)
  • *: Make a basic GET request with Hyper! (005edfd)
  • *: Initial commit (de0c27d)

23 Jan 2018

chore

  • box.json: Migrate to coldbox-modules organization (c16a9a4)
  • box.json: Update description (1c7f268)
  • release: Enable semantic release (39e9ebd)

doc

  • docblocks: Update the docblock for data in HyperBuilder (a33ef95)

docs

  • HyperRequest: Add missing docblocks (8f6a3b5)
  • README: Add initial documentation (9104e89)
  • docblocks: Update docblocks for HyperBuilder, HyperRequest, and HyperResponse (6eb0087)

feat

  • HyperRequest: Add a when helper to execute conditions without breaking chaining. (b8ec096)
  • HyperRequest: Clear out any configured values of the HyperRequest. (ca13e6f)
  • HyperRequest: Add configurable timeout (929abeb)

fix

  • box.json: Use * as default version to avoid installation issues (again) (d2b32ff)
  • box.json: Use latest as default version to avoid installation issues (4c56adc)
  • box.json: Use * as default version to avoid installation issues (c0b5294)
  • HyperRequest: Missing semicolon (484fa87)
  • HyperRequest: Fix for Lucee 4 compatibility on LinkedHashMaps and keyArray (a8b64de)
  • HyperRequest: Preserve case in header names (59c3349)
  • HyperRequest: Preserve case for query parameters (617f5db)
  • HyperRequest: Serialize both params from the url and set on the HyperRequest (7e960a0)
  • HyperResponse: Default to UTF-8 charset if none is present in the response. (9587100)
  • HyperRequest: Fix withHeaders and withQueryParams for Lucee compatibility (26d5628)
  • HyperBuilder: Only map HyperBuilder as a singleton in WireBox (e5556f7)

other

  • *: Trigger build (46ae862)
  • *: 1.3.0 (293bdc4)
  • *: 1.3.0 (a9f939b)
  • *: 1.3.0 (a66c31a)
  • *: 1.0.0 (215fb05)
  • *: Docblocks for HyperBuilder (18c4cb2)
  • *: Add PUT, PATCH, and DELETE method shortcuts (f79694b)
  • *: Add basic auth (dc90012)
  • *: Rename makeRequest to send (05041b5)
  • *: Serialize query params (9072a48)
  • *: Add withHeaders method to set multiple headers at once (f4adf3e)
  • *: Pass on any init arguments to builder to the default request (03a05a3)
  • *: Allow defaults to be set on the HyperBuilder that appear on each request (ab034ca)
  • *: Refactor to use internal api instead of external one (9f21e88)
  • *: Add status code detection to the response class (abbdd70)
  • *: Begin README (61cd67c)
  • *: Default engine to the most restrictive engine we support. (54eed06)
  • *: Switch ColdBox to be a devDependency (2871bc6)
  • *: Add in missing coldbox dependency (cefb604)
  • *: Some refactoring and fixes for ACF. (08dc986)
  • *: Allow for POST requests (77a28ee)
  • *: Handle redirects and store the request on the response (c1f6b33)
  • *: It passes through all other methods to the HyperRequest class. (ccfd213)
  • *: Throw an exception when trying to deserialize data to json that is not json (7030d38)
  • *: Make a basic GET request with Hyper! (005edfd)
  • *: Initial commit (de0c27d)

23 Jan 2018

chore

  • box.json: Migrate to coldbox-modules organization (c16a9a4)
  • box.json: Update description (1c7f268)
  • release: Enable semantic release (39e9ebd)

doc

  • docblocks: Update the docblock for data in HyperBuilder (a33ef95)

docs

  • HyperRequest: Add missing docblocks (8f6a3b5)
  • README: Add initial documentation (9104e89)
  • docblocks: Update docblocks for HyperBuilder, HyperRequest, and HyperResponse (6eb0087)

feat

  • HyperRequest: Add a when helper to execute conditions without breaking chaining. (b8ec096)
  • HyperRequest: Clear out any configured values of the HyperRequest. (ca13e6f)
  • HyperRequest: Add configurable timeout (929abeb)

fix

  • box.json: Use * as default version to avoid installation issues (again) (d2b32ff)
  • box.json: Use latest as default version to avoid installation issues (4c56adc)
  • box.json: Use * as default version to avoid installation issues (c0b5294)
  • HyperRequest: Missing semicolon (484fa87)
  • HyperRequest: Fix for Lucee 4 compatibility on LinkedHashMaps and keyArray (a8b64de)
  • HyperRequest: Preserve case in header names (59c3349)
  • HyperRequest: Preserve case for query parameters (617f5db)
  • HyperRequest: Serialize both params from the url and set on the HyperRequest (7e960a0)
  • HyperResponse: Default to UTF-8 charset if none is present in the response. (9587100)
  • HyperRequest: Fix withHeaders and withQueryParams for Lucee compatibility (26d5628)
  • HyperBuilder: Only map HyperBuilder as a singleton in WireBox (e5556f7)

other

  • *: Trigger build (46ae862)
  • *: 1.3.0 (293bdc4)
  • *: 1.3.0 (a9f939b)
  • *: 1.3.0 (a66c31a)
  • *: 1.0.0 (215fb05)
  • *: Docblocks for HyperBuilder (18c4cb2)
  • *: Add PUT, PATCH, and DELETE method shortcuts (f79694b)
  • *: Add basic auth (dc90012)
  • *: Rename makeRequest to send (05041b5)
  • *: Serialize query params (9072a48)
  • *: Add withHeaders method to set multiple headers at once (f4adf3e)
  • *: Pass on any init arguments to builder to the default request (03a05a3)
  • *: Allow defaults to be set on the HyperBuilder that appear on each request (ab034ca)
  • *: Refactor to use internal api instead of external one (9f21e88)
  • *: Add status code detection to the response class (abbdd70)
  • *: Begin README (61cd67c)
  • *: Default engine to the most restrictive engine we support. (54eed06)
  • *: Switch ColdBox to be a devDependency (2871bc6)
  • *: Add in missing coldbox dependency (cefb604)
  • *: Some refactoring and fixes for ACF. (08dc986)
  • *: Allow for POST requests (77a28ee)
  • *: Handle redirects and store the request on the response (c1f6b33)
  • *: It passes through all other methods to the HyperRequest class. (ccfd213)
  • *: Throw an exception when trying to deserialize data to json that is not json (7030d38)
  • *: Make a basic GET request with Hyper! (005edfd)
  • *: Initial commit (de0c27d)

23 Jan 2018

fix

  • box.json: Use * as default version to avoid installation issues (again) (d2b32ff)
  • box.json: Use latest as default version to avoid installation issues (4c56adc)

 

 
$ box install hyper
No collaborators yet.
  • Nov 17 2017 01:31 PM
  • Oct 31 2018 01:20 PM
  • 835
  • 0
  • 894