com.tvd12.ezyhttp.client.HttpClient Maven / Gradle / Ivy
package com.tvd12.ezyhttp.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tvd12.ezyfox.builder.EzyBuilder;
import com.tvd12.ezyfox.io.EzyStrings;
import com.tvd12.ezyfox.util.EzyFileUtil;
import com.tvd12.ezyfox.util.EzyLoggable;
import com.tvd12.ezyhttp.client.concurrent.DownloadCancellationToken;
import com.tvd12.ezyhttp.client.data.DownloadFileResult;
import com.tvd12.ezyhttp.client.exception.DownloadCancelledException;
import com.tvd12.ezyhttp.client.request.DownloadRequest;
import com.tvd12.ezyhttp.client.request.Request;
import com.tvd12.ezyhttp.client.request.RequestEntity;
import com.tvd12.ezyhttp.core.codec.BodyDeserializer;
import com.tvd12.ezyhttp.core.codec.BodySerializer;
import com.tvd12.ezyhttp.core.codec.DataConverters;
import com.tvd12.ezyhttp.core.constant.*;
import com.tvd12.ezyhttp.core.data.MultiValueMap;
import com.tvd12.ezyhttp.core.exception.*;
import com.tvd12.ezyhttp.core.json.ObjectMapperBuilder;
import com.tvd12.ezyhttp.core.response.ResponseEntity;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import static com.tvd12.ezyfox.io.EzyStrings.isBlank;
import static com.tvd12.ezyfox.util.EzyFileUtil.getFileExtension;
import static com.tvd12.ezyhttp.client.concurrent.DownloadCancellationToken.ALWAYS_RUN;
import static com.tvd12.ezyhttp.core.constant.Headers.ACCEPT_ENCODING;
import static com.tvd12.ezyhttp.core.constant.Headers.CONTENT_ENCODING;
public class HttpClient extends EzyLoggable {
protected final int defaultReadTimeout;
protected final int defaultConnectTimeout;
protected final DataConverters dataConverters;
public static final int NO_TIMEOUT = -1;
protected HttpClient(Builder builder) {
this.defaultReadTimeout = builder.readTimeout;
this.defaultConnectTimeout = builder.connectTimeout;
this.dataConverters = builder.dataConverters;
}
public T call(Request request) throws Exception {
ResponseEntity response = request(
request.getMethod(),
request.getURL(),
request.getEntity(),
request.getResponseTypes(),
request.getConnectTimeout(),
request.getReadTimeout()
);
return getResponseBody(response);
}
public ResponseEntity request(Request request) throws Exception {
return request(
request.getMethod(),
request.getURL(),
request.getEntity(),
request.getResponseTypes(),
request.getConnectTimeout(),
request.getReadTimeout()
);
}
@SuppressWarnings("MethodLength")
public ResponseEntity request(
HttpMethod method,
String url,
RequestEntity entity,
Map> responseTypes,
int connectTimeout,
int readTimeout
) throws Exception {
if (url == null) {
throw new IllegalArgumentException("url can not be null");
}
logger.debug(
"start: {} - {} - {}",
method, url,
entity != null ? entity.getHeaders() : null
);
HttpURLConnection connection = connect(url);
try {
connection.setConnectTimeout(
connectTimeout > 0 ? connectTimeout : defaultConnectTimeout
);
connection.setReadTimeout(
readTimeout > 0 ? readTimeout : defaultReadTimeout
);
connection.setRequestMethod(method.toString());
connection.setDoInput(true);
connection.setDoOutput(method.hasOutput());
connection.setInstanceFollowRedirects(method == HttpMethod.GET);
MultiValueMap requestHeaders = entity != null ? entity.getHeaders() : null;
if (requestHeaders != null) {
Map encodedHeaders = requestHeaders.toMap();
for (Entry requestHeader : encodedHeaders.entrySet()) {
connection.setRequestProperty(
requestHeader.getKey(),
requestHeader.getValue()
);
}
}
if (connection.getRequestProperty(ACCEPT_ENCODING) == null) {
connection.setRequestProperty(
ACCEPT_ENCODING,
ContentEncoding.GZIP.getValue()
);
}
Object requestBody = null;
if (method != HttpMethod.GET && entity != null) {
requestBody = entity.getBody();
}
byte[] requestBodyBytes = null;
if (requestBody != null) {
String requestContentType = connection.getRequestProperty(Headers.CONTENT_TYPE);
if (requestContentType == null) {
requestContentType = ContentTypes.APPLICATION_JSON;
connection.setRequestProperty(
Headers.CONTENT_TYPE,
ContentTypes.APPLICATION_JSON
);
}
requestBodyBytes = serializeRequestBody(requestContentType, requestBody);
int requestContentLength = requestBodyBytes.length;
connection.setFixedLengthStreamingMode(requestContentLength);
}
connection.connect();
if (requestBodyBytes != null) {
if (method.hasOutput()) {
OutputStream outputStream = connection.getOutputStream();
outputStream.write(requestBodyBytes);
outputStream.flush();
outputStream.close();
} else {
throw new IllegalArgumentException(
method + " method can not have a payload body"
);
}
}
int responseCode = connection.getResponseCode();
Map> headerFields = connection.getHeaderFields();
MultiValueMap responseHeaders = MultiValueMap.of(headerFields);
String responseContentType = responseHeaders.getValue(Headers.CONTENT_TYPE);
if (responseContentType == null) {
responseContentType = ContentTypes.APPLICATION_JSON;
}
InputStream inputStream = decorateInputStream(
connection,
responseCode >= 400
? connection.getErrorStream()
: connection.getInputStream()
);
Object responseBody = null;
if (inputStream != null) {
try {
int responseContentLength = connection.getContentLength();
Class> responseType = responseTypes.get(responseCode);
responseBody = deserializeResponseBody(
responseContentType,
responseContentLength,
inputStream,
responseType
);
} finally {
inputStream.close();
}
}
logger.debug("end: {} - {} - {} - {}", method, url, responseCode, responseHeaders);
return new ResponseEntity(responseCode, responseHeaders, responseBody);
} finally {
connection.disconnect();
}
}
public HttpURLConnection connect(String url) throws Exception {
URL requestURL = new URL(url);
return (HttpURLConnection) requestURL.openConnection();
}
protected byte[] serializeRequestBody(
String contentType,
Object requestBody
) throws IOException {
BodySerializer serializer = dataConverters.getBodySerializer(contentType);
return serializer.serialize(requestBody);
}
protected Object deserializeResponseBody(
String contentType,
int contentLength,
InputStream inputStream,
Class> responseType
) throws IOException {
BodyDeserializer deserializer = dataConverters.getBodyDeserializer(contentType);
Object body;
if (responseType != null) {
if (responseType == String.class) {
body = deserializer.deserializeToString(inputStream, contentLength);
} else {
body = deserializer.deserialize(inputStream, responseType);
}
} else {
body = deserializer.deserializeToString(inputStream, contentLength);
if (body != null) {
try {
body = deserializer.deserialize((String) body, Map.class);
} catch (Exception e) {
// do nothing
}
}
}
return body;
}
@SuppressWarnings("unchecked")
public T getResponseBody(ResponseEntity entity) throws Exception {
int statusCode = entity.getStatus();
Object body = entity.getBody();
if (statusCode < 400) {
return (T) body;
}
throw translateErrorCode(statusCode, body);
}
private Exception translateErrorCode(int statusCode, Object body) {
if (statusCode == StatusCodes.BAD_REQUEST) {
return new HttpBadRequestException(body);
}
if (statusCode == StatusCodes.NOT_FOUND) {
return new HttpNotFoundException(body);
}
if (statusCode == StatusCodes.UNAUTHORIZED) {
return new HttpUnauthorizedException(body);
}
if (statusCode == StatusCodes.PAYMENT_REQUIRED) {
return new HttpPaymentRequiredException(body);
}
if (statusCode == StatusCodes.FORBIDDEN) {
return new HttpForbiddenException(body);
}
if (statusCode == StatusCodes.METHOD_NOT_ALLOWED) {
return new HttpMethodNotAllowedException(body);
}
if (statusCode == StatusCodes.NOT_ACCEPTABLE) {
return new HttpNotAcceptableException(body);
}
if (statusCode == StatusCodes.REQUEST_TIMEOUT) {
return new HttpRequestTimeoutException(body);
}
if (statusCode == StatusCodes.CONFLICT) {
return new HttpConflictException(body);
}
if (statusCode == StatusCodes.UNSUPPORTED_MEDIA_TYPE) {
return new HttpUnsupportedMediaTypeException(body);
}
if (statusCode == StatusCodes.TOO_MANY_REQUESTS) {
return new HttpTooManyRequestsException(body);
}
if (statusCode == StatusCodes.INTERNAL_SERVER_ERROR) {
return new HttpInternalServerErrorException(body);
}
return new HttpRequestException(statusCode, body);
}
/**
* Downloads a file from a URL and store to a file.
*
* @param fileURL HTTP URL of the file to be downloaded
* @param storeLocation path of the directory to save the file
* @return the downloaded file name
* @throws IOException when there is any I/O error
*/
public String download(
String fileURL,
File storeLocation
) throws Exception {
return download(fileURL, storeLocation, ALWAYS_RUN);
}
/**
* Downloads a file from a URL and store to a file.
*
* @param fileURL HTTP URL of the file to be downloaded
* @param storeLocation path of the directory to save the file
* @param cancellationToken the token to cancel
* @return the downloaded file name
* @throws IOException when there is any I/O error
*/
public String download(
String fileURL,
File storeLocation,
DownloadCancellationToken cancellationToken
) throws Exception {
return download(
new DownloadRequest(fileURL),
storeLocation,
cancellationToken
);
}
/**
* Downloads a file from a URL and store to a file.
*
* @param request the request of the file to be downloaded
* @param storeLocation path of the directory to save the file
* @return the downloaded file name
* @throws IOException when there is any I/O error
*/
public String download(
DownloadRequest request,
File storeLocation
) throws Exception {
return download(request, storeLocation, ALWAYS_RUN);
}
/**
* Downloads a file from a URL and store to a file.
*
* @param request the request of the file to be downloaded
* @param storeLocation path of the directory to save the file
* @param cancellationToken the token to cancel
* @return the downloaded file name
* @throws IOException when there is any I/O error
*/
public String download(
DownloadRequest request,
File storeLocation,
DownloadCancellationToken cancellationToken
) throws Exception {
String fileURL = request.getFileURL();
HttpURLConnection connection = connect(fileURL);
try {
decorateConnection(connection, request);
connection.connect();
return download(connection, fileURL, storeLocation, cancellationToken);
} finally {
connection.disconnect();
}
}
private String download(
HttpURLConnection connection,
String fileURL,
File storeLocation,
DownloadCancellationToken cancellationToken
) throws Exception {
int responseCode = connection.getResponseCode();
if (responseCode >= 400) {
throw processDownloadError(connection, fileURL, responseCode);
}
String disposition = connection.getHeaderField("Content-Disposition");
String fileName = getDownloadFileName(fileURL, disposition);
Files.createDirectories(storeLocation.toPath());
File storeFile = Paths.get(storeLocation.toString(), fileName).toFile();
File downloadingFile = new File(storeFile + ".downloading");
Path downloadingFilePath = downloadingFile.toPath();
Files.deleteIfExists(downloadingFilePath);
Files.createFile(downloadingFilePath);
try (
InputStream inputStream = decorateInputStream(
connection,
connection.getInputStream()
)
) {
try (FileOutputStream outputStream = new FileOutputStream(downloadingFile)) {
int bytesRead;
byte[] buffer = new byte[1024];
while ((bytesRead = inputStream.read(buffer)) != -1) {
if (cancellationToken.isCancelled()) {
break;
}
outputStream.write(buffer, 0, bytesRead);
}
}
}
if (cancellationToken.isCancelled()) {
Files.deleteIfExists(downloadingFilePath);
throw new DownloadCancelledException(fileURL);
}
Files.move(
downloadingFile.toPath(),
storeFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
);
return fileName;
}
/**
* Downloads a file from a URL and store to an output stream.
*
* @param fileURL HTTP URL of the file to be downloaded
* @param outputStream the output stream to save the file
* @throws IOException when there is any I/O error
*/
public void download(
String fileURL,
OutputStream outputStream
) throws Exception {
download(fileURL, outputStream, ALWAYS_RUN);
}
/**
* Downloads a file from a URL and store to an output stream.
*
* @param fileURL HTTP URL of the file to be downloaded
* @param outputStream the output stream to save the file
* @param cancellationToken the token to cancel
* @throws IOException when there is any I/O error
*/
public void download(
String fileURL,
OutputStream outputStream,
DownloadCancellationToken cancellationToken
) throws Exception {
download(new DownloadRequest(fileURL), outputStream, cancellationToken);
}
/**
* Downloads a file from a URL and store to an output stream.
*
* @param request the request of the file to be downloaded
* @param outputStream the output stream to save the file
* @throws IOException when there is any I/O error
*/
public void download(
DownloadRequest request,
OutputStream outputStream
) throws Exception {
download(request, outputStream, ALWAYS_RUN);
}
/**
* Downloads a file from a URL and store to an output stream.
*
* @param request the request of the file to be downloaded
* @param outputStream the output stream to save the file
* @param cancellationToken the token to cancel
* @throws IOException when there is any I/O error
*/
public void download(
DownloadRequest request,
OutputStream outputStream,
DownloadCancellationToken cancellationToken
) throws Exception {
String fileURL = request.getFileURL();
HttpURLConnection connection = connect(fileURL);
try {
decorateConnection(connection, request);
connection.connect();
download(connection, fileURL, outputStream, cancellationToken);
} finally {
connection.disconnect();
}
}
private void download(
HttpURLConnection connection,
String fileURL,
OutputStream outputStream,
DownloadCancellationToken cancellationToken
) throws Exception {
int responseCode = connection.getResponseCode();
if (responseCode >= 400) {
throw processDownloadError(connection, fileURL, responseCode);
}
try (
InputStream inputStream = decorateInputStream(
connection,
connection.getInputStream()
)
) {
int bytesRead;
byte[] buffer = new byte[1024];
while ((bytesRead = inputStream.read(buffer)) != -1) {
if (cancellationToken.isCancelled()) {
break;
}
outputStream.write(buffer, 0, bytesRead);
}
}
if (cancellationToken.isCancelled()) {
throw new DownloadCancelledException(fileURL);
}
}
public DownloadFileResult download(
String fileUrl,
File storeLocation,
String fileName
) throws Exception {
return download(
fileUrl,
storeLocation,
fileName,
ALWAYS_RUN
);
}
public DownloadFileResult download(
String fileUrl,
File storeLocation,
String fileName,
DownloadCancellationToken cancellationToken
) throws Exception {
return download(
new DownloadRequest(fileUrl),
storeLocation,
fileName,
cancellationToken
);
}
public DownloadFileResult download(
DownloadRequest request,
File storeLocation,
String fileName
) throws Exception {
return download(
request,
storeLocation,
fileName,
ALWAYS_RUN
);
}
public DownloadFileResult download(
DownloadRequest request,
File storeLocation,
String fileName,
DownloadCancellationToken cancellationToken
) throws Exception {
String fileURL = request.getFileURL();
HttpURLConnection connection = connect(fileURL);
try {
decorateConnection(connection, request);
connection.connect();
return download(
connection,
request,
storeLocation,
fileName,
cancellationToken
);
} finally {
connection.disconnect();
}
}
@SuppressWarnings("MethodLength")
private DownloadFileResult download(
HttpURLConnection originalConnection,
DownloadRequest request,
File storeLocation,
String fileName,
DownloadCancellationToken cancellationToken
) throws Exception {
HttpURLConnection connection = originalConnection;
try {
String fileURL = request.getFileURL();
String newFileURL = fileURL;
while (true) {
int responseCode = connection.getResponseCode();
if (responseCode >= 400) {
throw processDownloadError(connection, newFileURL, responseCode);
}
boolean redirect = responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER;
if (redirect) {
newFileURL = connection.getHeaderField("Location");
String cookies = connection.getHeaderField("Set-Cookie");
connection.disconnect();
connection = (HttpURLConnection) new URL(newFileURL).openConnection();
decorateConnection(connection, request);
if (cookies != null) {
connection.setRequestProperty("Cookie", cookies);
}
} else {
break;
}
}
String disposition = connection.getHeaderField("Content-Disposition");
String originalFileName = getDownloadFileName(newFileURL, disposition);
String newFileName = makeDownloadFileName(disposition, newFileURL, fileName);
File storeFile = storeLocation
.toPath()
.resolve(newFileName)
.toFile();
EzyFileUtil.createFileIfNotExists(storeFile);
try (
InputStream inputStream = decorateInputStream(
connection,
connection.getInputStream()
);
OutputStream outputStream = Files.newOutputStream(storeFile.toPath())
) {
int bytesRead;
byte[] buffer = new byte[1024];
while ((bytesRead = inputStream.read(buffer)) != -1) {
if (cancellationToken.isCancelled()) {
break;
}
outputStream.write(buffer, 0, bytesRead);
}
}
if (cancellationToken.isCancelled()) {
throw new DownloadCancelledException(fileURL);
}
return new DownloadFileResult(originalFileName, newFileName);
} finally {
connection.disconnect();
}
}
private void decorateConnection(
HttpURLConnection connection,
DownloadRequest request
) {
int connectTimeout = request.getReadTimeout();
int readTimeout = request.getReadTimeout();
connection.setConnectTimeout(connectTimeout > 0 ? connectTimeout : defaultConnectTimeout);
connection.setReadTimeout(readTimeout > 0 ? readTimeout : defaultReadTimeout);
MultiValueMap requestHeaders = request.getHeaders();
if (requestHeaders != null) {
Map encodedHeaders = requestHeaders.toMap();
for (Entry requestHeader : encodedHeaders.entrySet()) {
connection.setRequestProperty(requestHeader.getKey(), requestHeader.getValue());
}
}
}
private InputStream decorateInputStream(
HttpURLConnection connection,
InputStream inputStream
) throws Exception {
return decorateInputStream(
connection.getHeaderField(CONTENT_ENCODING),
inputStream
);
}
private InputStream decorateInputStream(
String contentEncoding,
InputStream inputStream
) throws IOException {
ContentEncoding contentEncodingEnum = ContentEncoding
.ofValue(contentEncoding);
if (contentEncodingEnum == ContentEncoding.GZIP) {
return new GZIPInputStream(inputStream);
}
return inputStream;
}
private Exception processDownloadError(
HttpURLConnection connection,
String fileURL,
int responseCode
) throws Exception {
InputStream inputStream = decorateInputStream(
connection,
connection.getErrorStream()
);
Object responseBody = "";
if (inputStream != null) {
try {
int contentLength = connection.getContentLength();
responseBody = deserializeResponseBody(null, contentLength, inputStream, null);
} finally {
inputStream.close();
}
}
logger.debug("download error: {} - {} - {}", fileURL, responseCode, responseBody);
return translateErrorCode(responseCode, responseBody);
}
private static String makeDownloadFileName(
String contentDisposition,
String fileURL,
String fileName
) {
String originalFileName = getDownloadFileName(fileURL, contentDisposition);
String fileExtension = getFileExtension(originalFileName);
return isBlank(fileExtension)
? fileName
: fileName + "." + fileExtension;
}
public static String getDownloadFileName(
String fileURL,
String contentDisposition
) {
String answer = null;
if (contentDisposition != null) {
String prefix = "filename=";
int startIndex = contentDisposition.indexOf(prefix);
if (startIndex >= 0) {
int quoteCount = 0;
int quotesCount = 0;
StringBuilder builder = new StringBuilder();
for (int i = startIndex + prefix.length(); i < contentDisposition.length(); ++i) {
char ch = contentDisposition.charAt(i);
if (ch == ';') {
break;
}
if (ch == '\'') {
if ((++quoteCount) >= 2) {
break;
}
} else if (ch == '\"') {
if ((++quotesCount) >= 2) {
break;
}
} else {
builder.append(ch);
}
}
answer = builder.toString().trim();
}
}
if (EzyStrings.isBlank(answer)) {
answer = fileURL.substring(fileURL.lastIndexOf("/") + 1);
}
if (answer.contains("?")) {
answer = answer.substring(0, answer.indexOf('?'));
}
return answer;
}
public static Builder builder() {
return new Builder();
}
public static class Builder implements EzyBuilder {
protected int readTimeout;
protected int connectTimeout;
protected ObjectMapper objectMapper;
protected Object stringConverter;
protected DataConverters dataConverters;
protected final List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy