Index: src/org/openstreetmap/josm/gui/preferences/remotecontrol/RemoteControlPreference.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/gui/preferences/remotecontrol/RemoteControlPreference.java b/src/org/openstreetmap/josm/gui/preferences/remotecontrol/RemoteControlPreference.java
--- a/src/org/openstreetmap/josm/gui/preferences/remotecontrol/RemoteControlPreference.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/gui/preferences/remotecontrol/RemoteControlPreference.java	(date 1650797512949)
@@ -27,6 +27,7 @@
 import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
 import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
+import org.openstreetmap.josm.io.remotecontrol.RemoteControlHttpServer;
 import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.GBC;
@@ -78,7 +79,7 @@
         final JLabel portLabel = new JLabel("<html>"
                 + tr("JOSM will always listen at <b>port {0}</b> (http) on localhost."
                 + "<br>This port is not configurable because it is referenced by external applications talking to JOSM.",
-                Config.getPref().get("remote.control.port", "8111")) + "</html>");
+                RemoteControlHttpServer.PORT.get()) + "</html>");
         portLabel.setFont(portLabel.getFont().deriveFont(Font.PLAIN));
         remote.add(portLabel, GBC.eol().insets(5, 5, 0, 10).fill(GBC.HORIZONTAL));
 
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/AddNodeHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/AddNodeHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/AddNodeHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/AddNodeHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/AddNodeHandler.java	(date 1650797512965)
@@ -4,7 +4,9 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.Point;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import org.openstreetmap.josm.actions.AutoScaleAction;
 import org.openstreetmap.josm.actions.AutoScaleAction.AutoScaleMode;
@@ -19,6 +21,7 @@
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -32,6 +35,9 @@
      */
     public static final String command = "add_node";
 
+    private final RequestParameter<Double> latParameter = RequestParameter.mandatory("lat", Double::parseDouble, "number");
+    private final RequestParameter<Double> lonParameter = RequestParameter.mandatory("lon", Double::parseDouble, "number");
+
     private double lat;
     private double lon;
 
@@ -41,18 +47,17 @@
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[] {"lat", "lon"};
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[] {"addtags"};
+    public List<RequestParameter<?>> getParameters() {
+        return Arrays.asList(
+            latParameter,
+            lonParameter,
+            addTagsParameter
+        );
     }
 
     @Override
     public String getUsage() {
-        return "adds a node (given by its latitude and longitude) to the current dataset";
+        return "Adds a node (given by its latitude and longitude) to the current dataset";
     }
 
     @Override
@@ -66,7 +71,7 @@
     @Override
     public String getPermissionMessage() {
         return tr("Remote Control has been asked to create a new node.") +
-                "<br>" + tr("Coordinates: ") + args.get("lat") + ", " + args.get("lon");
+                "<br>" + tr("Coordinates: ") + latParameter.read(args) + ", " + lonParameter.read(args);
     }
 
     @Override
@@ -116,11 +121,12 @@
     @Override
     protected void validateRequest() throws RequestHandlerBadRequestException {
         try {
-            lat = Double.parseDouble(args != null ? args.get("lat") : "");
-            lon = Double.parseDouble(args != null ? args.get("lon") : "");
+            lat = latParameter.read(args);
+            lon = lonParameter.read(args);
         } catch (NumberFormatException e) {
             throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+')', e);
         }
+
         if (MainApplication.getLayerManager().getEditLayer() == null) {
              throw new RequestHandlerBadRequestException(tr("There is no layer opened to add node"));
         }
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/AddWayHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/AddWayHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/AddWayHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/AddWayHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/AddWayHandler.java	(date 1650797512977)
@@ -29,6 +29,7 @@
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.spi.preferences.Config;
 
 /**
@@ -41,6 +42,8 @@
      */
     public static final String command = "add_way";
 
+    private final RequestParameter<String> wayParameter = RequestParameter.mandatory("way");
+
     private final List<LatLon> allCoordinates = new ArrayList<>();
 
     /**
@@ -49,18 +52,16 @@
     private Map<LatLon, Node> addedNodes;
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[]{"way"};
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[] {"addtags"};
+    public List<RequestParameter<?>> getParameters() {
+        return Arrays.asList(
+            wayParameter,
+            addTagsParameter
+        );
     }
 
     @Override
     public String getUsage() {
-        return "adds a way (given by a semicolon separated sequence of lat,lon pairs) to the current dataset";
+        return "Adds a way (given by a semicolon separated sequence of lat,lon pairs) to the current dataset";
     }
 
     @Override
@@ -91,7 +92,7 @@
     @Override
     protected void validateRequest() throws RequestHandlerBadRequestException {
         allCoordinates.clear();
-        for (String coordinatesString : splitArg("way", SPLITTER_SEMIC)) {
+        for (String coordinatesString : SPLITTER_SEMIC.split(wayParameter.read(args), -1)) {
             String[] coordinates = coordinatesString.split(",\\s*", 2);
             if (coordinates.length < 2) {
                 throw new RequestHandlerBadRequestException(
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/FeaturesHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/FeaturesHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/FeaturesHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/FeaturesHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/FeaturesHandler.java	(date 1650797512993)
@@ -5,7 +5,9 @@
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.json.Json;
 import javax.json.JsonArray;
@@ -15,6 +17,7 @@
 
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
 import org.openstreetmap.josm.io.remotecontrol.RequestProcessor;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 
 /**
  * Reports available commands, their parameters and examples
@@ -27,14 +30,17 @@
      */
     public static final String command = "features";
 
+    private static final RequestParameter<String> jsonpParameter = RequestParameter.optional("jsonp");
+    private static final RequestParameter<String> queryParameter = RequestParameter.optional("q");
+
     @Override
     protected void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException {
-        String q = args.get("q");
+        String q = queryParameter.read(args);
         Collection<String> handlers = q == null ? null : Arrays.asList(q.split("[,\\s]+", -1));
         content = getHandlersInfoAsJSON(handlers).toString();
         contentType = "application/json";
-        if (args.containsKey("jsonp")) {
-            content = args.get("jsonp") + " && " + args.get("jsonp") + '(' + content + ')';
+        if (jsonpParameter.isPresent(args)) {
+            content = jsonpParameter.read(args) + " && " + jsonpParameter.read(args) + '(' + content + ')';
         }
     }
 
@@ -52,14 +58,14 @@
         if (handler.getUsage() != null) {
             json.add("usage", handler.getUsage());
         }
-        json.add("parameters", toJsonArray(handler.getMandatoryParams()));
-        json.add("optional", toJsonArray(handler.getOptionalParams()));
-        json.add("examples", toJsonArray(handler.getUsageExamples(handler.getCommand())));
+        json.add("parameters", toJsonArray(handler.getParameters().stream().filter(RequestParameter::isMandatory).map(RequestParameter::getName)));
+        json.add("optional", toJsonArray(handler.getParameters().stream().filter(RequestParameter::isOptional).map(RequestParameter::getName)));
+        json.add("examples", toJsonArray(Arrays.stream(handler.getUsageExamples(handler.getCommand()))));
         return json.build();
     }
 
