blob: dabffc0f63bd5bbaaa263f1b9115d5ed3dc04792 [file] [log] [blame]
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd.restapi;
import static com.google.gerrit.httpd.restapi.RestApiServlet.replyBinaryResult;
import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.inject.Inject;
import org.kohsuke.args4j.CmdLineException;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
class ParameterParser {
private static final ImmutableSet<String> RESERVED_KEYS = ImmutableSet.of(
"pp", "prettyPrint", "strict", "callback", "alt", "fields");
private final CmdLineParser.Factory parserFactory;
@Inject
ParameterParser(CmdLineParser.Factory pf) {
this.parserFactory = pf;
}
<T> boolean parse(T param,
Multimap<String, String> in,
HttpServletRequest req,
HttpServletResponse res)
throws IOException {
CmdLineParser clp = parserFactory.create(param);
try {
clp.parseOptionMap(in);
} catch (CmdLineException e) {
if (!clp.wasHelpRequestedByOption()) {
replyError(res, SC_BAD_REQUEST, e.getMessage());
return false;
}
}
if (clp.wasHelpRequestedByOption()) {
StringWriter msg = new StringWriter();
clp.printQueryStringUsage(req.getRequestURI(), msg);
msg.write('\n');
msg.write('\n');
clp.printUsage(msg, null);
msg.write('\n');
replyBinaryResult(req, res,
BinaryResult.create(msg.toString()).setContentType("text/plain"));
return false;
}
return true;
}
static void splitQueryString(String queryString,
Multimap<String, String> config,
Multimap<String, String> params) {
if (!Strings.isNullOrEmpty(queryString)) {
for (String kvPair : Splitter.on('&').split(queryString)) {
Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator();
String key = Url.decode(i.next());
String val = i.hasNext() ? Url.decode(i.next()) : "";
if (RESERVED_KEYS.contains(key)) {
config.put(key, val);
} else {
params.put(key, val);
}
}
}
}
private static Set<String> query(HttpServletRequest req) {
Set<String> params = Sets.newHashSet();
if (!Strings.isNullOrEmpty(req.getQueryString())) {
for (String kvPair : Splitter.on('&').split(req.getQueryString())) {
params.add(Iterables.getFirst(
Splitter.on('=').limit(2).split(kvPair),
null));
}
}
return params;
}
/**
* Convert a standard URL encoded form input into a parsed JSON tree.
* <p>
* Given an input such as:
*
* <pre>
* message=Does+not+compile.&labels.Verified=-1
* </pre>
*
* which is easily created using the curl command line tool:
*
* <pre>
* curl --data 'message=Does not compile.' --data labels.Verified=-1
* </pre>
*
* converts to a JSON object structure that is normally expected:
*
* <pre>
* {
* "message": "Does not compile.",
* "labels": {
* "Verified": "-1"
* }
* }
* </pre>
*
* This input can then be further processed into the Java input type expected
* by a view using Gson. Here we rely on Gson to perform implicit conversion
* of a string {@code "-1"} to a number type when the Java input type expects
* a number.
* <p>
* Conversion assumes any field name that does not contain {@code "."} will be
* a property of the top level input object. Any field with a dot will use the
* first segment as the top level property name naming an object, and the rest
* of the field name as a property in the nested object.
*
* @param req request to parse form input from and create JSON tree.
* @return the converted JSON object tree.
* @throws BadRequestException the request cannot be cast, as there are
* conflicting definitions for a nested object.
*/
static JsonObject formToJson(HttpServletRequest req)
throws BadRequestException {
@SuppressWarnings("unchecked")
Map<String, String[]> map = req.getParameterMap();
return formToJson(map, query(req));
}
@VisibleForTesting
static JsonObject formToJson(Map<String, String[]> map, Set<String> query)
throws BadRequestException {
JsonObject inputObject = new JsonObject();
for (Map.Entry<String, String[]> ent : map.entrySet()) {
String key = ent.getKey();
String[] values = ent.getValue();
if (query.contains(key) || values.length == 0) {
// Disallow processing query parameters as input body fields.
// Implementations of views should avoid duplicate naming.
continue;
}
JsonObject obj = inputObject;
int dot = key.indexOf('.');
if (0 <= dot) {
String property = key.substring(0, dot);
JsonElement e = inputObject.get(property);
if (e == null) {
obj = new JsonObject();
inputObject.add(property, obj);
} else if (e.isJsonObject()) {
obj = e.getAsJsonObject();
} else {
throw new BadRequestException(String.format(
"key %s conflicts with %s",
key, property));
}
key = key.substring(dot + 1);
}
if (obj.get(key) != null) {
// This error should never happen. If all form values are handled
// together in a single pass properties are set only once. Setting
// again indicates something has gone very wrong.
throw new BadRequestException("invalid form input, use JSON instead");
} else if (values.length == 1) {
obj.addProperty(key, values[0]);
} else {
JsonArray list = new JsonArray();
for (String v : values) {
list.add(new JsonPrimitive(v));
}
obj.add(key, list);
}
}
return inputObject;
}
}