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 okhttp3.*;
026
027import java.io.IOException;
028import java.io.UnsupportedEncodingException;
029import java.net.URLEncoder;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
034public class HttpNode extends BaseNode {
035
036    private String url;
037    private String method;
038
039    private List<Parameter> headers;
040    private List<Parameter> urlParameters;
041
042    private String bodyDataType;
043    private List<Parameter> fromData;
044    private List<Parameter> fromUrlencoded;
045    private String jsonBody;
046    private String rawBody;
047
048    public String getUrl() {
049        return url;
050    }
051
052    public void setUrl(String url) {
053        this.url = url;
054    }
055
056    public String getMethod() {
057        return method;
058    }
059
060    public void setMethod(String method) {
061        this.method = method;
062    }
063
064    public List<Parameter> getHeaders() {
065        return headers;
066    }
067
068    public void setHeaders(List<Parameter> headers) {
069        this.headers = headers;
070    }
071
072    public List<Parameter> getUrlParameters() {
073        return urlParameters;
074    }
075
076    public void setUrlParameters(List<Parameter> urlParameters) {
077        this.urlParameters = urlParameters;
078    }
079
080    public String getBodyDataType() {
081        return bodyDataType;
082    }
083
084    public void setBodyDataType(String bodyDataType) {
085        this.bodyDataType = bodyDataType;
086    }
087
088    public List<Parameter> getFromData() {
089        return fromData;
090    }
091
092    public void setFromData(List<Parameter> fromData) {
093        this.fromData = fromData;
094    }
095
096    public List<Parameter> getFromUrlencoded() {
097        return fromUrlencoded;
098    }
099
100    public void setFromUrlencoded(List<Parameter> fromUrlencoded) {
101        this.fromUrlencoded = fromUrlencoded;
102    }
103
104    public String getJsonBody() {
105        return jsonBody;
106    }
107
108    public void setJsonBody(String jsonBody) {
109        this.jsonBody = jsonBody;
110    }
111
112    public String getRawBody() {
113        return rawBody;
114    }
115
116    public void setRawBody(String rawBody) {
117        this.rawBody = rawBody;
118    }
119
120
121    @Override
122    protected Map<String, Object> execute(Chain chain) {
123
124        Map<String, Object> urlDataMap = getChainParameters(chain, urlParameters);
125        String parametersString = mapToQueryString(urlDataMap);
126        String newUrl = parametersString.isEmpty() ? url : url +
127                (url.contains("?") ? "&" + parametersString : "?" + parametersString);
128
129        Request.Builder reqBuilder = new Request.Builder()
130                .url(newUrl);
131
132        Map<String, Object> headersMap = getChainParameters(chain, headers);
133        headersMap.forEach((s, o) -> reqBuilder.addHeader(s, String.valueOf(o)));
134
135        if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) {
136            reqBuilder.method("GET", null);
137        } else {
138            reqBuilder.method(method.toUpperCase(), getRequestBody(chain));
139        }
140
141
142        OkHttpClient okHttpClient = OkHttpClientUtil.buildDefaultClient();
143        try (Response response = okHttpClient.newCall(reqBuilder.build()).execute()) {
144
145            Map<String, Object> result = new HashMap<>();
146            result.put("statusCode", response.code());
147
148            Map<String, String> responseHeaders = new HashMap<>();
149            for (String name : response.headers().names()) {
150                responseHeaders.put(name, response.header(name));
151            }
152            result.put("headers", responseHeaders);
153
154            ResponseBody body = response.body();
155            String bodyString = null;
156            if (body != null) bodyString = body.string();
157
158            if (StringUtil.hasText(bodyString)) {
159                DataType outDataType = null;
160                List<Parameter> outputDefs = getOutputDefs();
161                if (outputDefs != null) {
162                    for (Parameter outputDef : outputDefs) {
163                        if ("body".equalsIgnoreCase(outputDef.getName())) {
164                            outDataType = outputDef.getDataType();
165                            break;
166                        }
167                    }
168                }
169
170                if (outDataType != null && (outDataType == DataType.Object || outDataType
171                        .getValue().startsWith("Array"))) {
172                    result.put("body", JSON.parse(bodyString));
173                } else {
174                    result.put("body", bodyString);
175                }
176            }
177            return result;
178        } catch (IOException e) {
179            throw new RuntimeException(e);
180        }
181    }
182
183    private RequestBody getRequestBody(Chain chain) {
184        if ("json".equals(bodyDataType)) {
185            return RequestBody.create(jsonBody, MediaType.parse("application/json"));
186        }
187
188        if ("x-www-form-urlencoded".equals(bodyDataType)) {
189            Map<String, Object> formUrlencodedMap = getChainParameters(chain, fromUrlencoded);
190            String bodyString = mapToQueryString(formUrlencodedMap);
191            return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
192        }
193
194        if ("form-data".equals(bodyDataType)) {
195            Map<String, Object> formDataMap = getChainParameters(chain, fromData);
196
197            MultipartBody.Builder builder = new MultipartBody.Builder()
198                    .setType(MultipartBody.FORM);
199
200            formDataMap.forEach((s, o) -> {
201//                if (o instanceof File) {
202//                    File f = (File) o;
203//                    RequestBody body = RequestBody.create(f, MediaType.parse("application/octet-stream"));
204//                    builder.addFormDataPart(s, f.getName(), body);
205//                } else if (o instanceof InputStream) {
206//                    RequestBody body = new HttpClient.InputStreamRequestBody(MediaType.parse("application/octet-stream"), (InputStream) o);
207//                    builder.addFormDataPart(s, s, body);
208//                } else if (o instanceof byte[]) {
209//                    builder.addFormDataPart(s, s, RequestBody.create((byte[]) o));
210//                } else {
211//                    builder.addFormDataPart(s, String.valueOf(o));
212//                }
213                builder.addFormDataPart(s, String.valueOf(o));
214            });
215
216            return builder.build();
217        }
218
219        if ("raw".equals(bodyDataType)) {
220            return RequestBody.create(rawBody, null);
221        }
222        //none
223        return RequestBody.create("", null);
224    }
225
226    public static String mapToQueryString(Map<String, Object> map) {
227        if (map == null || map.isEmpty()) {
228            return "";
229        }
230
231        StringBuilder stringBuilder = new StringBuilder();
232
233        for (String key : map.keySet()) {
234            if (StringUtil.noText(key)) {
235                continue;
236            }
237            if (stringBuilder.length() > 0) {
238                stringBuilder.append("&");
239            }
240            stringBuilder.append(key.trim());
241            stringBuilder.append("=");
242            Object value = map.get(key);
243            stringBuilder.append(value == null ? "" : urlEncode(value.toString().trim()));
244        }
245        return stringBuilder.toString();
246    }
247
248    public static String urlEncode(String string) {
249        try {
250            return URLEncoder.encode(string, "UTF-8");
251        } catch (UnsupportedEncodingException e) {
252            throw new RuntimeException(e);
253        }
254    }
255
256    @Override
257    public String toString() {
258        return "HttpNode{" +
259                "url='" + url + '\'' +
260                ", method='" + method + '\'' +
261                ", headers=" + headers +
262                ", parameters=" + urlParameters +
263                ", bodyDataType='" + bodyDataType + '\'' +
264                ", fromData=" + fromData +
265                ", fromUrlencoded=" + fromUrlencoded +
266                ", jsonBody='" + jsonBody + '\'' +
267                ", rawBody='" + rawBody + '\'' +
268                ", description='" + description + '\'' +
269                ", parameter=" + urlParameters +
270                ", outputDefs=" + outputDefs +
271                ", id='" + id + '\'' +
272                ", name='" + name + '\'' +
273                ", async=" + async +
274                ", inwardEdges=" + inwardEdges +
275                ", outwardEdges=" + outwardEdges +
276                ", condition=" + condition +
277                ", memory=" + memory +
278                ", nodeStatus=" + nodeStatus +
279                '}';
280    }
281}