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