// 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.gwtexpui.server.CacheHeaders; 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(req, 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'); CacheHeaders.setNotCacheable(res); 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 { 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; } }