|
|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
package cokr.xit.foundation.web;
|
|
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.FileOutputStream;
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
import java.math.BigInteger;
|
|
|
|
|
@ -7,6 +8,7 @@ import java.net.Authenticator;
|
|
|
|
|
import java.net.CookieHandler;
|
|
|
|
|
import java.net.ProxySelector;
|
|
|
|
|
import java.net.URI;
|
|
|
|
|
import java.net.URLConnection;
|
|
|
|
|
import java.net.URLEncoder;
|
|
|
|
|
import java.net.http.HttpClient;
|
|
|
|
|
import java.net.http.HttpHeaders;
|
|
|
|
|
@ -29,9 +31,9 @@ import java.util.function.Consumer;
|
|
|
|
|
|
|
|
|
|
import javax.net.ssl.SSLContext;
|
|
|
|
|
|
|
|
|
|
import cokr.xit.foundation.Assert;
|
|
|
|
|
import cokr.xit.foundation.Log;
|
|
|
|
|
import cokr.xit.foundation.AbstractComponent;
|
|
|
|
|
import cokr.xit.foundation.data.JSON;
|
|
|
|
|
import cokr.xit.foundation.data.Named;
|
|
|
|
|
|
|
|
|
|
/**서버와의 http 통신을 지원하는 유틸리티.
|
|
|
|
|
* <p>WebClient는 {@link #get(Consumer) GET}, 또는 {@link #post(Consumer) POST} 방식의 통신을 지원한다. {@link Request 요청의 설정}에 따라
|
|
|
|
|
@ -83,7 +85,7 @@ import cokr.xit.foundation.data.JSON;
|
|
|
|
|
* </ul>
|
|
|
|
|
* @author mjkhan
|
|
|
|
|
*/
|
|
|
|
|
public class WebClient {
|
|
|
|
|
public class WebClient extends AbstractComponent {
|
|
|
|
|
/**http 통신의 로그를 출력하도록 설정한다.
|
|
|
|
|
*/
|
|
|
|
|
public static void debug() {
|
|
|
|
|
@ -147,7 +149,7 @@ public class WebClient {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Charset charset() {
|
|
|
|
|
return Assert.ifEmpty(charset, StandardCharsets.UTF_8);
|
|
|
|
|
return ifEmpty(charset, StandardCharsets.UTF_8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**문자셋을 설정한다. 디폴트는 UTF-8.
|
|
|
|
|
@ -216,10 +218,10 @@ public class WebClient {
|
|
|
|
|
return (HttpResponse<T>)handleRequest(req.async, hreq, HttpResponse.BodyHandlers.ofString(), req.textHandler);
|
|
|
|
|
} catch (Throwable e) {
|
|
|
|
|
if (req.async) {
|
|
|
|
|
req.errorHandler.accept(Assert.rootCause(e));
|
|
|
|
|
req.errorHandler.accept(rootCause(e));
|
|
|
|
|
return null;
|
|
|
|
|
} else
|
|
|
|
|
throw Assert.runtimeException(e);
|
|
|
|
|
throw runtimeException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -240,10 +242,6 @@ public class WebClient {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Log log() {
|
|
|
|
|
return Log.get(WebClient.class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**"POST" 방식의 요청을 전송한다.
|
|
|
|
|
* @param <T> 요청의 body 핸들러 유형
|
|
|
|
|
* @param configurer "POST" 방식 요청의 설정자
|
|
|
|
|
@ -263,10 +261,10 @@ public class WebClient {
|
|
|
|
|
return (HttpResponse<T>)handleRequest(req.async, hreq, HttpResponse.BodyHandlers.ofString(), req.textHandler);
|
|
|
|
|
} catch (Throwable e) {
|
|
|
|
|
if (req.async) {
|
|
|
|
|
req.errorHandler.accept(Assert.rootCause(e));
|
|
|
|
|
req.errorHandler.accept(rootCause(e));
|
|
|
|
|
return null;
|
|
|
|
|
} else
|
|
|
|
|
throw Assert.runtimeException(e);
|
|
|
|
|
throw runtimeException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -281,7 +279,7 @@ public class WebClient {
|
|
|
|
|
) {
|
|
|
|
|
inputStream.transferTo(out);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw Assert.runtimeException(e);
|
|
|
|
|
throw runtimeException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -289,7 +287,7 @@ public class WebClient {
|
|
|
|
|
try {
|
|
|
|
|
return new String(inputStream.readAllBytes(), charset());
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw Assert.runtimeException(e);
|
|
|
|
|
throw runtimeException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -327,7 +325,7 @@ public class WebClient {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static ContentType typeOf(String type) {
|
|
|
|
|
if (Assert.isEmpty(type)) return null;
|
|
|
|
|
if (isEmpty(type)) return null;
|
|
|
|
|
|
|
|
|
|
for (ContentType content: values())
|
|
|
|
|
if (type.contains(content.type))
|
|
|
|
|
@ -348,9 +346,9 @@ public class WebClient {
|
|
|
|
|
private JSON json;
|
|
|
|
|
private Consumer<HttpResponse<String>> textHandler = hresp -> {
|
|
|
|
|
HttpHeaders headers = hresp.headers();
|
|
|
|
|
headers.map().forEach((k, v) -> log().debug("{} = {}", k, v));
|
|
|
|
|
log().debug("status: {}", hresp.statusCode());
|
|
|
|
|
log().debug("{}", hresp.body());
|
|
|
|
|
headers.map().forEach((k, v) -> log(WebClient.class).debug("{} = {}", k, v));
|
|
|
|
|
log(WebClient.class).debug("status: {}", hresp.statusCode());
|
|
|
|
|
log(WebClient.class).debug("{}", hresp.body());
|
|
|
|
|
};
|
|
|
|
|
private Consumer<HttpResponse<InputStream>> downloadHandler = hresp -> {};
|
|
|
|
|
private Consumer<Throwable> errorHandler = t -> {
|
|
|
|
|
@ -374,7 +372,7 @@ public class WebClient {
|
|
|
|
|
* @return 현재 Request
|
|
|
|
|
*/
|
|
|
|
|
public Request header(Map<String, String> map) {
|
|
|
|
|
if (!Assert.isEmpty(map))
|
|
|
|
|
if (!isEmpty(map))
|
|
|
|
|
map.forEach(this::header);
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
@ -435,7 +433,7 @@ public class WebClient {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private JSON json() {
|
|
|
|
|
return Assert.ifEmpty(json, () -> json = new JSON());
|
|
|
|
|
return ifEmpty(json, () -> json = new JSON());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**요청의 응답이 파일인지 설정한다. 디폴트는 false(텍스트).
|
|
|
|
|
@ -546,12 +544,12 @@ public class WebClient {
|
|
|
|
|
private String inJSON() {
|
|
|
|
|
Object body = bodyData();
|
|
|
|
|
String str = "";
|
|
|
|
|
if (!Assert.isEmpty(body))
|
|
|
|
|
if (!isEmpty(body))
|
|
|
|
|
str = json().stringify(body);
|
|
|
|
|
else if (!Assert.isEmpty(keyValues))
|
|
|
|
|
else if (!isEmpty(keyValues))
|
|
|
|
|
str = json().stringify(keyValues);
|
|
|
|
|
|
|
|
|
|
log().debug("json request:\n{}", str);
|
|
|
|
|
log(WebClient.class).debug("json request:\n{}", str);
|
|
|
|
|
|
|
|
|
|
return str;
|
|
|
|
|
}
|
|
|
|
|
@ -570,7 +568,7 @@ public class WebClient {
|
|
|
|
|
|
|
|
|
|
return builder.build();
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw Assert.runtimeException(e);
|
|
|
|
|
throw runtimeException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -590,26 +588,77 @@ public class WebClient {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private BodyPublisher multipartPublisher() throws Exception {
|
|
|
|
|
String boundary = new BigInteger(64, new Random()).toString();
|
|
|
|
|
byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=").getBytes(charset);
|
|
|
|
|
String boundary = "==xwc-" + new BigInteger(64, new Random()) + "==";
|
|
|
|
|
header("Content-Type", ContentType.MULTIPART.type + "; boundary=" + boundary);
|
|
|
|
|
|
|
|
|
|
byte[] separator = line("--" + boundary);
|
|
|
|
|
ArrayList<byte[]> byteList = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, Object> entry: keyValues.entrySet()) {
|
|
|
|
|
byteList.add(separator);
|
|
|
|
|
|
|
|
|
|
String key = entry.getKey();
|
|
|
|
|
Object value = entry.getValue();
|
|
|
|
|
if (value instanceof Path path) {
|
|
|
|
|
String mimeType = Files.probeContentType(path);
|
|
|
|
|
byteList.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName() + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n").getBytes(charset));
|
|
|
|
|
byteList.add(Files.readAllBytes(path));
|
|
|
|
|
byteList.add("\r\n".getBytes(charset));
|
|
|
|
|
if (value instanceof Iterable<?> list) {
|
|
|
|
|
for (Object obj: list) {
|
|
|
|
|
byteList.addAll(readBytes(separator, key, obj));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
byteList.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n").getBytes(charset));
|
|
|
|
|
byteList.addAll(readBytes(separator, key, value));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
byteList.add(("--" + boundary + "--\r\n").getBytes(charset));
|
|
|
|
|
byteList.add(line("--" + boundary + "--"));
|
|
|
|
|
byteList.add(line(""));
|
|
|
|
|
// byteList.forEach(bytes -> System.out.print(new String(bytes)));
|
|
|
|
|
|
|
|
|
|
return BodyPublishers.ofByteArrays(byteList);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private byte[] line(String str) {
|
|
|
|
|
return (str + "\r\n").getBytes(charset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<byte[]> readBytes(byte[] separator, String key, Path path) throws Exception {
|
|
|
|
|
String mimeType = Files.probeContentType(path);
|
|
|
|
|
return List.of(
|
|
|
|
|
separator,
|
|
|
|
|
line("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + path.getFileName() + "\""),
|
|
|
|
|
line("Content-Type: " + mimeType),
|
|
|
|
|
line("Content-Transfer-Encodig: binary"),
|
|
|
|
|
line(""),
|
|
|
|
|
Files.readAllBytes(path),
|
|
|
|
|
line(""),
|
|
|
|
|
line("")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<byte[]> readBytes(byte[] separator, String key, String filename, InputStream inputStream) throws Exception {
|
|
|
|
|
String mimeType = URLConnection.guessContentTypeFromStream(inputStream);
|
|
|
|
|
return List.of(
|
|
|
|
|
separator,
|
|
|
|
|
line("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + filename + "\""),
|
|
|
|
|
line("Content-Type: " + mimeType),
|
|
|
|
|
line("Content-Transfer-Encodig: binary"),
|
|
|
|
|
line(""),
|
|
|
|
|
inputStream.readAllBytes(),
|
|
|
|
|
line(""),
|
|
|
|
|
line("")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<byte[]> readBytes(byte[] separator, String key, Object obj) throws Exception {
|
|
|
|
|
if (obj instanceof Path path)
|
|
|
|
|
return readBytes(separator, key, path);
|
|
|
|
|
else if (obj instanceof File file)
|
|
|
|
|
return readBytes(separator, key, file.toPath());
|
|
|
|
|
else if (obj instanceof Named<?> named)
|
|
|
|
|
return readBytes(separator, key, named.getName(), (InputStream)named.getValue());
|
|
|
|
|
else
|
|
|
|
|
return List.of(
|
|
|
|
|
separator,
|
|
|
|
|
line("Content-Disposition: form-data; name=\"" + key + "\""),
|
|
|
|
|
line("Content-Type: text/plain; charset=" + charset),
|
|
|
|
|
line(""),
|
|
|
|
|
line(blankIfEmpty(obj))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|