-    private static JsonArray toJsonArray(String[] strings) {
-        return Arrays.stream(strings)
+    private static JsonArray toJsonArray(Stream<String> strings) {
+        return strings
                 .collect(Collectors.collectingAndThen(Collectors.toList(), Json::createArrayBuilder))
                 .build();
     }
@@ -75,13 +81,11 @@
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[0];
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[]{"jsonp", "q"};
+    public List<RequestParameter<?>> getParameters() {
+        return Arrays.asList(
+            jsonpParameter,
+            queryParameter
+        );
     }
 
     @Override
@@ -91,11 +95,14 @@
 
     @Override
     public String getUsage() {
-        return "reports available commands, their parameters and examples";
+        return "Reports available commands, their parameters and examples";
     }
 
     @Override
     public String[] getUsageExamples() {
-        return new String[] {"/features", "/features?q=import,add_node"};
+        return new String[] {
+            "/features",
+            "/features?q=import,add_node"
+        };
     }
 }
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java	(date 1650797513005)
@@ -3,11 +3,14 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.openstreetmap.josm.data.StructUtils;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
@@ -18,6 +21,7 @@
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
@@ -33,6 +37,9 @@
      */
     public static final String command = "imagery";
 
+    private final RequestParameter<String> urlParameter = RequestParameter.optional("url");
+    private final RequestParameter<String> idParameters = RequestParameter.optional("id");
+
     @Override
     public String getPermissionMessage() {
         return tr("Remote Control has been asked to load an imagery layer from the following URL:")
@@ -40,19 +47,20 @@
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[0];
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        Set<String> params = new LinkedHashSet<>();
-        params.add("url");
-        params.add("id");
+    public List<RequestParameter<?>> getParameters() {
         Map<String, String> struct = StructUtils.serializeStruct(new ImageryPreferenceEntry(), ImageryPreferenceEntry.class,
-                StructUtils.SerializeOptions.INCLUDE_NULL, StructUtils.SerializeOptions.INCLUDE_DEFAULT);
-        params.addAll(struct.keySet());
-        return params.toArray(new String[0]);
+            StructUtils.SerializeOptions.INCLUDE_NULL, StructUtils.SerializeOptions.INCLUDE_DEFAULT);
+
+        List<RequestParameter<?>> imageryPreferenceParameters = struct.keySet().stream()
+            .map(RequestParameter::optional)
+            .filter(param -> !param.getName().equals("url") && !param.getName().equals("id"))
+            .collect(Collectors.toList());
+
+        List<RequestParameter<?>> parameters = new ArrayList<>();
+        parameters.add(urlParameter);
+        parameters.add(idParameters);
+        parameters.addAll(imageryPreferenceParameters);
+        return parameters;
     }
 
     @Override
@@ -61,7 +69,7 @@
     }
 
     protected ImageryInfo buildImageryInfo() {
-        String id = args.get("id");
+        String id = idParameters.read(args);
         if (id != null) {
             return ImageryLayerInfo.instance.getAllDefaultLayers().stream()
                     .filter(l -> Objects.equals(l.getId(), id))
@@ -108,7 +116,7 @@
 
     @Override
     public String getUsage() {
-        return "adds an imagery layer (e.g. WMS, TMS)";
+        return "Adds an imagery layer (e.g. WMS, TMS)";
     }
 
     @Override
@@ -119,8 +127,8 @@
             "/imagery?id=Bing",
             "/imagery?title=osm&type=tms&url=https://a.tile.openstreetmap.org/%7Bzoom%7D/%7Bx%7D/%7By%7D.png",
             "/imagery?title=landsat&type=wms&url=http://irs.gis-lab.info/?" +
-                    "layers=landsat&SRS=%7Bproj%7D&WIDTH=%7Bwidth%7D&HEIGHT=%7Bheight%7D&BBOX=%7Bbbox%7D",
-            "/imagery?title=...&type={"+types+"}&url=....[&cookies=...][&min_zoom=...][&max_zoom=...]"
-            };
+                "layers=landsat&SRS=%7Bproj%7D&WIDTH=%7Bwidth%7D&HEIGHT=%7Bheight%7D&BBOX=%7Bbbox%7D",
+            "/imagery?title=...&type={" + types + "}&url=....[&cookies=...][&min_zoom=...][&max_zoom=...]"
+        };
     }
 }
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/ImportHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/ImportHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/ImportHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/ImportHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/ImportHandler.java	(date 1650797513017)
@@ -3,16 +3,21 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.lang.reflect.Array;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
 
 import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
 import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
@@ -27,6 +32,8 @@
      */
     public static final String command = "import";
 
+    private final RequestParameter<String> urlParameter = RequestParameter.mandatory("url");
+
     private URL url;
     private Collection<DownloadTask> suitableDownloadTasks;
 
@@ -48,7 +55,7 @@
                     task.loadUrl(getDownloadParams(), url.toExternalForm(), null);
                 }
             }
