/*
* Copyright (c) 2014-2015 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.werval.runtime.http;
import java.net.HttpCookie;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import io.werval.api.Config;
import io.werval.api.http.Cookies;
import io.werval.api.http.Cookies.Cookie;
import io.werval.api.http.FormUploads;
import io.werval.api.http.Headers;
import io.werval.api.http.Method;
import io.werval.api.http.ProtocolVersion;
import io.werval.api.http.QueryString;
import io.werval.api.http.Request;
import io.werval.api.i18n.Langs;
import io.werval.runtime.exceptions.BadRequestException;
import io.werval.spi.http.HttpBuildersSPI;
import io.werval.util.ByteSource;
import io.werval.util.Strings;
import io.werval.util.URLs;
import static io.werval.api.http.Headers.Names.CONTENT_TYPE;
import static io.werval.api.http.Headers.Names.COOKIE;
import static io.werval.api.http.Headers.Names.X_HTTP_METHOD_OVERRIDE;
import static io.werval.api.http.Method.CONNECT;
import static io.werval.api.http.Method.DELETE;
import static io.werval.api.http.Method.GET;
import static io.werval.api.http.Method.HEAD;
import static io.werval.api.http.Method.OPTIONS;
import static io.werval.api.http.Method.PATCH;
import static io.werval.api.http.Method.POST;
import static io.werval.api.http.Method.PUT;
import static io.werval.api.http.Method.TRACE;
import static io.werval.api.http.ProtocolVersion.HTTP_1_1;
import static io.werval.runtime.ConfigKeys.WERVAL_HTTP_HEADERS_X_FORWARDED_FOR_CHECK;
import static io.werval.runtime.ConfigKeys.WERVAL_HTTP_HEADERS_X_FORWARDED_FOR_ENABLED;
import static io.werval.runtime.ConfigKeys.WERVAL_HTTP_HEADERS_X_FORWARDED_FOR_TRUSTED;
import static io.werval.util.IllegalArguments.ensureInRange;
import static io.werval.util.IllegalArguments.ensureNotEmpty;
import static io.werval.util.IllegalArguments.ensureNotNull;
import static java.util.Collections.emptyMap;
/**
* HTTP API Objects Builders Instance.
*/
public class HttpBuildersInstance
implements HttpBuildersSPI
{
private static final Headers EMPTY_REQ_HEADERS = new HeadersInstance( emptyMap(), BadRequestException.BUILDER );
private final Config config;
private final Charset defaultCharset;
private final Langs langs;
/**
* Create a new HttpBuilders instance.
*
* @param config Configuration
* @param defaultCharset Application default charset
* @param langs Applicaiton Langs
*/
public HttpBuildersInstance( Config config, Charset defaultCharset, Langs langs )
{
this.config = config;
this.defaultCharset = defaultCharset;
this.langs = langs;
}
@Override
public RequestBuilder newRequestBuilder()
{
return new RequestBuilderInstance(
config, defaultCharset, langs, null, null, null, null, null, null, null, null, null, null
);
}
private static final class RequestBuilderInstance
implements RequestBuilder
{
private final Config config;
private final Charset defaultCharset;
private final Langs langs;
private final String identity;
private final String remoteSocketAddress;
private final ProtocolVersion version;
private final Method method;
private final String uri;
private final Headers headers;
private final Cookies cookies;
private final ByteSource bodyBytes;
private final Map<String, List<String>> attributes;
private final Map<String, List<FormUploads.Upload>> uploads;
private RequestBuilderInstance(
Config config, Charset defaultCharset, Langs langs,
String identity, String remoteSocketAddress,
ProtocolVersion version, Method method, String uri,
Headers headers, Cookies cookies,
ByteSource bodyBytes,
Map<String, List<String>> attributes, Map<String, List<FormUploads.Upload>> uploads
)
{
this.config = config;
this.defaultCharset = defaultCharset;
this.langs = langs;
this.identity = Strings.isEmpty( identity ) ? "NO_REQUEST_ID" : identity;
this.remoteSocketAddress = remoteSocketAddress;
this.version = version == null ? HTTP_1_1 : version;
this.method = method;
this.uri = uri;
this.headers = headers == null ? EMPTY_REQ_HEADERS : headers;
this.cookies = cookies == null ? CookiesInstance.EMPTY : cookies;
this.bodyBytes = bodyBytes;
this.attributes = attributes;
this.uploads = uploads;
}
@Override
public RequestBuilder identifiedBy( String identity )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder remoteSocketAddress( String remoteSocketAddress )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder version( ProtocolVersion version )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder method( String method )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, Method.valueOf( method ), uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder method( Method method )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder uri( String uri )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder get( String uri )
{
return method( GET ).uri( uri );
}
@Override
public RequestBuilder head( String uri )
{
return method( HEAD ).uri( uri );
}
@Override
public RequestBuilder options( String uri )
{
return method( OPTIONS ).uri( uri );
}
@Override
public RequestBuilder trace( String uri )
{
return method( TRACE ).uri( uri );
}
@Override
public RequestBuilder connect( String uri )
{
return method( CONNECT ).uri( uri );
}
@Override
public RequestBuilder put( String uri )
{
return method( PUT ).uri( uri );
}
@Override
public RequestBuilder post( String uri )
{
return method( POST ).uri( uri );
}
@Override
public RequestBuilder patch( String uri )
{
return method( PATCH ).uri( uri );
}
@Override
public RequestBuilder delete( String uri )
{
return method( DELETE ).uri( uri );
}
@Override
public RequestBuilder headers( Headers headers )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder headers( Map<String, List<String>> headers )
{
return headers( new HeadersInstance( headers, BadRequestException.BUILDER ) );
}
@Override
public RequestBuilder cookies( Cookies cookies )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder bodyBytes( ByteSource bodyBytes )
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public RequestBuilder bodyForm(
Map<String, List<String>> attributes,
Map<String, List<FormUploads.Upload>> uploads
)
{
return new RequestBuilderInstance(
config, defaultCharset, langs,
identity, remoteSocketAddress, version, method, uri,
headers, cookies,
bodyBytes, attributes, uploads
);
}
@Override
public Request build()
{
ensureNotEmpty( "identity", identity );
ensureNotNull( "version", version );
ensureNotNull( "method", method );
ensureNotEmpty( "uri", uri );
ensureNotNull( "headers", headers );
ensureNotNull( "cookies", cookies );
// Parse Path & QueryString from URI
QueryString.Decoder decoder = new QueryString.Decoder( uri, defaultCharset );
String path = URLs.decode( decoder.path(), defaultCharset );
QueryString queryString = new QueryStringInstance( decoder.parameters(), BadRequestException.BUILDER );
// Request charset
Charset requestCharset = null;
if( headers.has( CONTENT_TYPE ) )
{
Optional<String> extractedCharset = Headers.extractCharset( headers.singleValue( CONTENT_TYPE ) );
if( extractedCharset.isPresent() )
{
requestCharset = Charset.forName( extractedCharset.get() );
}
}
if( requestCharset == null )
{
requestCharset = defaultCharset;
}
// Parse Cookies from Headers
Map<String, Cookie> allCookies = new HashMap<>();
if( headers.has( COOKIE ) )
{
List<String> cookieHeaders = headers.values( COOKIE );
for( String cookieHeader : cookieHeaders )
{
for( String splitCookieHeader : splitMultiCookies( cookieHeader ) )
{
for( HttpCookie jCookie : HttpCookie.parse( splitCookieHeader ) )
{
allCookies.put(
jCookie.getName(),
new CookiesInstance.CookieInstance(
jCookie.getVersion(),
jCookie.getName(),
jCookie.getValue(),
jCookie.getPath(),
jCookie.getDomain(),
jCookie.getMaxAge(),
jCookie.getSecure(),
jCookie.isHttpOnly(),
jCookie.getComment(),
jCookie.getCommentURL()
)
);
}
}
}
}
// Override with cookies given through API
for( Cookie cookie : cookies )
{
allCookies.put( cookie.name(), cookie );
}
// Build Request
return new RequestInstance(
// Request Header
new RequestHeaderInstance(
// Langs
langs,
// Identity
identity,
// Remote Address can be null
remoteSocketAddress,
// X-Forwarded-For configuration
config.bool( WERVAL_HTTP_HEADERS_X_FORWARDED_FOR_ENABLED ),
config.bool( WERVAL_HTTP_HEADERS_X_FORWARDED_FOR_CHECK ),
config.stringList( WERVAL_HTTP_HEADERS_X_FORWARDED_FOR_TRUSTED ),
version,
// HTTP Method Override
headers.has( X_HTTP_METHOD_OVERRIDE )
? Method.valueOf( headers.singleValue( X_HTTP_METHOD_OVERRIDE ) )
: method,
// Path & QueryString parsed from URI
uri, path, queryString,
// Headers
headers,
// Cookies
new CookiesInstance( allCookies )
),
// Request Body
( attributes != null || uploads != null )
? new RequestBodyInstance( requestCharset, attributes, uploads )
: new RequestBodyInstance( requestCharset, bodyBytes )
);
}
}
@Override
public CookieBuilder newCookieBuilder()
{
return new CookieBuilderInstance( null, null, null, null, null, null, null, null, null, null );
}
private static final class CookieBuilderInstance
implements CookieBuilder
{
private final int version;
private final String name;
private final String value;
private final String path;
private final String domain;
private final long maxAge;
private final boolean secure;
private final boolean httpOnly;
private final String comment;
private final String commentUrl;
private CookieBuilderInstance(
Integer version,
String name, String value,
String path, String domain,
Long maxAge,
Boolean secure, Boolean httpOnly,
String comment, String commentUrl )
{
this.version = version == null ? 0 : version;
this.name = name;
this.value = value == null ? Strings.EMPTY : value;
this.path = path == null ? "/" : path;
this.domain = domain;
this.maxAge = maxAge == null ? Long.MIN_VALUE : maxAge;
this.secure = secure == null ? false : secure;
this.httpOnly = httpOnly == null ? true : httpOnly;
this.comment = comment;
this.commentUrl = commentUrl;
}
@Override
public CookieBuilder version( int version )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder name( String name )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder value( String value )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder path( String path )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder domain( String domain )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder maxAge( long maxAge )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder secure( boolean secure )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder httpOnly( boolean httpOnly )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder comment( String comment )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public CookieBuilder commentUrl( String commentUrl )
{
return new CookieBuilderInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
@Override
public Cookie build()
{
ensureNotEmpty( "name", name );
ensureInRange( "version", version, 0, 1 );
return new CookiesInstance.CookieInstance(
version, name, value, path, domain, maxAge, secure, httpOnly, comment, commentUrl
);
}
}
private static List<String> splitMultiCookies( String header )
{
List<String> cookies = new java.util.ArrayList<>();
int quoteCount = 0;
int p, q;
for( p = 0, q = 0; p < header.length(); )
{
int c = header.codePointAt( p );
if( c == '"' )
{
quoteCount++;
}
if( c == ';' && ( quoteCount % 2 == 0 ) )
{
// it is ; and not surrounded by double-quotes
cookies.add( header.substring( q, p ) );
q = p + Character.charCount( c );
}
p += Character.charCount( c );
}
cookies.add( header.substring( q ) );
return cookies;
}
}