com.akamai.edgegrid.signer.Request Maven / Gradle / Ivy
/*
* Copyright 2018 Akamai Technologies, Inc. All Rights Reserved.
*
* 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 com.akamai.edgegrid.signer;
import com.akamai.edgegrid.signer.exceptions.RequestSigningException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Library-agnostic representation of an HTTP request. This object is immutable, so you probably
* want to build an instance using {@link RequestBuilder}. Extenders of
* {@link AbstractEdgeGridRequestSigner} will need to build one of these as part of their
* implementation.
*
* @author [email protected]
* @author [email protected]
*/
public class Request implements Comparable {
/** A {@link String} {@link Comparator}. */
private static Comparator stringComparator = new NullSafeComparator<>();
/** A {@link URI} {@link Comparator}. */
private static Comparator uriComparator = new NullSafeComparator<>();
private final byte[] body;
private final String method;
private final URI uri;
private final Map headers;
/**
* Creates a new instance of {@link Request} using provided request builder.
*
* @param b {@link RequestBuilder}
*/
private Request(RequestBuilder b) {
this.body = b.body;
this.method = b.method;
this.headers = b.headers;
this.uri = b.uri;
}
/**
* Returns a new builder. The returned builder is equivalent to the builder
* generated by {@link RequestBuilder}.
*
* @return a fresh {@link RequestBuilder}
*/
public static RequestBuilder builder() {
return new RequestBuilder();
}
@Override
public int compareTo(Request that) {
if (that == null) {
return 1;
}
int comparison = 0;
comparison = uriComparator.compare(this.uri, that.uri);
if (comparison == 0) {
comparison = stringComparator.compare(this.method, that.method);
}
if (comparison == 0) {
comparison = Integer.compare(this.body.length, that.body.length);
}
if (comparison == 0) {
for (int i = 0; i < this.body.length && comparison == 0; i++) {
byte left = this.body[i];
byte right = that.body[i];
comparison = left < right ? -1 : left > right ? 1 : 0;
}
}
if (comparison == 0) {
comparison = Integer.compare(this.headers.size(), that.headers.size());
}
if (comparison == 0) {
for (String key : this.headers.keySet()) {
if (!that.headers.containsKey(key)) {
comparison = 1;
break;
}
comparison = this.headers.get(key).compareTo(that.headers.get(key));
if (comparison != 0) {
break;
}
}
}
return comparison;
}
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (getClass() != o.getClass()) return false;
final Request that = (Request) o;
return compareTo(that) == 0;
}
@Override
public int hashCode() {
return Objects.hash(body, headers, method, uri);
}
@Override
public String toString() {
return new StringBuilder("[ ")
.append("body: ").append(body).append("; ")
.append("headers: ").append(headers).append("; ")
.append("method: ").append(method).append("; ")
.append("uri: ").append(uri)
.append(" ]")
.toString();
}
byte[] getBody() {
return body;
}
Map getHeaders() {
return Collections.unmodifiableMap(headers);
}
String getMethod() {
return method;
}
URI getUri() {
return uri;
}
/**
* Creates an instance of {@link Request#builder()}. The returned builder is equivalent to the builder
* generated by {@link Request#builder()}.
*/
public static class RequestBuilder {
private byte[] body = new byte[]{};
private Map headers = new HashMap<>();
private String method;
private URI uri;
/**
* Sets a content of HTTP request body. If not set, body is empty by default.
*
* @param requestBody a request body, in bytes
* @return reference back to this builder instance
*/
public RequestBuilder body(byte[] requestBody) {
if (requestBody != null && requestBody.length != 0) {
this.body = Arrays.copyOf(requestBody, requestBody.length);
}
return this;
}
/**
*
* Adds a single header for an HTTP request. This can be called multiple times to add as
* many headers as needed.
*
*
* NOTE: All header names are lower-cased for storage. In HTTP, header names are
* case-insensitive anyway, and EdgeGrid does not support multiple headers with the same
* name. Forcing to lowercase here improves our chance of detecting bad requests early.
*
*
* @param headerName a header name
* @param value a header value
* @return reference back to this builder instance
* @throws IllegalArgumentException if a duplicate header name is encountered
*/
public RequestBuilder header(String headerName, String value) {
if (headerName == null || "".equals(headerName)) {
throw new IllegalArgumentException("headerName cannot be empty");
}
if (value == null || "".equals(value)) {
throw new IllegalArgumentException("value cannot be empty");
}
headerName = headerName.toLowerCase();
if (this.headers.containsKey(headerName)) {
throw new IllegalArgumentException("Duplicate header found: " + headerName);
}
headers.put(headerName, value);
return this;
}
/**
*
* Sets headers of HTTP request. The {@code headers} parameter is copied so that changes
* to the original {@link Map} will not impact the stored reference.
*
*
* NOTE: All header names are lower-cased for storage. In HTTP, header names are
* case-insensitive anyway, and EdgeGrid does not support multiple headers with the same
* name. Forcing to lowercase here improves our chance of detecting bad requests early.
*
*
* @param headers a {@link Map} of headers
* @return reference back to this builder instance
* @throws IllegalArgumentException if a duplicate header name is encountered
*/
public RequestBuilder headers(Map headers) {
Objects.requireNonNull(headers, "headers cannot be null");
for (Map.Entry entry : headers.entrySet()) {
header(entry.getKey(), entry.getValue());
}
return this;
}
/**
* Sets HTTP method: GET, PUT, POST, DELETE. Mandatory to set.
*
* @param method an HTTP method
* @return reference back to this builder instance
*/
public RequestBuilder method(String method) {
if (Objects.isNull(method) || "".equals(method)) {
throw new IllegalArgumentException("method cannot be empty");
}
this.method = method;
return this;
}
/**
*
* Sets the URI of the HTTP request. This URI MUST have the correct path and query
* segments set. Scheme is assumed to be "HTTPS" for the purpose of this library. Host is
* actually taken from a {@link ClientCredential} as signing time; any value in this URI is
* discarded. Fragments are not signed.
*
*
* A path and/or query string is required.
*
*
* @param uri a {@link URI}
* @return reference back to this builder instance
*/
public RequestBuilder uri(String uri) {
if (uri == null || "".equals(uri)) {
throw new IllegalArgumentException("uri cannot be empty");
}
return uri(URI.create(uri));
}
/**
*
* Sets the URI of the HTTP request. This URI MUST have the correct path and query
* segments set. Scheme is assumed to be "HTTPS" for the purpose of this library. Host is
* actually taken from a {@link ClientCredential} as signing time; any value in this URI is
* discarded. Fragments are not signed.
*
*
* A path and/or query string is required.
*
*
* @param uri a {@link URI}
* @return reference back to this builder instance
*/
public RequestBuilder uri(URI uri) {
Objects.requireNonNull(uri, "uri cannot be empty");
try {
this.uri = new URI(null, null, uri.getPath(), uri.getRawQuery(), null);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Error setting URI", e);
}
return this;
}
/**
*
* Sets the URI of the HTTP request without any processing it, which is important when URI contains
* path parameters which consists of encoded URLs.
* This URI MUST have the correct path and query segments set. Scheme is assumed to be "HTTPS" for the purpose of this library. Host is
* actually taken from a {@link ClientCredential} at signing time; any value in this URI is
* discarded. Fragments are not removed from signing process.
*
*
* A path and/or query string is required.
*
*
* @param uri a {@link URI}
* @return reference back to this builder instance
*/
public RequestBuilder rawUri(URI uri) {
Objects.requireNonNull(uri, "uri cannot be empty");
this.uri = uri;
return this;
}
/**
* Returns a newly-created immutable HTTP request.
*
* @return new HTTP request {@link Request}
*/
public Request build() {
Objects.requireNonNull(body, "body cannot be empty");
Objects.requireNonNull(uri, "uriWithQuery cannot be empty");
if (Objects.isNull(method) || "".equals(method)) {
throw new IllegalArgumentException("method cannot be empty");
}
return new Request(this);
}
}
}