001package jmri.jmrit.logixng.actions; 002 003import java.beans.*; 004import java.io.*; 005import java.net.HttpURLConnection; 006import java.net.MalformedURLException; 007import java.net.URI; 008import java.net.URISyntaxException; 009import java.net.URL; 010import java.net.URLEncoder; 011import java.nio.charset.Charset; 012import java.util.*; 013 014import javax.net.ssl.HttpsURLConnection; 015 016import jmri.*; 017import jmri.jmrit.logixng.*; 018import jmri.jmrit.logixng.SymbolTable.InitialValueType; 019import jmri.jmrit.logixng.implementation.DefaultSymbolTable; 020import jmri.jmrit.logixng.util.*; 021import jmri.jmrit.logixng.util.parser.ParserException; 022import jmri.util.ThreadingUtil; 023 024/** 025 * This action sends a web request. 026 * 027 * @author Daniel Bergqvist Copyright 2023 028 */ 029public class WebRequest extends AbstractDigitalAction 030 implements FemaleSocketListener, VetoableChangeListener { 031 032 private static final ResourceBundle rbx = 033 ResourceBundle.getBundle("jmri.jmrit.logixng.implementation.ImplementationBundle"); 034 035 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox 036 public static final String DEFAULT_USER_AGENT = "Mozilla/5.0"; 037 038 // Note that it's valid if the url has parameters as well, like https://www.mysite.org/somepage.php?name=Jim&city=Boston 039 // The parameters are the string after the question mark. 040 private final LogixNG_SelectString _selectUrl = new LogixNG_SelectString(this); 041 042 private final LogixNG_SelectCharset _selectCharset = new LogixNG_SelectCharset(this); 043 044 private final LogixNG_SelectEnum<RequestMethodType> _selectRequestMethod = 045 new LogixNG_SelectEnum<>(this, RequestMethodType.values(), RequestMethodType.Get); 046 047 private final LogixNG_SelectString _selectUserAgent = 048 new LogixNG_SelectString(this, DEFAULT_USER_AGENT); 049 050 private final LogixNG_SelectEnum<ReplyType> _selectReplyType = 051 new LogixNG_SelectEnum<>(this, ReplyType.values(), ReplyType.String); 052 053 private final LogixNG_SelectEnum<LineEnding> _selectLineEnding = 054 new LogixNG_SelectEnum<>(this, LineEnding.values(), LineEnding.System); 055 056 private final List<Parameter> _parameters = new ArrayList<>(); 057 058 private String _socketSystemName; 059 private final FemaleDigitalActionSocket _socket; 060 private String _localVariableForResponseCode = ""; 061 private String _localVariableForReplyContent = ""; 062 private String _localVariableForCookies = ""; 063 064 private final InternalFemaleSocket _internalSocket = new InternalFemaleSocket(); 065 066 067 public WebRequest(String sys, String user) 068 throws BadUserNameException, BadSystemNameException { 069 super(sys, user); 070 _socket = InstanceManager.getDefault(DigitalActionManager.class) 071 .createFemaleSocket(this, this, Bundle.getMessage("ShowDialog_SocketExecute")); 072 } 073 074 @Override 075 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) 076 throws ParserException, JmriException { 077 DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class); 078 String sysName = systemNames.get(getSystemName()); 079 String userName = userNames.get(getSystemName()); 080 if (sysName == null) sysName = manager.getAutoSystemName(); 081 WebRequest copy = new WebRequest(sysName, userName); 082 copy.setComment(getComment()); 083 getSelectUrl().copy(copy._selectUrl); 084 getSelectCharset().copy(copy._selectCharset); 085 getSelectRequestMethod().copy(copy._selectRequestMethod); 086 getSelectUserAgent().copy(copy._selectUserAgent); 087 copy._parameters.addAll(_parameters); 088// getSelectMime().copy(copy._selectMime); 089 copy.setLocalVariableForResponseCode(_localVariableForResponseCode); 090 copy.setLocalVariableForReplyContent(_localVariableForReplyContent); 091 copy.setLocalVariableForCookies(_localVariableForCookies); 092// copy.setModal(_modal); 093// copy.setMultiLine(_multiLine); 094// copy.setFormat(_format); 095// copy.setFormatType(_formatType); 096// for (Data data : _dataList) { 097// copy.getDataList().add(new Data(data)); 098// } 099 return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames); 100 } 101 102 public LogixNG_SelectString getSelectUrl() { 103 return _selectUrl; 104 } 105 106 public LogixNG_SelectCharset getSelectCharset() { 107 return _selectCharset; 108 } 109 110 public LogixNG_SelectEnum<RequestMethodType> getSelectRequestMethod() { 111 return _selectRequestMethod; 112 } 113 114 public LogixNG_SelectString getSelectUserAgent() { 115 return _selectUserAgent; 116 } 117 118 public LogixNG_SelectEnum<ReplyType> getSelectReplyType() { 119 return _selectReplyType; 120 } 121 122 public LogixNG_SelectEnum<LineEnding> getSelectLineEnding() { 123 return _selectLineEnding; 124 } 125 126 public List<Parameter> getParameters() { 127 return _parameters; 128 } 129 130 public void setLocalVariableForResponseCode(String localVariable) { 131 _localVariableForResponseCode = localVariable; 132 } 133 134 public String getLocalVariableForResponseCode() { 135 return _localVariableForResponseCode; 136 } 137 138 public void setLocalVariableForReplyContent(String localVariable) { 139 _localVariableForReplyContent = localVariable; 140 } 141 142 public String getLocalVariableForReplyContent() { 143 return _localVariableForReplyContent; 144 } 145 146 public void setLocalVariableForCookies(String localVariable) { 147 _localVariableForCookies = localVariable; 148 } 149 150 public String getLocalVariableForCookies() { 151 return _localVariableForCookies; 152 } 153 154 /** {@inheritDoc} */ 155 @Override 156 public LogixNG_Category getCategory() { 157 return LogixNG_Category.OTHER; 158 } 159 160 /** {@inheritDoc} */ 161 @SuppressWarnings("unchecked") 162 @Override 163 public void execute() throws JmriException { 164 165 final ConditionalNG conditionalNG = getConditionalNG(); 166 final DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(conditionalNG.getSymbolTable()); 167 final boolean useThread = conditionalNG.getRunDelayed(); 168 169 String urlString = _selectUrl.evaluateValue(conditionalNG); 170 Charset charset = _selectCharset.evaluateCharset(conditionalNG); 171 String userAgent = _selectUserAgent.evaluateValue(conditionalNG); 172 RequestMethodType requestMethodType = _selectRequestMethod.evaluateEnum(conditionalNG); 173 ReplyType replyType = _selectReplyType.evaluateEnum(conditionalNG); 174 LineEnding lineEnding = _selectLineEnding.evaluateEnum(conditionalNG); 175 176 URL url; 177 StringBuilder paramString = new StringBuilder(); 178 179 try { 180 for (Parameter parameter : _parameters) { 181 182 Object v = SymbolTable.getInitialValue( 183 SymbolTable.Type.Parameter, 184 parameter._name, 185 parameter._type, 186 parameter._data, 187 newSymbolTable, 188 newSymbolTable.getSymbols()); 189 190 String value; 191 if (v != null) value = v.toString(); 192 else value = ""; 193 paramString.append(URLEncoder.encode(parameter._name, charset)); 194 paramString.append("="); 195 paramString.append(URLEncoder.encode(value, charset)); 196 paramString.append("&"); 197 } 198 199 if (paramString.length() > 0) { 200 paramString.deleteCharAt(paramString.length() - 1); 201 } 202 203 if (requestMethodType == RequestMethodType.Get) { 204 if (urlString.contains("?")) { 205 urlString += "&"; 206 } else { 207 urlString += "?"; 208 } 209 urlString += paramString.toString(); 210// System.out.format("Param string: \"%s\". URL: \"%s\"%n", paramString, urlString); 211 } 212 213 url = new URI(urlString).toURL(); 214// System.out.format("URL: %s, query: %s, userInfo: %s%n", url.toString(), url.getQuery(), url.getUserInfo()); 215// if (!urlString.contains("LogixNG_WebRequest_Test.php") && !urlString.contains("https://www.modulsyd.se/")) return; 216// if (!urlString.contains("LogixNG_WebRequest_Test.php")) return; 217// if (!urlString.contains("https://www.modulsyd.se/")) return; 218 } catch (MalformedURLException | URISyntaxException ex) { 219 throw new JmriException(ex.getMessage(), ex); 220 } 221 222 boolean useHttps = urlString.toLowerCase().startsWith("https://"); 223 224 Runnable runnable = () -> { 225// String https_url = "https://www.google.com/"; 226// String https_url = "https://jmri.bergqvist.se/LogixNG_WebRequest_Test.php"; 227 try { 228 229// long startTime = System.currentTimeMillis(); 230 231 HttpURLConnection con; 232 if (useHttps) { 233 con = (HttpsURLConnection) url.openConnection(); 234 } else { 235 con = (HttpURLConnection) url.openConnection(); 236 } 237 238 con.setRequestMethod(requestMethodType._identifier); 239 con.setRequestProperty("User-Agent", userAgent); 240 241 242// con.setRequestProperty("Cookie", "phpbb3_tm7zs_sid=5b33176e78318082f439a0a302fa4c25; expires=Fri, 29-Mar-2024 18:22:48 GMT; path=/; domain=.modulsyd.se; secure; HttpOnly"); 243// con.setRequestProperty("Cookie", "Daniel=Hej; expires=Fri, 29-Mar-2024 18:22:48 GMT; path=/; domain=.modulsyd.se; secure; HttpOnly"); 244// con.setRequestProperty("Cookie", "DanielAA=Hej; expires=Fri, 29-Mar-2024 18:22:48 GMT; path=/; domain=.modulsyd.se; secure; HttpOnly"); 245// con.setRequestProperty("Cookie", "DanielBB=Hej; expires=Fri, 29-Mar-2024 18:22:48 GMT; path=/; domain=.modulsyd.se; secure; HttpOnly"); 246// con.setRequestProperty("Cookie", "Aaa=Abb; Abb=Add; Acc=Aff"); 247 248 Map<String,String> cookiesMap = null; 249 250 if (!_localVariableForCookies.isEmpty()) { 251 StringBuilder cookies = new StringBuilder(); 252 253 Object cookiesObject = newSymbolTable.getValue(_localVariableForCookies); 254 if (cookiesObject != null) { 255 if (!(cookiesObject instanceof Map)) { 256 throw new IllegalArgumentException(String.format("The value of the local variable '%s' must be a Map", _localVariableForCookies)); 257 } 258 cookiesMap = (Map<String,String>)cookiesObject; 259// System.out.format("Set cookies to connection. Count: %d%n", ((List<Object>)cookiesObject).size()); 260 for (Map.Entry<String,String> entry : cookiesMap.entrySet()) { 261 if (cookies.length() > 0) { 262 cookies.append("; "); 263 } 264 String[] cookieParts = entry.getValue().split("; "); 265 cookies.append(cookieParts[0]); 266// System.out.format("Set cookie to connection: '%s=%s'%n", entry.getKey(), entry.getValue()); 267 } 268 if (cookies.length() > 0) { 269// System.out.format("Set cookie to connection: '%s'%n", cookies.toString()); 270 con.setRequestProperty("Cookie", cookies.toString()); 271 } 272 } 273 } 274 275 276 277 278 279 280////DANIEL con.setRequestProperty("Content-Type", "text/html"); 281// con.setRequestProperty("Content-Type", mime); 282 283// con.setRequestProperty("Content-Type", "application/json"); 284// con.setRequestProperty("Content-Type", "application/html"); 285// con.setRequestProperty("Content-Type", "text/html"); 286// con.setRequestProperty("Content-Type", "text/plain"); 287// con.setRequestProperty("Content-Type", "text/csv"); 288// con.setRequestProperty("Content-Type", "text/markdown"); 289 290 if (requestMethodType == RequestMethodType.Post) { 291 con.setRequestMethod("POST"); 292 con.setDoOutput(true); 293 try (DataOutputStream out = new DataOutputStream(con.getOutputStream())) { 294 out.writeBytes(paramString.toString()); 295 out.flush(); 296 } 297 } 298 299 300 301 302 303 304 305 //dumpl all cert info 306// print_https_cert(con); 307 308 309// System.out.println("Response Code: " + con.getResponseCode()); 310/* 311 System.out.println("Header fields:"); 312 for (var entry : con.getHeaderFields().entrySet()) { 313 for (String value : entry.getValue()) { 314 System.out.format("Header: %s, value: %s%n", entry.getKey(), value); 315 } 316 } 317*/ 318 //dump all the content 319//DANIEL print_content(con); 320 321 Object reply; 322 323 if (replyType == ReplyType.Bytes) { 324 reply = con.getInputStream().readAllBytes(); 325 } else if (replyType == ReplyType.String || replyType == ReplyType.ListOfStrings) { 326 List<String> list = new ArrayList<>(); 327 try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()))) { 328 String input; 329 while ((input = br.readLine()) != null) { 330 // System.out.println(input); 331 list.add(input); 332 } 333 // } catch (IOException e) { 334 // e.printStackTrace(); 335 } 336 337 if (replyType == ReplyType.String) { 338 reply = String.join(lineEnding.getLineEnding(), list); 339 } else { 340 reply = list; 341 } 342 } else { 343 throw new IllegalArgumentException("replyType has unknown value: " + replyType.name()); 344 } 345 346 347 348 349 if (cookiesMap == null) { 350 cookiesMap = new HashMap<>(); 351 } 352 for (var entry : con.getHeaderFields().entrySet()) { 353 if ("Set-Cookie".equals(entry.getKey())) { 354 for (String value : entry.getValue()) { 355 String[] parts = value.split("="); 356 cookiesMap.put(parts[0], value); 357 } 358 } 359 } 360 361 362// long time = System.currentTimeMillis() - startTime; 363 364// System.out.format("Total time: %d%n", time); 365 366 synchronized (WebRequest.this) { 367 _internalSocket._conditionalNG = conditionalNG; 368 _internalSocket._newSymbolTable = newSymbolTable; 369 _internalSocket._cookies = cookiesMap; 370 _internalSocket._responseCode = con.getResponseCode(); 371 _internalSocket._reply = reply; 372 373 if (useThread) { 374 conditionalNG.execute(_internalSocket); 375 } else { 376 _internalSocket.execute(); 377 } 378 } 379 380// } catch (MalformedURLException e) { 381// e.printStackTrace(); 382// } catch (IOException e) { 383// e.printStackTrace(); 384// } catch (JmriException ex) { 385 } catch (IOException | IllegalArgumentException | JmriException ex) { 386 log.error("An exception has occurred: {}", ex, ex); 387 } 388 }; 389 390 if (useThread) { 391 ThreadingUtil.newThread(runnable, "LogixNG action WebRequest").start(); 392 } else { 393 runnable.run(); 394 } 395 } 396/* 397 private void print_content(HttpURLConnection con) { 398 if (con != null) { 399 try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()))) { 400 401 System.out.println("****** Content of the URL ********"); 402 403 String input; 404 while ((input = br.readLine()) != null) { 405 System.out.println(input); 406 } 407 br.close(); 408 409 } catch (IOException e) { 410 e.printStackTrace(); 411 } 412 } 413 } 414*/ 415 416 @Override 417 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 418 switch (index) { 419 case 0: 420 return _socket; 421 422 default: 423 throw new IllegalArgumentException( 424 String.format("index has invalid value: %d", index)); 425 } 426 } 427 428 @Override 429 public int getChildCount() { 430 return 1; 431 } 432 433 @Override 434 public void connected(FemaleSocket socket) { 435 if (socket == _socket) { 436 _socketSystemName = socket.getConnectedSocket().getSystemName(); 437 } else { 438 throw new IllegalArgumentException("unkown socket"); 439 } 440 } 441 442 @Override 443 public void disconnected(FemaleSocket socket) { 444 if (socket == _socket) { 445 _socketSystemName = null; 446 } else { 447 throw new IllegalArgumentException("unkown socket"); 448 } 449 } 450 451 @Override 452 public String getShortDescription(Locale locale) { 453 return Bundle.getMessage(locale, "WebRequest_Short"); 454 } 455 456 @Override 457 public String getLongDescription(Locale locale) { 458 return Bundle.getMessage("WebRequest_Long", _selectUrl.getDescription(locale)); 459/* 460 String bundleKey; 461 switch (_formatType) { 462 case OnlyText: 463 bundleKey = "ShowDialog_Long_TextOnly"; 464 break; 465 case CommaSeparatedList: 466 bundleKey = "ShowDialog_Long_CommaSeparatedList"; 467 break; 468 case StringFormat: 469 bundleKey = "ShowDialog_Long_StringFormat"; 470 break; 471 default: 472 throw new RuntimeException("_formatType has unknown value: "+_formatType.name()); 473 } 474 return Bundle.getMessage(locale, bundleKey, _format); 475*/ 476 } 477 478 public FemaleDigitalActionSocket getSocket() { 479 return _socket; 480 } 481 482 public String getSocketSystemName() { 483 return _socketSystemName; 484 } 485 486 public void setSocketSystemName(String systemName) { 487 _socketSystemName = systemName; 488 } 489 490 /** {@inheritDoc} */ 491 @Override 492 public void setup() { 493 try { 494 if (!_socket.isConnected() 495 || !_socket.getConnectedSocket().getSystemName() 496 .equals(_socketSystemName)) { 497 498 String socketSystemName = _socketSystemName; 499 500 _socket.disconnect(); 501 502 if (socketSystemName != null) { 503 MaleSocket maleSocket = 504 InstanceManager.getDefault(DigitalActionManager.class) 505 .getBySystemName(socketSystemName); 506 if (maleSocket != null) { 507 _socket.connect(maleSocket); 508 maleSocket.setup(); 509 } else { 510 log.error("cannot load digital action {}", socketSystemName); 511 } 512 } 513 } else { 514 _socket.getConnectedSocket().setup(); 515 } 516 } catch (SocketAlreadyConnectedException ex) { 517 // This shouldn't happen and is a runtime error if it does. 518 throw new RuntimeException("socket is already connected"); 519 } 520 } 521 522 /** {@inheritDoc} */ 523 @Override 524 public void disposeMe() { 525 } 526 527 528 /** {@inheritDoc} */ 529 @Override 530 public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) { 531/* 532 log.debug("getUsageReport :: ShowDialog: bean = {}, report = {}", cdl, report); 533 for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) { 534 if (namedBeanReference._handle != null) { 535 if (bean.equals(namedBeanReference._handle.getBean())) { 536 report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription())); 537 } 538 } 539 } 540*/ 541 } 542 543 544 public enum RequestMethodType { 545 Get("WebRequest_GetPostType_Get", "GET"), // "GET" should not be i11n 546 Post("WebRequest_GetPostType_Post", "POST"); // "POST" should not be i11n 547 548 private final String _text; 549 private final String _identifier; 550 551 private RequestMethodType(String text, String identifier) { 552 this._text = Bundle.getMessage(text, identifier); 553 this._identifier = identifier; 554 } 555 556 @Override 557 public String toString() { 558 return _text; 559 } 560 561 } 562 563 564 public enum ReplyType { 565 String(Bundle.getMessage("WebRequest_ReplyType_String")), 566 ListOfStrings(Bundle.getMessage("WebRequest_ReplyType_ListOfStrings")), 567 Bytes(Bundle.getMessage("WebRequest_ReplyType_Bytes")); 568 569 private final String _text; 570 571 private ReplyType(String text) { 572 this._text = text; 573 } 574 575 @Override 576 public String toString() { 577 return _text; 578 } 579 580 } 581 582 583 public static class Parameter { 584 585 public String _name; 586 public InitialValueType _type; 587 public String _data; 588 589 public Parameter(String name, InitialValueType type, String data) { 590 this._name = name; 591 this._type = type; 592 this._data = data; 593 } 594 595 public void setName(String name) { _name = name; } 596 public String getName() { return _name; } 597 598 public void setType(InitialValueType dataType) { _type = dataType; } 599 public InitialValueType getType() { return _type; } 600 601 public void setData(String valueData) { _data = valueData; } 602 public String getData() { return _data; } 603 604 } 605 606 607 private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket { 608 609 private ConditionalNG _conditionalNG; 610 private SymbolTable _newSymbolTable; 611 private int _responseCode; 612 private Map<String,String> _cookies; 613 private Object _reply; 614 615 public InternalFemaleSocket() { 616 super(null, new FemaleSocketListener(){ 617 @Override 618 public void connected(FemaleSocket socket) { 619 // Do nothing 620 } 621 622 @Override 623 public void disconnected(FemaleSocket socket) { 624 // Do nothing 625 } 626 }, "A"); 627 } 628 629 @Override 630 public void execute() throws JmriException { 631 if (_socket != null) { 632 MaleSocket maleSocket = (MaleSocket)WebRequest.this.getParent(); 633 try { 634 SymbolTable oldSymbolTable = _conditionalNG.getSymbolTable(); 635 _conditionalNG.setSymbolTable(_newSymbolTable); 636 if (!_localVariableForResponseCode.isEmpty()) { 637 _newSymbolTable.setValue(_localVariableForResponseCode, _responseCode); 638 } 639 if (!_localVariableForReplyContent.isEmpty()) { 640 _newSymbolTable.setValue(_localVariableForReplyContent, _reply); 641 } 642 if (!_localVariableForCookies.isEmpty()) { 643// System.out.format("Set cookies:%n"); 644// for (String s : _cookies) { 645// System.out.format("Set cookies: '%s'%n", s); 646// } 647 if (!_cookies.isEmpty()) { 648 _newSymbolTable.setValue(_localVariableForCookies, _cookies); 649 } 650// } else { 651// System.out.format("Local variable for cookies is empty!!!%n"); 652 } 653 _socket.execute(); 654 _conditionalNG.setSymbolTable(oldSymbolTable); 655 } catch (JmriException e) { 656 if (e.getErrors() != null) { 657 maleSocket.handleError(WebRequest.this, rbx.getString("ExceptionExecuteMulti"), e.getErrors(), e, log); 658 } else { 659 maleSocket.handleError(WebRequest.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log); 660 } 661 } catch (RuntimeException e) { 662 maleSocket.handleError(WebRequest.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log); 663 } 664 } 665 } 666 667 } 668 669 670 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WebRequest.class); 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693/* 694 695 https://jmri.bergqvist.se/LogixNG_WebRequest_Test.php 696 697 698 Class HttpURLConnection 699 https://docs.oracle.com/javase/8/docs/api/java/net/HttpURLConnection.html 700 701 702 Class HttpsURLConnection 703 https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/HttpsURLConnection.html 704 705 706 Do a Simple HTTP Request in Java 707 https://www.baeldung.com/java-http-request 708 709 710 711 Java HttpsURLConnection example 712 https://mkyong.com/java/java-https-client-httpsurlconnection-example/ 713 714 HttpsURLConnection - Send POST request 715 https://stackoverflow.com/questions/43352000/httpsurlconnection-send-post-request 716 717 HttpsURLConnection 718 https://developer.android.com/reference/javax/net/ssl/HttpsURLConnection 719 720 721 722 723 MIME types (IANA media types) 724 https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types 725 726 Why is it `text/html` but `application/json` in media types? 727 https://stackoverflow.com/questions/51191184/why-is-it-text-html-but-application-json-in-media-types 728 729 730 How To Use Java HttpURLConnection for HTTP GET and POST Requests 731 https://www.digitalocean.com/community/tutorials/java-httpurlconnection-example-java-http-request-get-post 732 733 734 Making a JSON POST Request With HttpURLConnection 735 https://www.baeldung.com/httpurlconnection-post 736 737 https://www.jmri.org/JavaDoc/doc/jmri/server/json/JSON.html 738 739 740*/ 741 742}