diff --git a/src/main/java/cokr/xit/foundation/data/JSON.java b/src/main/java/cokr/xit/foundation/data/JSON.java index c68aca2..406b158 100644 --- a/src/main/java/cokr/xit/foundation/data/JSON.java +++ b/src/main/java/cokr/xit/foundation/data/JSON.java @@ -78,6 +78,8 @@ public class JSON extends AbstractComponent { * @return 객체를 변환한 JSON 문자열 */ public String stringify(Object obj, boolean indent) { + if (obj instanceof String) + return (String)obj; try { return getObjectMapper().writeValueAsString(obj); } catch (Exception e) { diff --git a/src/main/java/cokr/xit/foundation/web/WebClient.java b/src/main/java/cokr/xit/foundation/web/WebClient.java index 84e06e5..26eb940 100644 --- a/src/main/java/cokr/xit/foundation/web/WebClient.java +++ b/src/main/java/cokr/xit/foundation/web/WebClient.java @@ -2,6 +2,7 @@ package cokr.xit.foundation.web; import java.io.FileOutputStream; import java.io.InputStream; +import java.math.BigInteger; import java.net.Authenticator; import java.net.ProxySelector; import java.net.URI; @@ -9,12 +10,19 @@ import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -74,7 +82,8 @@ import cokr.xit.foundation.data.JSON; public class WebClient { private static final String FORM_DATA = "application/x-www-form-urlencoded", - JSON_DATA = "application/json"; + JSON_DATA = "application/json", + XML_DATA = "text/xml"; // MULTIPART = "multipart/form-data"; private HttpClient.Version version = HttpClient.Version.HTTP_2; @@ -238,14 +247,38 @@ public class WebClient { * @author mjkhan */ public static class Request { + public static enum ContentType { + FORM("application/x-www-form-urlencoded"), + JSON("application/json"), + XML("text/xml"), + PLAIN("text/plain"), + MULTIPART("multipart/form-data"); + + private final String type; + + private ContentType(String type) { + this.type = type; + } + + public static ContentType typeOf(String type) { + if (Assert.isEmpty(type)) return null; + + for (ContentType content: values()) + if (type.equals(content.type)) + return content; + + throw new IllegalArgumentException(type); + } + } + private String uri; private boolean async, - json, +// json, download; private Charset charset = StandardCharsets.UTF_8; private LinkedHashMap headers; - private LinkedHashMap data; + private LinkedHashMap keyValues; private Consumer> textHandler = hresp -> { HttpHeaders headers = hresp.headers(); headers.map().forEach((k, v) -> System.out.println(k + " = " + v)); @@ -269,6 +302,21 @@ public class WebClient { return this; } + public Request contentType(ContentType type) { + return header("Content-Type", type.type); + } + + private Request.ContentType contentType() { + if (headers == null) return null; + + List found = headers.entrySet().stream() + .filter(entry -> "Content-Type".equals(entry.getKey())) + .map(entry -> entry.getValue()) + .toList(); + String type = !found.isEmpty() ? found.get(0) : null; + return ContentType.typeOf(type); + } + /**요청의 uri를 설정한다. * @param uri 요청 대상 서버의 uri * @return 현재 Request @@ -305,11 +353,11 @@ public class WebClient { *
  • 그렇지 않으면 false
  • * * @return 현재 Request - */ public Request json(boolean json) { this.json = json; return this; } + */ /**요청의 응답이 파일인지 설정한다. 디폴트는 false(텍스트). * @param download @@ -323,18 +371,38 @@ public class WebClient { return this; } + private Object bodyData() { + if (Assert.isEmpty(keyValues)) return null; + + Object value = keyValues.remove("body"); + if (value != null) + return value; + + return null; + } + /**요청으로 전달할 데이터를 설정한다. * @param key 데이터 키(이름) * @param value 데이터 값 * @return 현재 Request */ public Request data(String key, Object value) { - if (data == null) - data = new LinkedHashMap<>(); - data.put(key, value); + if (keyValues == null) + keyValues = new LinkedHashMap<>(); + keyValues.put(key, value); return this; } + /**요청으로 전달할 데이터를 설정한다. + * @param data 데이터 값 + * @return 현재 Request + */ + public Request bodyData(Object value) { + if (keyValues != null) + keyValues.clear(); + return data("body", value); + } + /**비동기 요청의 텍스트 응답을 처리하는 핸들러를 설정한다. * @param handler 비동기 요청의 텍스트 응답을 처리하는 핸들러 * @return 현재 Request @@ -369,9 +437,10 @@ public class WebClient { HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(uri + queryString)) .GET(); - +/* if (json) builder.header("Accept", JSON_DATA); +*/ if (headers != null) headers.forEach((k, v) -> builder.header(k, v)); @@ -379,16 +448,23 @@ public class WebClient { } private String getParams() { - if (data == null) return ""; + if (keyValues == null) return ""; - List params = data.entrySet().stream() + List params = keyValues.entrySet().stream() .map(entry -> String.format("%s=%s", encode(entry.getKey()), encode((String)entry.getValue()))) .toList(); return String.join("&", params); } private String inJSON() { - return data != null ? new JSON().stringify(data) : ""; + Object body = bodyData(); + if (!Assert.isEmpty(body)) + return new JSON().stringify(body); + + if (!Assert.isEmpty(keyValues)) + return new JSON().stringify(keyValues); + + return ""; } private String encode(String str) { @@ -396,14 +472,61 @@ public class WebClient { } HttpRequest post() { - HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(uri)) - .header("Content-Type", !json ? FORM_DATA : JSON_DATA) - .POST(HttpRequest.BodyPublishers.ofString(!json ? getParams() : inJSON())); + try { + ContentType contentType = contentType(); + HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(uri)); + if (contentType != null) + builder.header("Content-Type", contentType.type); + builder.POST(bodyPublisher(contentType)); +// .header("Content-Type", !json ? FORM_DATA : JSON_DATA) +// .POST(HttpRequest.BodyPublishers.ofString(!json ? getParams() : inJSON())); if (headers != null) headers.forEach((k, v) -> builder.header(k, v)); return builder.build(); + } catch (Exception e) { + throw Assert.runtimeException(e); + } + } + + private BodyPublisher bodyPublisher(ContentType type) throws Exception { + if (type == null) + return HttpRequest.BodyPublishers.noBody(); + + switch (type) { + case JSON: return HttpRequest.BodyPublishers.ofString(inJSON()); + case XML: + case PLAIN: + Object value = bodyData(); + return HttpRequest.BodyPublishers.ofString(value != null ? value.toString() : ""); + case MULTIPART: return multipartPublisher(); + default: return HttpRequest.BodyPublishers.ofString(getParams()); + } + } + + private BodyPublisher multipartPublisher() throws Exception { + String boundary = new BigInteger(64, new Random()).toString(); + byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=").getBytes(charset); + ArrayList byteList = new ArrayList<>(); + + for (Map.Entry entry: keyValues.entrySet()) { + byteList.add(separator); + + Object value = entry.getValue(); + if (value instanceof Path) { + Path path = (Path)value; + String mimeType = Files.probeContentType(path); + byteList.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName() + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + byteList.add(Files.readAllBytes(path)); + byteList.add("\r\n".getBytes(charset)); + } else { + byteList.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n").getBytes(charset)); + } + } + byteList.add(("--" + boundary + "--\r\n").getBytes(charset)); + + return BodyPublishers.ofByteArrays(byteList); } } } \ No newline at end of file