source: josm/trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/RequestHandler.java@ 9732

Last change on this file since 9732 was 9732, checked in by Don-vip, 8 years ago

remote control: add more unit tests, robustness

  • Property svn:eol-style set to native
File size: 12.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.remotecontrol.handler;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.net.URI;
7import java.net.URISyntaxException;
8import java.text.MessageFormat;
9import java.util.Collections;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Map;
15import java.util.Set;
16
17import javax.swing.JLabel;
18import javax.swing.JOptionPane;
19
20import org.openstreetmap.josm.Main;
21import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
22import org.openstreetmap.josm.tools.Utils;
23
24/**
25 * This is the parent of all classes that handle a specific remote control command
26 *
27 * @author Bodo Meissner
28 */
29public abstract class RequestHandler {
30
31 public static final String globalConfirmationKey = "remotecontrol.always-confirm";
32 public static final boolean globalConfirmationDefault = false;
33 public static final String loadInNewLayerKey = "remotecontrol.new-layer";
34 public static final boolean loadInNewLayerDefault = false;
35
36 /** The GET request arguments */
37 protected Map<String, String> args;
38
39 /** The request URL without "GET". */
40 protected String request;
41
42 /** default response */
43 protected String content = "OK\r\n";
44 /** default content type */
45 protected String contentType = "text/plain";
46
47 /** will be filled with the command assigned to the subclass */
48 protected String myCommand;
49
50 /**
51 * who sent the request?
52 * the host from referer header or IP of request sender
53 */
54 protected String sender;
55
56 /**
57 * Check permission and parameters and handle request.
58 *
59 * @throws RequestHandlerForbiddenException if request is forbidden by preferences
60 * @throws RequestHandlerBadRequestException if request is invalid
61 * @throws RequestHandlerErrorException if an error occurs while processing request
62 */
63 public final void handle() throws RequestHandlerForbiddenException, RequestHandlerBadRequestException, RequestHandlerErrorException {
64 checkMandatoryParams();
65 validateRequest();
66 checkPermission();
67 handleRequest();
68 }
69
70 /**
71 * Validates the request before attempting to perform it.
72 * @throws RequestHandlerBadRequestException if request is invalid
73 * @since 5678
74 */
75 protected abstract void validateRequest() throws RequestHandlerBadRequestException;
76
77 /**
78 * Handle a specific command sent as remote control.
79 *
80 * This method of the subclass will do the real work.
81 *
82 * @throws RequestHandlerErrorException if an error occurs while processing request
83 * @throws RequestHandlerBadRequestException if request is invalid
84 */
85 protected abstract void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException;
86
87 /**
88 * Get a specific message to ask the user for permission for the operation
89 * requested via remote control.
90 *
91 * This message will be displayed to the user if the preference
92 * remotecontrol.always-confirm is true.
93 *
94 * @return the message
95 */
96 public abstract String getPermissionMessage();
97
98 /**
99 * Get a PermissionPref object containing the name of a special permission
100 * preference to individually allow the requested operation and an error
101 * message to be displayed when a disabled operation is requested.
102 *
103 * Default is not to check any special preference. Override this in a
104 * subclass to define permission preference and error message.
105 *
106 * @return the preference name and error message or null
107 */
108 public abstract PermissionPrefWithDefault getPermissionPref();
109
110 public abstract String[] getMandatoryParams();
111
112 public String[] getOptionalParams() {
113 return null;
114 }
115
116 public String getUsage() {
117 return null;
118 }
119
120 public String[] getUsageExamples() {
121 return null;
122 }
123
124 /**
125 * Returns usage examples for the given command. To be overriden only my handlers that define several commands.
126 * @param cmd The command asked
127 * @return Usage examples for the given command
128 * @since 6332
129 */
130 public String[] getUsageExamples(String cmd) {
131 return getUsageExamples();
132 }
133
134 /**
135 * Check permissions in preferences and display error message or ask for permission.
136 *
137 * @throws RequestHandlerForbiddenException if request is forbidden by preferences
138 */
139 public final void checkPermission() throws RequestHandlerForbiddenException {
140 /*
141 * If the subclass defines a specific preference and if this is set
142 * to false, abort with an error message.
143 *
144 * Note: we use the deprecated class here for compatibility with
145 * older versions of WMSPlugin.
146 */
147 PermissionPrefWithDefault permissionPref = getPermissionPref();
148 if (permissionPref != null && permissionPref.pref != null) {
149 if (!Main.pref.getBoolean(permissionPref.pref, permissionPref.defaultVal)) {
150 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand);
151 Main.info(err);
152 throw new RequestHandlerForbiddenException(err);
153 }
154 }
155
156 /* Does the user want to confirm everything?
157 * If yes, display specific confirmation message.
158 */
159 if (Main.pref.getBoolean(globalConfirmationKey, globalConfirmationDefault)) {
160 // Ensure dialog box does not exceed main window size
161 Integer maxWidth = (int) Math.max(200, Main.parent.getWidth()*0.6);
162 String message = "<html><div>" + getPermissionMessage() +
163 "<br/>" + tr("Do you want to allow this?") + "</div></html>";
164 JLabel label = new JLabel(message);
165 if (label.getPreferredSize().width > maxWidth) {
166 label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">"));
167 }
168 if (JOptionPane.showConfirmDialog(Main.parent, label,
169 tr("Confirm Remote Control action"),
170 JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
171 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand);
172 throw new RequestHandlerForbiddenException(err);
173 }
174 }
175 }
176
177 /**
178 * Set request URL and parse args.
179 *
180 * @param url The request URL.
181 */
182 public void setUrl(String url) {
183 this.request = url;
184 parseArgs();
185 }
186
187 /**
188 * Parse the request parameters as key=value pairs.
189 * The result will be stored in {@code this.args}.
190 *
191 * Can be overridden by subclass.
192 */
193 protected void parseArgs() {
194 try {
195 this.args = getRequestParameter(new URI(this.request));
196 } catch (URISyntaxException ex) {
197 throw new RuntimeException(ex);
198 }
199 }
200
201 /**
202 * Returns the request parameters.
203 * @param uri URI as string
204 * @return map of request parameters
205 * @see <a href="http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding">
206 * What every web developer must know about URL encoding</a>
207 */
208 static Map<String, String> getRequestParameter(URI uri) {
209 Map<String, String> r = new HashMap<>();
210 if (uri.getRawQuery() == null) {
211 return r;
212 }
213 for (String kv : uri.getRawQuery().split("&")) {
214 final String[] kvs = Utils.decodeUrl(kv).split("=", 2);
215 r.put(kvs[0], kvs.length > 1 ? kvs[1] : null);
216 }
217 return r;
218 }
219
220 void checkMandatoryParams() throws RequestHandlerBadRequestException {
221 String[] mandatory = getMandatoryParams();
222 String[] optional = getOptionalParams();
223 List<String> missingKeys = new LinkedList<>();
224 boolean error = false;
225 if (mandatory != null && args != null) {
226 for (String key : mandatory) {
227 String value = args.get(key);
228 if (value == null || value.isEmpty()) {
229 error = true;
230 Main.warn('\'' + myCommand + "' remote control request must have '" + key + "' parameter");
231 missingKeys.add(key);
232 }
233 }
234 }
235 Set<String> knownParams = new HashSet<>();
236 if (mandatory != null)
237 Collections.addAll(knownParams, mandatory);
238 if (optional != null)
239 Collections.addAll(knownParams, optional);
240 if (args != null) {
241 for (String par: args.keySet()) {
242 if (!knownParams.contains(par)) {
243 Main.warn("Unknown remote control parameter {0}, skipping it", par);
244 }
245 }
246 }
247 if (error) {
248 throw new RequestHandlerBadRequestException(
249 "The following keys are mandatory, but have not been provided: "
250 + Utils.join(", ", missingKeys));
251 }
252 }
253
254 /**
255 * Save command associated with this handler.
256 *
257 * @param command The command.
258 */
259 public void setCommand(String command) {
260 if (command.charAt(0) == '/') {
261 command = command.substring(1);
262 }
263 myCommand = command;
264 }
265
266 public String getContent() {
267 return content;
268 }
269
270 public String getContentType() {
271 return contentType;
272 }
273
274 protected boolean isLoadInNewLayer() {
275 return args.get("new_layer") != null && !args.get("new_layer").isEmpty()
276 ? Boolean.parseBoolean(args.get("new_layer"))
277 : Main.pref.getBoolean(loadInNewLayerKey, loadInNewLayerDefault);
278 }
279
280 public void setSender(String sender) {
281 this.sender = sender;
282 }
283
284 public static class RequestHandlerException extends Exception {
285
286 /**
287 * Constructs a new {@code RequestHandlerException}.
288 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
289 */
290 public RequestHandlerException(String message) {
291 super(message);
292 }
293
294 /**
295 * Constructs a new {@code RequestHandlerException}.
296 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
297 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
298 */
299 public RequestHandlerException(String message, Throwable cause) {
300 super(message, cause);
301 }
302
303 /**
304 * Constructs a new {@code RequestHandlerException}.
305 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
306 */
307 public RequestHandlerException(Throwable cause) {
308 super(cause);
309 }
310 }
311
312 public static class RequestHandlerErrorException extends RequestHandlerException {
313 public RequestHandlerErrorException(Throwable cause) {
314 super(cause);
315 }
316 }
317
318 public static class RequestHandlerBadRequestException extends RequestHandlerException {
319
320 public RequestHandlerBadRequestException(String message) {
321 super(message);
322 }
323
324 public RequestHandlerBadRequestException(String message, Throwable cause) {
325 super(message, cause);
326 }
327 }
328
329 public static class RequestHandlerForbiddenException extends RequestHandlerException {
330 private static final long serialVersionUID = 2263904699747115423L;
331
332 public RequestHandlerForbiddenException(String message) {
333 super(message);
334 }
335 }
336
337 public abstract static class RawURLParseRequestHandler extends RequestHandler {
338 @Override
339 protected void parseArgs() {
340 Map<String, String> args = new HashMap<>();
341 if (request.indexOf('?') != -1) {
342 String query = request.substring(request.indexOf('?') + 1);
343 if (query.indexOf("url=") == 0) {
344 args.put("url", Utils.decodeUrl(query.substring(4)));
345 } else {
346 int urlIdx = query.indexOf("&url=");
347 if (urlIdx != -1) {
348 args.put("url", Utils.decodeUrl(query.substring(urlIdx + 5)));
349 query = query.substring(0, urlIdx);
350 } else if (query.indexOf('#') != -1) {
351 query = query.substring(0, query.indexOf('#'));
352 }
353 String[] params = query.split("&", -1);
354 for (String param : params) {
355 int eq = param.indexOf('=');
356 if (eq != -1) {
357 args.put(param.substring(0, eq), Utils.decodeUrl(param.substring(eq + 1)));
358 }
359 }
360 }
361 }
362 this.args = args;
363 }
364 }
365}
Note: See TracBrowser for help on using the repository browser.