-            LoadAndZoomHandler.parseChangesetTags(args);
+            LoadAndZoomHandler.parseChangesetTags(LoadAndZoomHandler.changesetTagsParameter, args);
         } catch (RuntimeException ex) { // NOPMD
             Logging.warn("RemoteControl: Error parsing import remote control request:");
             Logging.error(ex);
@@ -57,24 +64,29 @@
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[]{"url"};
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[] {"new_layer", "layer_name", "layer_locked", "download_policy", "upload_policy", "changeset_tags"};
+    public List<RequestParameter<?>> getParameters() {
+        return Arrays.asList(
+            urlParameter,
+            newLayerParameter,
+            layerNameParameter,
+            layerLockedParameter,
+            downloadPolicyParameter,
+            uploadPolicyParameter,
+            LoadAndZoomHandler.changesetTagsParameter
+        );
     }
 
     @Override
     public String getUsage() {
-        return "downloads the specified OSM file and adds it to the current data set";
+        return "Downloads the specified OSM file and adds it to the current data set";
     }
 
     @Override
     public String[] getUsageExamples() {
-        return new String[] {"/import?url=" + Utils.encodeUrl(
-                Config.getUrls().getJOSMWebsite()+"/browser/josm/trunk/nodist/data/direction-arrows.osm?format=txt")};
+        return new String[] {
+            "/import?url=" + Utils.encodeUrl(
+                Config.getUrls().getJOSMWebsite()+"/browser/josm/trunk/nodist/data/direction-arrows.osm?format=txt")
+        };
     }
 
     @Override
@@ -102,7 +114,7 @@
     @Override
     protected void validateRequest() throws RequestHandlerBadRequestException {
         validateDownloadParams();
-        String urlString = args != null ? args.get("url") : null;
+        String urlString = urlParameter.read(args);
         if (Config.getPref().getBoolean("remotecontrol.importhandler.fix_url_query", true)) {
             urlString = Utils.fixURLQuery(urlString);
         }
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java	(date 1650797513033)
@@ -9,6 +9,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -42,6 +43,7 @@
 import org.openstreetmap.josm.io.OsmTransferException;
 import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.SubclassFilteredCollection;
 import org.openstreetmap.josm.tools.Utils;
@@ -63,6 +65,18 @@
     public static final String command2 = "zoom";
     private static final String CURRENT_SELECTION = "currentselection";
 
+    private final RequestParameter<Double> bottomParameter = RequestParameter.mandatory("bottom", Double::parseDouble, "number");
+    private final RequestParameter<Double> topParameter = RequestParameter.mandatory("top", Double::parseDouble, "number");
+    private final RequestParameter<Double> leftParameter = RequestParameter.mandatory("left", Double::parseDouble, "number");
+    private final RequestParameter<Double> rightParameter = RequestParameter.mandatory("right", Double::parseDouble, "number");
+    private final RequestParameter<String> selectParameter = RequestParameter.optional("select");
+    private final RequestParameter<String> searchParameter = RequestParameter.optional("search");
+    private final RequestParameter<String> zoomModeParameter = RequestParameter.optional("zoom_mode");
+    private final RequestParameter<String> changesetCommentParameter = RequestParameter.optional("changeset_comment");
+    private final RequestParameter<String> changesetSourceParameter = RequestParameter.optional("changeset_source");
+    private final RequestParameter<String> changesetHashtagsParameter = RequestParameter.optional("changeset_hashtags");
+    public static final RequestParameter<String> changesetTagsParameter = RequestParameter.optional("changeset_tags");
+
     // Mandatory arguments
     private double minlat;
     private double maxlat;
@@ -78,27 +92,38 @@
     public String getPermissionMessage() {
         String msg = tr("Remote Control has been asked to load data from the API.") +
                 "<br>" + tr("Bounding box: ") + new BBox(minlon, minlat, maxlon, maxlat).toStringCSV(", ");
-        if (args.containsKey("select") && !toSelect.isEmpty()) {
+        if (selectParameter.isPresent(args) && !toSelect.isEmpty()) {
             msg += "<br>" + tr("Selection: {0}", toSelect.size());
         }
         return msg;
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[] {"bottom", "top", "left", "right"};
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[] {"new_layer", "layer_name", "addtags", "select", "zoom_mode",
-                "changeset_comment", "changeset_source", "changeset_hashtags", "changeset_tags",
-                "search", "layer_locked", "download_policy", "upload_policy"};
+    public List<RequestParameter<?>> getParameters() {
+        return Arrays.asList(
+            bottomParameter,
+            topParameter,
+            leftParameter,
+            rightParameter,
+            newLayerParameter,
+            layerNameParameter,
+            addTagsParameter,
+            selectParameter,
+            zoomModeParameter,
+            changesetCommentParameter,
+            changesetSourceParameter,
+            changesetHashtagsParameter,
+            changesetTagsParameter,
+            searchParameter,
+            layerLockedParameter,
+            downloadPolicyParameter,
+            uploadPolicyParameter
+        );
     }
 
     @Override
     public String getUsage() {
-        return "download a bounding box from the API, zoom to the downloaded area and optionally select one or more objects";
+        return "Download a bounding box from the API, zoom to the downloaded area and optionally select one or more objects";
     }
 
     @Override
@@ -110,14 +135,15 @@
     public String[] getUsageExamples(String cmd) {
         if (command.equals(cmd)) {
             return new String[] {
-                    "/load_and_zoom?addtags=wikipedia:de=Wei%C3%9Fe_Gasse|maxspeed=5&select=way23071688,way23076176,way23076177," +
-                            "&left=13.740&right=13.741&top=51.05&bottom=51.049",
-                    "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"};
+                "/load_and_zoom?addtags=wikipedia:de=Wei%C3%9Fe_Gasse|maxspeed=5&select=way23071688,way23076176,way23076177," +
+                    "&left=13.740&right=13.741&top=51.05&bottom=51.049",
+                "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"
+            };
         } else {
             return new String[] {
-            "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999",
-            "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=highway+OR+railway",
-            "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=" + CURRENT_SELECTION + "&addtags=foo=bar",
+                "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999",
+                "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=highway+OR+railway",
+                "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=" + CURRENT_SELECTION + "&addtags=foo=bar"
             };
         }
     }
@@ -185,7 +211,7 @@
         /**
          * deselect objects if parameter addtags given
          */
-        if (args.containsKey("addtags") && !isKeepingCurrentSelection) {
+        if (addTagsParameter.isPresent(args) && !isKeepingCurrentSelection) {
             GuiHelper.executeByMainWorkerInEDT(() -> {
                 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
                 if (ds == null) // e.g. download failed
@@ -196,7 +222,7 @@
 
         final Collection<OsmPrimitive> forTagAdd = new LinkedHashSet<>();
         final Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon);
-        if (args.containsKey("select") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
+        if (selectParameter.isPresent(args) && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
             // select objects after downloading, zoom to selection.
             GuiHelper.executeByMainWorkerInEDT(() -> {
                 Set<OsmPrimitive> newSel = new LinkedHashSet<>();
@@ -225,9 +251,9 @@
                     map.relationListDialog.selectRelations(Utils.filteredCollection(newSel, Relation.class));
                 }
             });
-        } else if (args.containsKey("search") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
+        } else if (selectParameter.isPresent(args) && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
             try {
-                final SearchCompiler.Match search = SearchCompiler.compile(args.get("search"));
+                final SearchCompiler.Match search = SearchCompiler.compile(selectParameter.read(args));
                 MainApplication.worker.submit(() -> {
                     final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
                     final Collection<OsmPrimitive> filteredPrimitives = SubclassFilteredCollection.filter(ds.allPrimitives(), search);
@@ -245,30 +271,28 @@
         }
 
         // This comes before the other changeset tags, so that they can be overridden
-        parseChangesetTags(args);
+        parseChangesetTags(changesetTagsParameter, args);
 
         // add changeset tags after download if necessary
-        if (args.containsKey("changeset_comment") || args.containsKey("changeset_source") || args.containsKey("changeset_hashtags")) {
+        if (changesetCommentParameter.isPresent(args) || changesetSourceParameter.isPresent(args) || changesetHashtagsParameter.isPresent(args)) {
             MainApplication.worker.submit(() -> {
                 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
                 if (ds != null) {
-                    for (String tag : Arrays.asList("changeset_comment", "changeset_source", "changeset_hashtags")) {
-                        if (args.containsKey(tag)) {
-                            final String tagKey = tag.substring("changeset_".length());
-                            final String value = args.get(tag);
-                            if (!Utils.isStripEmpty(value)) {
-                                ds.addChangeSetTag(tagKey, value);
-                            } else {
-                                ds.addChangeSetTag(tagKey, null);
-                            }
-                        }
+                    if (changesetCommentParameter.isPresent(args)) {
+                        ds.addChangeSetTag("comment", changesetCommentParameter.read(args));
+                    }
+                    if (changesetSourceParameter.isPresent(args)) {
+                        ds.addChangeSetTag("source", changesetSourceParameter.read(args));
+                    }
+                    if (changesetHashtagsParameter.isPresent(args)) {
+                        ds.addChangeSetTag("hashtags", changesetHashtagsParameter.read(args));
                     }
                 }
             });
         }
 
         // add tags to objects
-        if (args.containsKey("addtags")) {
+        if (addTagsParameter.isPresent(args)) {
             // needs to run in EDT since forTagAdd is updated in EDT as well
             GuiHelper.executeByMainWorkerInEDT(() -> {
                 if (!forTagAdd.isEmpty()) {
@@ -288,12 +312,12 @@
         }
     }
 
-    static void parseChangesetTags(Map<String, String> args) {
-        if (args.containsKey("changeset_tags")) {
+    static void parseChangesetTags(RequestParameter<String> changesetTagsParameter, Map<String, String> args) {
+        if (changesetTagsParameter.isPresent(args)) {
             MainApplication.worker.submit(() -> {
                 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
                 if (ds != null) {
-                    AddTagsDialog.parseUrlTagsToKeyValues(args.get("changeset_tags")).forEach(ds::addChangeSetTag);
+                    AddTagsDialog.parseUrlTagsToKeyValues(changesetTagsParameter.read(args)).forEach(ds::addChangeSetTag);
                 }
             });
         }
@@ -304,7 +328,7 @@
             return;
         }
         // zoom_mode=(download|selection), defaults to selection
-        if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) {
+        if (!"download".equals(zoomModeParameter.read(args)) && !primitives.isEmpty()) {
             AutoScaleAction.autoScale(AutoScaleMode.SELECTION);
         } else if (MainApplication.isDisplayingMapView()) {
             // make sure this isn't called unless there *is* a MapView
@@ -330,10 +354,10 @@
         minlon = 0;
         maxlon = 0;
         try {
-            minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("bottom") : ""));
-            maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("top") : ""));
-            minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("left") : ""));
-            maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("right") : ""));
+            minlat = LatLon.roundToOsmPrecision(bottomParameter.read(args));
+            maxlat = LatLon.roundToOsmPrecision(topParameter.read(args));
+            minlon = LatLon.roundToOsmPrecision(leftParameter.read(args));
+            maxlon = LatLon.roundToOsmPrecision(rightParameter.read(args));
         } catch (NumberFormatException e) {
             throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+')', e);
         }
@@ -352,9 +376,9 @@
         }
 
         // Process optional argument 'select'
-        if (args != null && args.containsKey("select")) {
+        if (args != null && selectParameter.isPresent(args)) {
             toSelect.clear();
-            for (String item : args.get("select").split(",", -1)) {
+            for (String item : selectParameter.read(args).split(",", -1)) {
                 if (!item.isEmpty()) {
                     if (CURRENT_SELECTION.equalsIgnoreCase(item)) {
                         isKeepingCurrentSelection = true;
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/LoadDataHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadDataHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadDataHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadDataHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadDataHandler.java	(date 1650797513045)
@@ -1,11 +1,6 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.io.remotecontrol.handler;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
-
 import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
 import org.openstreetmap.josm.actions.downloadtasks.DownloadParams;
 import org.openstreetmap.josm.data.osm.DataSet;
@@ -13,8 +8,17 @@
 import org.openstreetmap.josm.io.IllegalDataException;
 import org.openstreetmap.josm.io.OsmReader;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.tools.Utils;
 
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 /**
  * Handler to load data directly from the URL.
  * @since 7636
@@ -28,6 +32,9 @@
      */
     public static final String command = "load_data";
 
+    private final RequestParameter<String> dataParameter = RequestParameter.mandatory("data");
+    private final RequestParameter<String> mimeTypeParameter = RequestParameter.optional("mime_type", () -> OSM_MIME_TYPE);
+
     /**
      * Holds the data input string
      */
@@ -40,17 +47,19 @@
 
     @Override
     protected void handleRequest() throws RequestHandlerErrorException {
-        MainApplication.worker.submit(new LoadDataTask(getDownloadParams(), dataSet, args.get("layer_name")));
+        MainApplication.worker.submit(new LoadDataTask(getDownloadParams(), dataSet, layerNameParameter.read(args)));
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[]{"data"};
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[] {"new_layer", "mime_type", "layer_name", "layer_locked", "download_policy", "upload_policy"};
+    public List<RequestParameter<?>> getParameters() {
+        return Arrays.asList(
+            dataParameter,
+            mimeTypeParameter,
+            layerNameParameter,
+            layerLockedParameter,
+            downloadPolicyParameter,
+            uploadPolicyParameter
+        );
     }
 
     @Override
@@ -60,9 +69,10 @@
 
     @Override
     public String[] getUsageExamples() {
-        return new String[]{
-                "/load_data?layer_name=extra_layer&new_layer=true&data=" +
-                    Utils.encodeUrl("<osm version='0.6'><node id='-1' lat='1' lon='2' /></osm>")};
+        return new String[] {
+            "/load_data?layer_name=extra_layer&new_layer=true&data=" +
+                Utils.encodeUrl("<osm version='0.6'><node id='-1' lat='1' lon='2' /></osm>")
+        };
     }
 
     @Override
@@ -80,12 +90,12 @@
     @Override
     protected void validateRequest() throws RequestHandlerBadRequestException {
         validateDownloadParams();
-        this.data = args.get("data");
+        this.data = dataParameter.read(args);
         /**
          * Holds the mime type. Currently only OSM_MIME_TYPE is supported
          * But it could be extended to text/csv, application/gpx+xml, ... or even binary encoded data
          */
-        final String mimeType = Utils.firstNonNull(args.get("mime_type"), OSM_MIME_TYPE);
+        final String mimeType = mimeTypeParameter.read(args);
         try {
             if (OSM_MIME_TYPE.equals(mimeType)) {
                 final ByteArrayInputStream in = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/LoadObjectHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadObjectHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadObjectHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadObjectHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadObjectHandler.java	(date 1650797513057)
@@ -3,6 +3,7 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -18,6 +19,7 @@
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.tools.Logging;
 
 /**
@@ -34,25 +36,33 @@
 
     private final List<PrimitiveId> ps = new LinkedList<>();
 
+    private final RequestParameter<String> objectsParameter = RequestParameter.mandatory("objects");
+    private final RequestParameter<Boolean> relationMembersParameter = RequestParameter.optional("relation_members", Boolean::parseBoolean, () -> false, "boolean");
+    private final RequestParameter<Boolean> referrersParameter = RequestParameter.optional("referrers", Boolean::parseBoolean, () -> false, "boolean");
+
     @Override
-    public String[] getMandatoryParams() {
-        return new String[]{"objects"};
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[] {"new_layer", "layer_name", "layer_locked", "download_policy", "upload_policy",
-                "addtags", "relation_members", "referrers"};
+    public List<RequestParameter<?>> getParameters() {
+        return Arrays.asList(
+            objectsParameter,
+            layerNameParameter,
+            layerLockedParameter,
+            downloadPolicyParameter,
+            uploadPolicyParameter,
+            addTagsParameter,
+            relationMembersParameter,
+            referrersParameter
+        );
     }
 
     @Override
     public String getUsage() {
-        return "downloads the specified objects from the server";
+        return "Downloads the specified objects from the server";
     }
 
     @Override
     public String[] getUsageExamples() {
-        return new String[] {"/load_object?new_layer=true&objects=w106159509",
+        return new String[]{
+            "/load_object?new_layer=true&objects=w106159509",
             "/load_object?new_layer=true&objects=r2263653&relation_members=true",
             "/load_object?objects=n100000&referrers=false"
         };
@@ -64,11 +74,11 @@
             Logging.info("RemoteControl: download forbidden by preferences");
         }
         if (!ps.isEmpty()) {
-            final boolean newLayer = getDownloadParams().isNewLayer();
-            final boolean relationMembers = Boolean.parseBoolean(args.get("relation_members"));
-            final boolean referrers = Boolean.parseBoolean(args.get("referrers"));
+            final boolean newLayer = newLayerParameter.read(args);
+            final boolean relationMembers = relationMembersParameter.read(args);
+            final boolean referrers = referrersParameter.read(args);
             final DownloadPrimitivesWithReferrersTask task = new DownloadPrimitivesWithReferrersTask(
-                    newLayer, ps, referrers, relationMembers, args.get("layer_name"), null);
+                    newLayer, ps, referrers, relationMembers, layerNameParameter.read(args), null);
             try {
                 MainApplication.worker.submit(task).get(OSM_DOWNLOAD_TIMEOUT.get(), TimeUnit.SECONDS);
             } catch (InterruptedException | ExecutionException | TimeoutException e) {
@@ -100,7 +110,7 @@
     protected void validateRequest() throws RequestHandlerBadRequestException {
         validateDownloadParams();
         ps.clear();
-        for (String i : splitArg("objects", SPLITTER_COMMA)) {
+        for (String i : SPLITTER_COMMA.split(objectsParameter.read(args), -1)) {
             if (!i.isEmpty()) {
                 try {
                     ps.add(SimplePrimitiveId.fromString(i));
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/OpenApiHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenApiHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenApiHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenApiHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenApiHandler.java	(date 1650799626640)
@@ -5,8 +5,8 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.io.StringWriter;
-import java.util.Arrays;
-import java.util.stream.Stream;
+import java.util.Collections;
+import java.util.List;
 
 import javax.json.Json;
 import javax.json.JsonArrayBuilder;
@@ -15,7 +15,9 @@
 import org.openstreetmap.josm.data.preferences.JosmUrls;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
 import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
+import org.openstreetmap.josm.io.remotecontrol.RemoteControlHttpServer;
 import org.openstreetmap.josm.io.remotecontrol.RequestProcessor;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -28,6 +30,11 @@
      */
     public static final String command = "openapi.json";
 
+    /**
+     * OpenAPI description.
+     */
+    public static final String DESCRIPTION = "The Remote control API for JOSM.\n\nAllows using GET HTTP requests with query parameters to invoke actions in JOSM.";
+
     @Override
     protected void handleRequest() {
         JsonObjectBuilder openapi = getOpenApi();
@@ -39,16 +46,29 @@
 
     private JsonObjectBuilder getOpenApi() {
         return Json.createObjectBuilder()
-                .add("openapi", "3.0.0")
-                .add("info", Json.createObjectBuilder()
-                        .add("title", RequestProcessor.JOSM_REMOTE_CONTROL)
-                        .add("version", RemoteControl.getVersion())
-                        .add("contact", Json.createObjectBuilder()
-                                .add("name", "JOSM")
-                                .add("url", JosmUrls.getInstance().getJOSMWebsite())))
-                .add("servers", Json.createArrayBuilder()
-                        .add(Json.createObjectBuilder().add("url", "http://localhost:8111/")))
-                .add("paths", getHandlers());
+                .add("openapi", "3.0.3")
+                .add("info", getInfo())
+                .add("servers", getServers())
+                .add("paths", getHandlers());
+    }
+
+    private JsonObjectBuilder getInfo() {
+        return Json.createObjectBuilder()
+            .add("title", RequestProcessor.JOSM_REMOTE_CONTROL)
+            .add("description", DESCRIPTION)
+            .add("version", RemoteControl.getVersion())
+            .add("contact", getContact());
+    }
+
+    private JsonObjectBuilder getContact() {
+        return Json.createObjectBuilder()
+            .add("name", "JOSM")
+            .add("url", JosmUrls.getInstance().getJOSMWebsite());
+    }
+
+    private JsonArrayBuilder getServers() {
+        return Json.createArrayBuilder()
+            .add(Json.createObjectBuilder().add("url", String.format("http://localhost:%s/", RemoteControlHttpServer.PORT.get())));
     }
 
     private JsonObjectBuilder getHandlers() {
@@ -59,23 +79,46 @@
     }
 
     private JsonObjectBuilder getHandler(RequestHandler handler) {
+        return Json.createObjectBuilder()
+            .add("get", getOperation(handler));
+    }
+
+    private JsonObjectBuilder getOperation(RequestHandler handler) {
+        return Json.createObjectBuilder()
+            .add("summary", getSummary(handler))
+            .add("description", getDescription(handler))
+            .add("operationId", handler.getCommand())
+            .add("parameters", getParameters(handler))
+            .add("responses", operationResponses());
+    }
+
+    private JsonObjectBuilder operationResponses() {
+        return Json.createObjectBuilder()
+            .add("200", Json.createObjectBuilder().add("description", "Successful operation"))
+            .add("400", Json.createObjectBuilder().add("description", "Missing required parameters or bad request format"))
+            .add("403", Json.createObjectBuilder().add("description", "Action is not permitted"))
+            .add("500", Json.createObjectBuilder().add("description", "Internal server error"))
+            .add("502", Json.createObjectBuilder().add("description", "Bad gateway (upstream)"));
+    }
+
+    private JsonArrayBuilder getParameters(RequestHandler handler) {
         JsonArrayBuilder parameters = Json.createArrayBuilder();
-        Stream.concat(
-                Arrays.stream(handler.getMandatoryParams()),
-                Arrays.stream(handler.getOptionalParams())
-        ).distinct().map(param -> Json.createObjectBuilder()
-                .add("name", param)
+
+        handler.getParameters()
+            .stream()
+            .map(param -> Json.createObjectBuilder()
+                .add("name", param.getName())
                 .add("in", "query")
-                .add("required", Arrays.asList(handler.getMandatoryParams()).contains(param))
-                .add("schema", Json.createObjectBuilder().add("type", "string")) // TODO fix type
-        ).forEach(parameters::add);
-        return Json.createObjectBuilder().add("get", Json.createObjectBuilder()
-                .add("description", getDescription(handler))
-                .add("operationId", handler.getCommand())
-                .add("parameters", parameters)
-                .add("responses", Json.createObjectBuilder()
-                        .add("200", Json.createObjectBuilder().add("description", "successful operation")))
-        );
+                .add("required", param.isMandatory())
+                .add("schema", Json.createObjectBuilder().add("type", param.getOpenapiType()))
+            )
+            .forEach(parameters::add);
+
+        return parameters;
+    }
+
+    private String getSummary(RequestHandler handler) {
+        return Utils.firstNonNull(handler.getUsage(), String.format("Command %s", handler.getCommand()));
     }
 
     private String getDescription(RequestHandler handler) {
@@ -90,7 +133,10 @@
 
     @Override
     public String[] getUsageExamples() {
-        return new String[]{"https://petstore.swagger.io/?url=http://localhost:8111/openapi.json", "https://swagger.io/specification/"};
+        return new String[]{
+            "https://petstore.swagger.io/?url=http://localhost:8111/openapi.json",
+            "https://swagger.io/specification/"
+        };
     }
 
     @Override
@@ -104,8 +150,8 @@
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[0];
+    public List<RequestParameter<?>> getParameters() {
+        return Collections.emptyList();
     }
 
     @Override
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/OpenFileHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenFileHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenFileHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenFileHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/OpenFileHandler.java	(date 1650797513081)
@@ -4,13 +4,15 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.io.File;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.EnumSet;
+import java.util.List;
 
 import org.openstreetmap.josm.actions.OpenFileAction;
 import org.openstreetmap.josm.gui.io.importexport.Options;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 
 /**
  * Opens a local file
@@ -22,19 +24,23 @@
      */
     public static final String command = "open_file";
 
+    private final RequestParameter<File> filenameParameter = RequestParameter.mandatory("filename", File::new, "string");
+
     @Override
-    public String[] getMandatoryParams() {
-        return new String[]{"filename"};
+    public List<RequestParameter<?>> getParameters() {
+        return Collections.singletonList(filenameParameter);
     }
 
     @Override
     public String getUsage() {
-        return "opens a local file in JOSM";
+        return "Opens a local file in JOSM";
     }
 
     @Override
     public String[] getUsageExamples() {
-        return new String[] {"/open_file?filename=/tmp/test.osm"};
+        return new String[]{
+            "/open_file?filename=/tmp/test.osm"
+        };
     }
 
     @Override
@@ -49,7 +55,7 @@
             options.add(Options.ALLOW_WEB_RESOURCES);
         }
         GuiHelper.runInEDT(() ->
-            OpenFileAction.openFiles(Arrays.asList(new File(args.get("filename"))), options.toArray(new Options[0])));
+            OpenFileAction.openFiles(Collections.singletonList(filenameParameter.read(args)), options.toArray(new Options[0])));
     }
 
     @Override
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/RequestHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/RequestHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/RequestHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/RequestHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/RequestHandler.java	(date 1650799626656)
@@ -6,17 +6,16 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.text.MessageFormat;
-import java.util.Collections;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
-import java.util.function.Supplier;
 import java.util.regex.Pattern;
-
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 
@@ -28,6 +27,7 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.io.OsmApiException;
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Pair;
@@ -53,6 +53,13 @@
     /** past confirmations */
     protected static final PermissionCache PERMISSIONS = new PermissionCache();
 
+    protected final RequestParameter<String> layerNameParameter = RequestParameter.optional("layer_name");
+    protected final RequestParameter<Boolean> newLayerParameter = RequestParameter.optional("new_layer", Boolean::parseBoolean, LOAD_IN_NEW_LAYER::get, "boolean");
+    protected final RequestParameter<Boolean> layerLockedParameter = RequestParameter.optional("layer_locked", Boolean::parseBoolean, () -> false, "boolean");
+    protected final RequestParameter<DownloadPolicy> downloadPolicyParameter = RequestParameter.optional("download_policy", DownloadPolicy::of, () -> DownloadPolicy.NORMAL, "string");
+    protected final RequestParameter<UploadPolicy> uploadPolicyParameter = RequestParameter.optional("upload_policy", UploadPolicy::of, () -> UploadPolicy.NORMAL, "string");
+    protected final RequestParameter<String> addTagsParameter = RequestParameter.optional("addtags");
+
     /** The GET request arguments */
     protected Map<String, String> args;
 
@@ -128,16 +135,35 @@
      */
     public abstract PermissionPrefWithDefault getPermissionPref();
 
+    /**
+     * Returns the request parameters. Both used in runtime and for documentation.
+     * @since xxx
+     * @return the request parameters
+     */
+    public List<RequestParameter<?>> getParameters() {
+        // Default implementation for backwards compatibility
+        return Stream.concat(
+            Arrays.stream(getMandatoryParams()).map(RequestParameter::mandatory),
+            Arrays.stream(getOptionalParams()).map(RequestParameter::optional)
+        ).collect(Collectors.toList());
+    }
+
     /**
      * Returns the mandatory parameters. Both used to enforce their presence at runtime and for documentation.
+     * @deprecated implement `getParameters` instead.
      * @return the mandatory parameters
      */
-    public abstract String[] getMandatoryParams();
+    @Deprecated
+    public String[] getMandatoryParams() {
+        return new String[0];
+    }
 
     /**
      * Returns the optional parameters. Both used to enforce their presence at runtime and for documentation.
+     * @deprecated implement `getParameters` instead.
      * @return the optional parameters
      */
+    @Deprecated
     public String[] getOptionalParams() {
         return new String[0];
     }
@@ -247,10 +273,6 @@
         this.args = getRequestParameter(new URI(this.request));
     }
 
-    protected final String[] splitArg(String arg, Pattern splitter) {
-        return splitter.split(args != null ? args.get(arg) : "", -1);
-    }
-
     /**
      * Returns the request parameters.
      * @param uri URI as string
@@ -271,25 +293,20 @@
     }
 
     void checkMandatoryParams() throws RequestHandlerBadRequestException {
-        String[] mandatory = getMandatoryParams();
-        String[] optional = getOptionalParams();
+        List<RequestParameter<?>> mandatory = getParameters().stream().filter(RequestParameter::isMandatory).collect(Collectors.toList());
         List<String> missingKeys = new LinkedList<>();
         boolean error = false;
-        if (mandatory != null && args != null) {
-            for (String key : mandatory) {
-                String value = args.get(key);
-                if (Utils.isEmpty(value)) {
+        if (args != null) {
+            for (RequestParameter<?> parameter : mandatory) {
+                String value = args.get(parameter.getName());
+                if (Utils.isStripEmpty(value)) {
                     error = true;
-                    Logging.warn('\'' + myCommand + "' remote control request must have '" + key + "' parameter");
-                    missingKeys.add(key);
+                    Logging.warn('\'' + myCommand + "' remote control request must have '" + parameter.getName() + "' parameter");
+                    missingKeys.add(parameter.getName());
                 }
             }
         }
-        Set<String> knownParams = new HashSet<>();
-        if (mandatory != null)
-            Collections.addAll(knownParams, mandatory);
-        if (optional != null)
-            Collections.addAll(knownParams, optional);
+        Set<String> knownParams = getParameters().stream().map(RequestParameter::getName).collect(Collectors.toSet());
         if (args != null) {
             for (String par: args.keySet()) {
                 if (!knownParams.contains(par)) {
@@ -340,28 +357,15 @@
         return contentType;
     }
 
-    private <T> T get(String key, Function<String, T> parser, Supplier<T> defaultSupplier) {
-        String val = args.get(key);
-        return !Utils.isEmpty(val) ? parser.apply(val) : defaultSupplier.get();
-    }
-
-    private boolean get(String key) {
-        return get(key, Boolean::parseBoolean, () -> Boolean.FALSE);
-    }
-
-    private boolean isLoadInNewLayer() {
-        return get("new_layer", Boolean::parseBoolean, LOAD_IN_NEW_LAYER::get);
-    }
-
     protected DownloadParams getDownloadParams() {
         DownloadParams result = new DownloadParams();
         if (args != null) {
             result = result
-                .withNewLayer(isLoadInNewLayer())
-                .withLayerName(args.get("layer_name"))
-                .withLocked(get("layer_locked"))
-                .withDownloadPolicy(get("download_policy", DownloadPolicy::of, () -> DownloadPolicy.NORMAL))
-                .withUploadPolicy(get("upload_policy", UploadPolicy::of, () -> UploadPolicy.NORMAL));
+                .withNewLayer(newLayerParameter.read(args))
+                .withLayerName(layerNameParameter.read(args))
+                .withLocked(layerLockedParameter.read(args))
+                .withDownloadPolicy(downloadPolicyParameter.read(args))
+                .withUploadPolicy(uploadPolicyParameter.read(args));
         }
         return result;
     }
@@ -501,7 +505,7 @@
      */
     public abstract static class RawURLParseRequestHandler extends RequestHandler {
         @Override
-        protected void parseArgs() throws URISyntaxException {
+        protected void parseArgs() {
             Map<String, String> args = new HashMap<>();
             if (request.indexOf('?') != -1) {
                 String query = request.substring(request.indexOf('?') + 1);
Index: src/org/openstreetmap/josm/io/remotecontrol/handler/VersionHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/VersionHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/VersionHandler.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/handler/VersionHandler.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/VersionHandler.java	(date 1650797513113)
@@ -5,6 +5,11 @@
 
 import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
 import org.openstreetmap.josm.io.remotecontrol.RequestProcessor;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Handler for version request.
@@ -16,13 +21,15 @@
      */
     public static final String command = "version";
 
+    private final RequestParameter<String> jsonpParameter = RequestParameter.optional("jsonp");
+
     @Override
     protected void handleRequest() throws RequestHandlerErrorException,
             RequestHandlerBadRequestException {
         content = RequestProcessor.PROTOCOLVERSION;
         contentType = "application/json";
-        if (args.containsKey("jsonp")) {
-            content = args.get("jsonp") + " && " + args.get("jsonp") + '(' + content + ')';
+        if (jsonpParameter.isPresent(args)) {
+            content = jsonpParameter.read(args) + " && " + jsonpParameter.read(args) + '(' + content + ')';
         }
     }
 
@@ -37,13 +44,10 @@
     }
 
     @Override
-    public String[] getMandatoryParams() {
-        return new String[0];
-    }
-
-    @Override
-    public String[] getOptionalParams() {
-        return new String[]{"jsonp"};
+    public List<RequestParameter<?>> getParameters() {
+        return Collections.singletonList(
+            jsonpParameter
+        );
     }
 
     @Override
@@ -53,11 +57,14 @@
 
     @Override
     public String getUsage() {
-        return "returns the current protocol version of the installed JOSM RemoteControl";
+        return "Returns the current protocol version of the installed JOSM RemoteControl";
     }
 
     @Override
     public String[] getUsageExamples() {
-        return new String[] {"/version", "/version?jsonp=test"};
+        return new String[]{
+            "/version",
+            "/version?jsonp=test"
+        };
     }
 }
Index: src/org/openstreetmap/josm/io/remotecontrol/parameter/RequestParameter.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/parameter/RequestParameter.java b/src/org/openstreetmap/josm/io/remotecontrol/parameter/RequestParameter.java
new file mode 100644
--- /dev/null	(date 1650797851323)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/parameter/RequestParameter.java	(date 1650797851323)
@@ -0,0 +1,99 @@
+package org.openstreetmap.josm.io.remotecontrol.parameter;
+
+import org.openstreetmap.josm.tools.Utils;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Logic for parsing a mandatory or optional request parameter with a name, a default value and an OpenAPI type.
+ * @since xxx
+ */
+public class RequestParameter<T> {
+
+    private final String name;
+    private final Function<String, T> parser;
+    private final boolean mandatory;
+    private final Supplier<T> defaultValue;
+    private final String openapiType;
+
+    private RequestParameter(String name, Function<String, T> parser, boolean mandatory, Supplier<T> defaultValue, String openapiType) {
+        this.name = Objects.requireNonNull(name, "name");
+        this.parser = Objects.requireNonNull(parser, "parser");
+        this.mandatory = mandatory;
+        this.defaultValue = defaultValue;
+        this.openapiType = openapiType;
+    }
+
+    public static <T> RequestParameter<T> mandatory(String name, Function<String, T> parser, String openapiType) {
+        return new RequestParameter<>(name, parser, true, () -> null, openapiType);
+    }
+
+    public static RequestParameter<String> mandatory(String name) {
+        return new RequestParameter<>(name, Function.identity(), true, () -> null, "string");
+    }
+
+    public static <T> RequestParameter<T> optional(String name, Function<String, T> parser, String openapiType) {
+        return new RequestParameter<>(name, parser, false, () -> null, openapiType);
+    }
+
+    public static <T> RequestParameter<T> optional(String name, Function<String, T> parser, Supplier<T> defaultValue, String openapiType) {
+        return new RequestParameter<>(name, parser, false, defaultValue, openapiType);
+    }
+
+    public static RequestParameter<String> optional(String name) {
+        return new RequestParameter<>(name, Function.identity(), false, () -> null, "string");
+    }
+
+    public static RequestParameter<String> optional(String name, Supplier<String> defaultValue) {
+        return new RequestParameter<>(name, Function.identity(), false, defaultValue, "string");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean isMandatory() {
+        return mandatory;
+    }
+
+    public boolean isOptional() {
+        return !mandatory;
+    }
+
+    /**
+     * Is the parameter present in the request
+     * @param arguments the request parameters
+     */
+    public boolean isPresent(Map<String, String> arguments) {
+        return arguments.containsKey(name);
+    }
+
+    /**
+     * Read the parameter from the request
+     * @param arguments the request parameters
+     * @return The parameter, or the default value if the parameter is missing.
+     * @throws IllegalArgumentException when a mandatory parameter is missing.
+     */
+    public T read(Map<String, String> arguments) {
+        String value = arguments.get(name);
+        if (Utils.isStripEmpty(value)) {
+            if (mandatory) {
+                throw new IllegalArgumentException("Empty value supplied for mandatory parameter");
+            }
+            return defaultValue.get();
+        }
+
+        return parser.apply(value);
+    }
+
+    /**
+     * For use in the OpenAPI documentation of the JOSM API.
+     * @see <a href="https://swagger.io/docs/specification/data-models/data-types/">OpenAPI data types</a>
+     */
+    public String getOpenapiType() {
+        return openapiType;
+    }
+}
Index: src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpServer.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpServer.java b/src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpServer.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpServer.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpServer.java	(date 1650799482892)
@@ -8,7 +8,7 @@
 import java.net.Socket;
 import java.net.SocketException;
 
-import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
 import org.openstreetmap.josm.tools.Logging;
 
 /**
@@ -19,6 +19,11 @@
  */
 public class RemoteControlHttpServer extends Thread {
 
+    /**
+     * preference to define remote control port
+     */
+    public static final IntegerProperty PORT = new IntegerProperty("remote.control.port", 8111);
+
     /** The server socket */
     private final ServerSocket server;
 
@@ -32,7 +37,7 @@
      */
     public static void restartRemoteControlHttpServer() {
         stopRemoteControlHttpServer();
-        int port = Config.getPref().getInt("remote.control.port", 8111);
+        int port = PORT.get();
         try {
             instance4 = new RemoteControlHttpServer(port, false);
             instance4.start();
Index: src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java b/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
--- a/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(revision 18434)
+++ b/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(date 1650797513137)
@@ -13,6 +13,7 @@
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -22,6 +23,7 @@
 import java.util.TreeMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.json.Json;
@@ -44,6 +46,7 @@
 import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException;
 import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerOsmApiException;
 import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler;
+import org.openstreetmap.josm.io.remotecontrol.parameter.RequestParameter;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -450,24 +453,24 @@
         StringBuilder usage = new StringBuilder(1024);
         for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) {
             RequestHandler sample = handler.getValue().getConstructor().newInstance();
-            String[] mandatory = sample.getMandatoryParams();
-            String[] optional = sample.getOptionalParams();
+            List<String> mandatory = sample.getParameters().stream().filter(RequestParameter::isMandatory).map(RequestParameter::getName).sorted().collect(Collectors.toList());
+            List<String> optional = sample.getParameters().stream().filter(RequestParameter::isOptional).map(RequestParameter::getName).sorted().collect(Collectors.toList());
             String[] examples = sample.getUsageExamples(handler.getKey().substring(1));
             usage.append("<li>")
                  .append(handler.getKey());
             if (!Utils.isEmpty(sample.getUsage())) {
                 usage.append(" &mdash; <i>").append(sample.getUsage()).append("</i>");
             }
-            if (mandatory != null && mandatory.length > 0) {
+            if (!mandatory.isEmpty()) {
                 usage.append("<br/>mandatory parameters: ").append(String.join(", ", mandatory));
             }
-            if (optional != null && optional.length > 0) {
+            if (!optional.isEmpty()) {
                 usage.append("<br/>optional parameters: ").append(String.join(", ", optional));
             }
             if (examples != null && examples.length > 0) {
                 usage.append("<br/>examples: ");
                 for (String ex: examples) {
-                    usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>");
+                    usage.append("<br/> <a href=\"http://localhost:" + RemoteControlHttpServer.PORT.get()).append(ex).append("\">").append(ex).append("</a>");
                 }
             }
             usage.append("</li>");
Index: test/unit/org/openstreetmap/josm/io/remotecontrol/RemoteControlTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/test/unit/org/openstreetmap/josm/io/remotecontrol/RemoteControlTest.java b/test/unit/org/openstreetmap/josm/io/remotecontrol/RemoteControlTest.java
--- a/test/unit/org/openstreetmap/josm/io/remotecontrol/RemoteControlTest.java	(revision 18434)
+++ b/test/unit/org/openstreetmap/josm/io/remotecontrol/RemoteControlTest.java	(date 1650797513149)
@@ -41,7 +41,7 @@
     @BeforeEach
     public void setUp() throws GeneralSecurityException {
         RemoteControl.start();
-        httpBase = "http://127.0.0.1:"+Config.getPref().getInt("remote.control.port", 8111);
+        httpBase = String.format("http://127.0.0.1:%s", RemoteControlHttpServer.PORT.get());
     }
 
     /**
