nbbrd.io.curl.Curl Maven / Gradle / Ivy
Show all versions of java-io-curl Show documentation
package nbbrd.io.curl;
import lombok.NonNull;
import nbbrd.design.BuilderPattern;
import nbbrd.design.VisibleForTesting;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.util.*;
@lombok.experimental.UtilityClass
class Curl {
public static final int CURL_UNSUPPORTED_PROTOCOL = 1;
public static final int CURL_COULD_NOT_RESOLVE_HOST = 6;
public static final int CURL_OPERATION_TIMEOUT = 28;
public static final int CURL_FAILURE_RECEIVING = 56;
@VisibleForTesting
@lombok.Value
static class Status {
int code;
String message;
}
@VisibleForTesting
@lombok.Value
static class Head {
@NonNull
Status status;
@NonNull
SortedMap> headers;
public static LinkedList parseResponse(BufferedReader reader) throws IOException {
LinkedList result = new LinkedList<>();
String line = reader.readLine();
while (line != null) {
Status status = parseStatusLine(line);
SortedMap> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
while ((line = reader.readLine()) != null && !line.isEmpty()) {
parseHeaders(line, headers);
}
if (line != null) {
// flush empty line
line = reader.readLine();
}
result.add(new Head(status, Collections.unmodifiableSortedMap(headers)));
}
return result;
}
private static char SP = 32;
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#status_line
private static Status parseStatusLine(String statusLine) {
if (statusLine == null) {
return new Status(-1, null);
}
int codeStart = statusLine.indexOf(SP);
if (codeStart == -1) {
return new Status(-1, null);
}
int codeEnd = statusLine.indexOf(SP, codeStart + 1);
if (codeEnd == -1) {
return new Status(Integer.parseInt(statusLine.substring(codeStart + 1)), null);
} else {
return new Status(Integer.parseInt(statusLine.substring(codeStart + 1, codeEnd)), statusLine.substring(codeEnd + 1));
}
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#headers_2
private static void parseHeaders(String line, SortedMap> result) {
int index = line.indexOf(":");
if (index != -1) {
String key = line.substring(0, index);
String value = line.substring(index + 1).trim();
if (!value.isEmpty()) {
result.computeIfAbsent(key, ignore -> new ArrayList<>()).add(value);
}
}
}
}
@lombok.Value
@lombok.Builder
static class Version {
@lombok.Singular
List lines;
public static Version parseText(BufferedReader reader) throws IOException {
Builder result = new Builder();
try {
reader.lines().forEach(result::line);
} catch (UncheckedIOException ex) {
throw ex.getCause();
}
return result.build();
}
public static final class Builder {
// fix error when generating Javadoc
}
}
// https://curl.se/docs/manpage.html
@BuilderPattern(String[].class)
static final class CommandBuilder {
public static final String STDOUT_FILENAME = "-";
private final List items;
public CommandBuilder() {
this.items = new ArrayList<>();
items.add("curl");
}
private CommandBuilder push(String item) {
items.add(item);
return this;
}
/**
* Change the method to use when starting the transfer.
*
* curl passes on the verbatim string you give it the request without any filter or other safeguards.
* That includes white space and control characters.
*
* @param method the method to use
* @return this builder
* @see curl man page
*/
public CommandBuilder request(String method) {
return isDefaultMethod(method) ? this : push("-X").push(method);
}
/**
* The URL syntax is protocol-dependent. You find a detailed description in RFC 3986.
*
* If you provide a URL without a leading protocol:// scheme, curl guesses what protocol you want.
* It then defaults to HTTP but assumes others based on often-used host name prefixes.
* For example, for host names starting with "ftp." curl assumes you want FTP.
*
* @param url a non-null URL
* @return this builder
* @see curl man page
*/
public CommandBuilder url(URL url) {
return push(url.toString());
}
/**
* Use the specified proxy.
*
* @param proxy the specified proxy
* @return this builder
* @see curl man page
*/
public CommandBuilder proxy(Proxy proxy) {
if (hasProxy(proxy)) {
InetSocketAddress address = (InetSocketAddress) proxy.address();
push("-x").push(address.getHostString() + ":" + address.getPort());
}
return this;
}
/**
* Write output to instead of stdout.
*
* @param file a non-null file
* @return this builder
* @see curl man page
*/
public CommandBuilder output(File file) {
return push("-o").push(file.toString());
}
/**
* Silent or quiet mode. Do not show progress meter or error messages. Makes Curl mute.
* It still outputs the data you ask for, potentially even to the terminal/stdout unless you redirect it.
*
* @param silent true if silent, false otherwise
* @return this builder
* @see curl man page
*/
public CommandBuilder silent(boolean silent) {
return silent ? push("-s") : this;
}
/**
* Write the received protocol headers to the specified file.
* If no headers are received, the use of this option creates an empty file.
*
* @param filename the specified file
* @return this builder
* @see curl man page
*/
public CommandBuilder dumpHeader(String filename) {
return push("-D").push(filename);
}
/**
* Maximum time in seconds that you allow curl's connection to take.
* This only limits the connection phase, so if curl connects within the given period it continues - if not it exits.
*
* This option accepts decimal values.
* The decimal value needs to be provided using a dot (.) as decimal separator - not the local version even if it might be using another separator.
*
* The connection phase is considered complete when the DNS lookup and requested TCP, TLS or QUIC handshakes are done.
*
* @param seconds time in seconds
* @return this builder
* @see curl man page
*/
public CommandBuilder connectTimeout(float seconds) {
return push("--connect-timeout").push(fixNumericalParameter(seconds));
}
/**
* Maximum time in seconds that you allow each transfer to take.
* This is useful for preventing your batch jobs from hanging for hours due to slow networks or links going down.
* This option accepts decimal values.
*
* @param seconds time in seconds
* @return this builder
* @see curl man page
*/
public CommandBuilder maxTime(float seconds) {
return push("-m").push(fixNumericalParameter(seconds));
}
/**
* (Schannel) This option tells curl to ignore certificate revocation checks when they failed due to missing/offline distribution points for the revocation check lists.
*
* @param sslRevokeBestEffort true if certificate revocation is ignored, false otherwise
* @return this builder
* @see curl man page
*/
@MinVersion("7.70.0")
public CommandBuilder sslRevokeBestEffort(boolean sslRevokeBestEffort) {
return sslRevokeBestEffort ? push("--ssl-revoke-best-effort") : this;
}
/**
* By default, every secure connection curl makes is verified to be secure before the transfer takes place.
* This option makes curl skip the verification step and proceed without checking.
*
* @param insecure true if secure connection verification are skipped, false otherwise
* @return this builder
* @see curl man page
*/
public CommandBuilder insecure(boolean insecure) {
return insecure ? push("-k") : this;
}
/**
* Extra header to include in information sent. When used within an HTTP request, it is added to the regular request headers.
*
* @param key key part of the header
* @param value value part of the header
* @return this builder
* @see curl man page
*/
public CommandBuilder header(String key, String value) {
return push("-H").push(key + ": " + value);
}
public CommandBuilder headers(Map> headers) {
headers.forEach((key, values) -> values.forEach(value -> header(key, value)));
return this;
}
/**
* Displays information about curl and the libcurl version it uses.
*
* @return this builder
* @see curl man page
*/
public CommandBuilder version() {
return push("-V");
}
/**
* Tells curl to use HTTP version 1.1.
*
* @return this builder
* @see curl man page
*/
@MinVersion("7.33.0")
public CommandBuilder http1_1() {
return push("--http1.1");
}
/**
* This posts data similarly to -d, --data but without the special interpretation of the @ character.
*
* @param data the data to be posted
* @return this builder
* @see curl man page
*/
public CommandBuilder dataRaw(@Nullable String data) {
return data != null ? push("--data-raw").push(data) : this;
}
/**
* This posts data exactly as specified with no extra processing whatsoever.
*
* @param data the file containing the data to be posted
* @return this builder
* @see curl man page
*/
public CommandBuilder dataBinary(@Nullable File data) {
return data != null ? push("--data-binary").push("@" + data) : this;
}
/**
* If the server reports that the requested page has moved to a different location
* (indicated with a Location: header and a 3XX response code),
* this option makes curl redo the request on the new place.
*
* @param location true if location header should be followed, false otherwise
* @return this builder
* @see curl man page
*/
public CommandBuilder location(boolean location) {
return location ? push("-L") : this;
}
/**
* Set maximum number of redirections to follow. When -L, --location is used,
* to prevent curl from following too many redirects, by default, the limit is set to 50 redirects.
* Set this option to -1 to make it unlimited.
*
* @param maxRedirs the maximum number of redirections to follow
* @return this builder
* @see curl man page
*/
public CommandBuilder maxRedirs(int maxRedirs) {
return push("--max-redirs").push(Integer.toString(maxRedirs));
}
/**
* Tell curl to not handle sequences of /../ or /./ in the given URL path.
* Normally curl squashes or merges them according to standards but with this option set you tell it not to do that.
*
* @return this builder
* @see curl man page
*/
@MinVersion("7.42.0")
public CommandBuilder pathAsIs() {
return push("--path-as-is");
}
public String[] build() {
return items.toArray(new String[0]);
}
// some old versions don't accept decimal values!
private String fixNumericalParameter(float seconds) {
return Integer.toString((int) seconds);
}
private boolean isDefaultMethod(String method) {
return method.equals("GET");
}
}
private @interface MinVersion {
String value();
}
@VisibleForTesting
static boolean hasProxy(@NonNull Proxy proxy) {
return !proxy.equals(Proxy.NO_PROXY);
}
}