001/**
002 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.gnu.org/licenses/lgpl-3.0.txt
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package dev.tinyflow.core.node;
017
018import com.agentsflex.core.chain.Chain;
019import com.agentsflex.core.chain.DataType;
020import com.agentsflex.core.chain.Parameter;
021import com.agentsflex.core.chain.node.BaseNode;
022import com.agentsflex.core.llm.client.OkHttpClientUtil;
023import com.agentsflex.core.util.StringUtil;
024import com.alibaba.fastjson.JSON;
025import com.alibaba.fastjson.JSONObject;
026import okhttp3.MediaType;
027import okhttp3.MultipartBody;
028import okhttp3.OkHttpClient;
029import okhttp3.Request;
030import okhttp3.RequestBody;
031import okhttp3.Response;
032import okhttp3.ResponseBody;
033
034import java.io.IOException;
035import java.io.UnsupportedEncodingException;
036import java.net.URLEncoder;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040
041public class HttpNode extends BaseNode {
042
043    private String url;
044    private String method;
045
046    private List<Parameter> headers;
047    private List<Parameter> urlParameters;
048
049    private String bodyType;
050    private List<Parameter> fromData;
051    private List<Parameter> fromUrlencoded;
052    private String bodyJson;
053    private String rawBody;
054
055    public static String mapToQueryString(Map<String, Object> map) {
056        if (map == null || map.isEmpty()) {
057            return "";
058        }
059
060        StringBuilder stringBuilder = new StringBuilder();
061
062        for (String key : map.keySet()) {
063            if (StringUtil.noText(key)) {
064                continue;
065            }
066            if (stringBuilder.length() > 0) {
067                stringBuilder.append("&");
068            }
069            stringBuilder.append(key.trim());
070            stringBuilder.append("=");
071            Object value = map.get(key);
072            stringBuilder.append(value == null ? "" : urlEncode(value.toString().trim()));
073        }
074        return stringBuilder.toString();
075    }
076
077    public static String urlEncode(String string) {
078        try {
079            return URLEncoder.encode(string, "UTF-8");
080        } catch (UnsupportedEncodingException e) {
081            throw new RuntimeException(e);
082        }
083    }
084
085    public String getUrl() {
086        return url;
087    }
088
089    public void setUrl(String url) {
090        this.url = url;
091    }
092
093    public String getMethod() {
094        return method;
095    }
096
097    public void setMethod(String method) {
098        this.method = method;
099    }
100
101    public List<Parameter> getHeaders() {
102        return headers;
103    }
104
105    public void setHeaders(List<Parameter> headers) {
106        this.headers = headers;
107    }
108
109    public List<Parameter> getUrlParameters() {
110        return urlParameters;
111    }
112
113    public void setUrlParameters(List<Parameter> urlParameters) {
114        this.urlParameters = urlParameters;
115    }
116
117    public String getBodyType() {
118        return bodyType;
119    }
120
121    public void setBodyType(String bodyType) {
122        this.bodyType = bodyType;
123    }
124
125    public List<Parameter> getFromData() {
126        return fromData;
127    }
128
129    public void setFromData(List<Parameter> fromData) {
130        this.fromData = fromData;
131    }
132
133    public List<Parameter> getFromUrlencoded() {
134        return fromUrlencoded;
135    }
136
137    public void setFromUrlencoded(List<Parameter> fromUrlencoded) {
138        this.fromUrlencoded = fromUrlencoded;
139    }
140
141    public String getBodyJson() {
142        return bodyJson;
143    }
144
145    public void setBodyJson(String bodyJson) {
146        this.bodyJson = bodyJson;
147    }
148
149    public String getRawBody() {
150        return rawBody;
151    }
152
153    public void setRawBody(String rawBody) {
154        this.rawBody = rawBody;
155    }
156
157    @Override
158    protected Map<String, Object> execute(Chain chain) {
159
160        Map<String, Object> urlDataMap = chain.getParameterValues(this, urlParameters);
161        String parametersString = mapToQueryString(urlDataMap);
162        String newUrl = "POST".equalsIgnoreCase(method) || parametersString.isEmpty() ? url : url +
163                (url.contains("?") ? "&" + parametersString : "?" + parametersString);
164
165        Request.Builder reqBuilder = new Request.Builder().url(newUrl);
166
167        Map<String, Object> headersMap = chain.getParameterValues(this, headers);
168        headersMap.forEach((s, o) -> reqBuilder.addHeader(s, String.valueOf(o)));
169
170        if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) {
171            reqBuilder.method("GET", null);
172        } else {
173            reqBuilder.method(method.toUpperCase(), getRequestBody(chain, urlDataMap));
174        }
175
176
177        OkHttpClient okHttpClient = OkHttpClientUtil.buildDefaultClient();
178        try (Response response = okHttpClient.newCall(reqBuilder.build()).execute()) {
179
180            Map<String, Object> result = new HashMap<>();
181            result.put("statusCode", response.code());
182
183            Map<String, String> responseHeaders = new HashMap<>();
184            for (String name : response.headers().names()) {
185                responseHeaders.put(name, response.header(name));
186            }
187            result.put("headers", responseHeaders);
188
189            ResponseBody body = response.body();
190            String bodyString = null;
191            if (body != null) bodyString = body.string();
192
193            if (StringUtil.hasText(bodyString)) {
194                DataType outDataType = null;
195                List<Parameter> outputDefs = getOutputDefs();
196                if (outputDefs != null) {
197                    for (Parameter outputDef : outputDefs) {
198                        if ("body".equalsIgnoreCase(outputDef.getName())) {
199                            outDataType = outputDef.getDataType();
200                            break;
201                        }
202                    }
203                }
204
205                if (outDataType != null && (outDataType == DataType.Object || outDataType
206                        .getValue().startsWith("Array"))) {
207                    result.put("body", JSON.parse(bodyString));
208                } else {
209                    result.put("body", bodyString);
210                }
211            }
212            return result;
213        } catch (IOException e) {
214            throw new RuntimeException(e);
215        }
216    }
217
218    private RequestBody getRequestBody(Chain chain, Map<String, Object> urlDataMap) {
219        if ("json".equals(bodyType)) {
220            JSONObject object = JSON.parseObject(bodyJson);
221            object.putAll(urlDataMap);
222            return RequestBody.create(JSON.toJSONString(object), MediaType.parse("application/json"));
223        }
224
225        if ("x-www-form-urlencoded".equals(bodyType)) {
226            Map<String, Object> formUrlencodedMap = chain.getParameterValues(this, fromUrlencoded);
227            String bodyString = mapToQueryString(formUrlencodedMap);
228            return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
229        }
230
231        if ("form-data".equals(bodyType)) {
232            Map<String, Object> formDataMap = chain.getParameterValues(this, fromData);
233
234            MultipartBody.Builder builder = new MultipartBody.Builder()
235                    .setType(MultipartBody.FORM);
236
237            formDataMap.forEach((s, o) -> {
238//                if (o instanceof File) {
239//                    File f = (File) o;
240//                    RequestBody body = RequestBody.create(f, MediaType.parse("application/octet-stream"));
241//                    builder.addFormDataPart(s, f.getName(), body);
242//                } else if (o instanceof InputStream) {
243//                    RequestBody body = new HttpClient.InputStreamRequestBody(MediaType.parse("application/octet-stream"), (InputStream) o);
244//                    builder.addFormDataPart(s, s, body);
245//                } else if (o instanceof byte[]) {
246//                    builder.addFormDataPart(s, s, RequestBody.create((byte[]) o));
247//                } else {
248//                    builder.addFormDataPart(s, String.valueOf(o));
249//                }
250                builder.addFormDataPart(s, String.valueOf(o));
251            });
252
253            return builder.build();
254        }
255
256        if ("raw".equals(bodyType)) {
257            return RequestBody.create(rawBody, null);
258        }
259        //none
260        return RequestBody.create("", null);
261    }
262
263    @Override
264    public String toString() {
265        return "HttpNode{" +
266                "url='" + url + '\'' +
267                ", method='" + method + '\'' +
268                ", headers=" + headers +
269                ", parameters=" + urlParameters +
270                ", bodyType='" + bodyType + '\'' +
271                ", fromData=" + fromData +
272                ", fromUrlencoded=" + fromUrlencoded +
273                ", bodyJson='" + bodyJson + '\'' +
274                ", rawBody='" + rawBody + '\'' +
275                ", description='" + description + '\'' +
276                ", parameter=" + urlParameters +
277                ", outputDefs=" + outputDefs +
278                ", id='" + id + '\'' +
279                ", name='" + name + '\'' +
280                ", async=" + async +
281                ", inwardEdges=" + inwardEdges +
282                ", outwardEdges=" + outwardEdges +
283                ", condition=" + condition +
284                ", memory=" + memory +
285                ", nodeStatus=" + nodeStatus +
286                '}';
287    }
288}