001package jmri.jmrit.logixng.actions; 002 003import java.beans.*; 004import java.io.*; 005import java.net.HttpURLConnection; 006import java.net.MalformedURLException; 007import java.net.URL; 008import java.net.URLEncoder; 009import java.nio.charset.Charset; 010import java.util.*; 011 012import javax.net.ssl.HttpsURLConnection; 013 014import jmri.*; 015import jmri.jmrit.logixng.*; 016import jmri.jmrit.logixng.SymbolTable.InitialValueType; 017import jmri.jmrit.logixng.implementation.DefaultSymbolTable; 018import jmri.jmrit.logixng.util.*; 019import jmri.jmrit.logixng.util.parser.ParserException; 020import jmri.util.ThreadingUtil; 021 022/** 023 * This action sends a web request. 024 * 025 * @author Daniel Bergqvist Copyright 2023 026 */ 027public class WebRequest extends AbstractDigitalAction 028 implements FemaleSocketListener, PropertyChangeListener, VetoableChangeListener { 029 030 private static final ResourceBundle rbx = 031 ResourceBundle.getBundle("jmri.jmrit.logixng.implementation.ImplementationBundle"); 032 033 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox 034 public static final String DEFAULT_USER_AGENT = "Mozilla/5.0"; 035 036 // Note that it's valid if the url has parameters as well, like https://www.mysite.org/somepage.php?name=Jim&city=Boston 037 // The parameters are the string after the question mark. 038 private final LogixNG_SelectString _selectUrl = 039 new LogixNG_SelectString(this, this); 040 041 private final LogixNG_SelectCharset _selectCharset = 042 new LogixNG_SelectCharset(this, this); 043 044 private final LogixNG_SelectEnum<RequestMethodType> _selectRequestMethod = 045 new LogixNG_SelectEnum<>(this, RequestMethodType.values(), RequestMethodType.Get, this); 046 047 private final LogixNG_SelectString _selectUserAgent = 048 new LogixNG_SelectString(this, DEFAULT_USER_AGENT, this); 049 050 private final LogixNG_SelectEnum<ReplyType> _selectReplyType = 051 new LogixNG_SelectEnum<>(this, ReplyType.values(), ReplyType.String, this); 052 053 private final LogixNG_SelectEnum<LineEnding> _selectLineEnding = 054 new LogixNG_SelectEnum<>(this, LineEnding.values(), LineEnding.System, this); 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 Category getCategory() { 157 return 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 URL(urlString); 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 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 registerListenersForThisClass() { 525 // Do nothing 526 } 527 528 /** {@inheritDoc} */ 529 @Override 530 public void unregisterListenersForThisClass() { 531 // Do nothing 532 } 533 534 /** {@inheritDoc} */ 535 @Override 536 public void propertyChange(PropertyChangeEvent evt) { 537 getConditionalNG().execute(); 538 } 539 540 /** {@inheritDoc} */ 541 @Override 542 public void disposeMe() { 543 } 544 545 546 /** {@inheritDoc} */ 547 @Override 548 public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) { 549/* 550 log.debug("getUsageReport :: ShowDialog: bean = {}, report = {}", cdl, report); 551 for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) { 552 if (namedBeanReference._handle != null) { 553 if (bean.equals(namedBeanReference._handle.getBean())) { 554 report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription())); 555 } 556 } 557 } 558*/ 559 } 560 561 562 public enum RequestMethodType { 563 Get("WebRequest_GetPostType_Get", "GET"), // "GET" should not be i11n 564 Post("WebRequest_GetPostType_Post", "POST"); // "POST" should not be i11n 565 566 private final String _text; 567 private final String _identifier; 568 569 private RequestMethodType(String text, String identifier) { 570 this._text = Bundle.getMessage(text, identifier); 571 this._identifier = identifier; 572 } 573 574 @Override 575 public String toString() { 576 return _text; 577 } 578 579 } 580 581 582 public enum ReplyType { 583 String(Bundle.getMessage("WebRequest_ReplyType_String")), 584 ListOfStrings(Bundle.getMessage("WebRequest_ReplyType_ListOfStrings")), 585 Bytes(Bundle.getMessage("WebRequest_ReplyType_Bytes")); 586 587 private final String _text; 588 589 private ReplyType(String text) { 590 this._text = text; 591 } 592 593 @Override 594 public String toString() { 595 return _text; 596 } 597 598 } 599 600 601 public static class Parameter { 602 603 public String _name; 604 public InitialValueType _type; 605 public String _data; 606 607 public Parameter(String name, InitialValueType type, String data) { 608 this._name = name; 609 this._type = type; 610 this._data = data; 611 } 612 613 public void setName(String name) { _name = name; } 614 public String getName() { return _name; } 615 616 public void setType(InitialValueType dataType) { _type = dataType; } 617 public InitialValueType getType() { return _type; } 618 619 public void setData(String valueData) { _data = valueData; } 620 public String getData() { return _data; } 621 622 } 623 624 625 private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket { 626 627 private ConditionalNG _conditionalNG; 628 private SymbolTable _newSymbolTable; 629 private int _responseCode; 630 private Map<String,String> _cookies; 631 private Object _reply; 632 633 public InternalFemaleSocket() { 634 super(null, new FemaleSocketListener(){ 635 @Override 636 public void connected(FemaleSocket socket) { 637 // Do nothing 638 } 639 640 @Override 641 public void disconnected(FemaleSocket socket) { 642 // Do nothing 643 } 644 }, "A"); 645 } 646 647 @Override 648 public void execute() throws JmriException { 649 if (_socket != null) { 650 MaleSocket maleSocket = (MaleSocket)WebRequest.this.getParent(); 651 try { 652 SymbolTable oldSymbolTable = _conditionalNG.getSymbolTable(); 653 _conditionalNG.setSymbolTable(_newSymbolTable); 654 if (!_localVariableForResponseCode.isEmpty()) { 655 _newSymbolTable.setValue(_localVariableForResponseCode, _responseCode); 656 } 657 if (!_localVariableForReplyContent.isEmpty()) { 658 _newSymbolTable.setValue(_localVariableForReplyContent, _reply); 659 } 660 if (!_localVariableForCookies.isEmpty()) { 661// System.out.format("Set cookies:%n"); 662// for (String s : _cookies) { 663// System.out.format("Set cookies: '%s'%n", s); 664// } 665 if (!_cookies.isEmpty()) { 666 _newSymbolTable.setValue(_localVariableForCookies, _cookies); 667 } 668// } else { 669// System.out.format("Local variable for cookies is empty!!!%n"); 670 } 671 _socket.execute(); 672 _conditionalNG.setSymbolTable(oldSymbolTable); 673 } catch (JmriException e) { 674 if (e.getErrors() != null) { 675 maleSocket.handleError(WebRequest.this, rbx.getString("ExceptionExecuteMulti"), e.getErrors(), e, log); 676 } else { 677 maleSocket.handleError(WebRequest.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log); 678 } 679 } catch (RuntimeException e) { 680 maleSocket.handleError(WebRequest.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log); 681 } 682 } 683 } 684 685 } 686 687 688 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WebRequest.class); 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711/* 712 713 https://jmri.bergqvist.se/LogixNG_WebRequest_Test.php 714 715 716 Class HttpURLConnection 717 https://docs.oracle.com/javase/8/docs/api/java/net/HttpURLConnection.html 718 719 720 Class HttpsURLConnection 721 https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/HttpsURLConnection.html 722 723 724 Do a Simple HTTP Request in Java 725 https://www.baeldung.com/java-http-request 726 727 728 729 Java HttpsURLConnection example 730 https://mkyong.com/java/java-https-client-httpsurlconnection-example/ 731 732 HttpsURLConnection - Send POST request 733 https://stackoverflow.com/questions/43352000/httpsurlconnection-send-post-request 734 735 HttpsURLConnection 736 https://developer.android.com/reference/javax/net/ssl/HttpsURLConnection 737 738 739 740 741 MIME types (IANA media types) 742 https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types 743 744 Why is it `text/html` but `application/json` in media types? 745 https://stackoverflow.com/questions/51191184/why-is-it-text-html-but-application-json-in-media-types 746 747 748 How To Use Java HttpURLConnection for HTTP GET and POST Requests 749 https://www.digitalocean.com/community/tutorials/java-httpurlconnection-example-java-http-request-get-post 750 751 752 Making a JSON POST Request With HttpURLConnection 753 https://www.baeldung.com/httpurlconnection-post 754 755 https://www.jmri.org/JavaDoc/doc/jmri/server/json/JSON.html 756 757 758*/ 759 760}