All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.httpd.restapi.ParameterParser Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// 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.CorsResponder.ALLOWED_CORS_METHODS;
import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_AUTHORIZATION;
import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_CONTENT_TYPE;
import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_METHOD;
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.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.common.Nullable;
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.server.DynamicOptions;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gerrit.util.http.CacheHeaders;
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 com.google.inject.Singleton;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.kohsuke.args4j.CmdLineException;

public class ParameterParser {
  public static final String TRACE_PARAMETER = "trace";
  public static final String EXPERIMENT_PARAMETER = "experiment";

  private static final ImmutableSet RESERVED_KEYS =
      ImmutableSet.of(
          "pp",
          "prettyPrint",
          "strict",
          "callback",
          "alt",
          "fields",
          TRACE_PARAMETER,
          EXPERIMENT_PARAMETER);

  @AutoValue
  public abstract static class QueryParams {
    static final String I = QueryParams.class.getName();

    static QueryParams create(
        @Nullable String accessToken,
        @Nullable String xdMethod,
        @Nullable String xdContentType,
        ImmutableListMultimap config,
        ImmutableListMultimap params) {
      return new AutoValue_ParameterParser_QueryParams(
          accessToken, xdMethod, xdContentType, config, params);
    }

    @Nullable
    public abstract String accessToken();

    @Nullable
    abstract String xdMethod();

    @Nullable
    abstract String xdContentType();

    abstract ImmutableListMultimap config();

    abstract ImmutableListMultimap params();

    boolean hasXdOverride() {
      return xdMethod() != null || xdContentType() != null;
    }
  }

  public static QueryParams getQueryParams(HttpServletRequest req) throws BadRequestException {
    QueryParams qp = (QueryParams) req.getAttribute(QueryParams.I);
    if (qp != null) {
      return qp;
    }

    String accessToken = null;
    String xdMethod = null;
    String xdContentType = null;
    ListMultimap config = MultimapBuilder.hashKeys(4).arrayListValues().build();
    ListMultimap params = MultimapBuilder.hashKeys().arrayListValues().build();

    String queryString = req.getQueryString();
    if (!Strings.isNullOrEmpty(queryString)) {
      for (String kvPair : Splitter.on('&').split(queryString)) {
        Iterator i = Splitter.on('=').limit(2).split(kvPair).iterator();
        String key = Url.decode(i.next());
        String val = i.hasNext() ? Url.decode(i.next()) : "";

        if (XD_AUTHORIZATION.equals(key)) {
          if (accessToken != null) {
            throw new BadRequestException("duplicate " + XD_AUTHORIZATION);
          }
          accessToken = val;
        } else if (XD_METHOD.equals(key)) {
          if (xdMethod != null) {
            throw new BadRequestException("duplicate " + XD_METHOD);
          } else if (!ALLOWED_CORS_METHODS.contains(val)) {
            throw new BadRequestException("invalid " + XD_METHOD);
          }
          xdMethod = val;
        } else if (XD_CONTENT_TYPE.equals(key)) {
          if (xdContentType != null) {
            throw new BadRequestException("duplicate " + XD_CONTENT_TYPE);
          }
          xdContentType = val;
        } else if (RESERVED_KEYS.contains(key)) {
          config.put(key, val);
        } else {
          params.put(key, val);
        }
      }
    }

    qp =
        QueryParams.create(
            accessToken,
            xdMethod,
            xdContentType,
            ImmutableListMultimap.copyOf(config),
            ImmutableListMultimap.copyOf(params));
    req.setAttribute(QueryParams.I, qp);
    return qp;
  }

  private final CmdLineParser.Factory parserFactory;

  @Inject
  ParameterParser(CmdLineParser.Factory pf) {
    this.parserFactory = pf;
  }

  /**
   * Parses query parameters ({@code in}) into annotated option fields of {@code param}.
   *
   * @return true if parsing was successful. Requesting help is considered failure and returns
   *     false.
   */
   boolean parse(
      T param,
      DynamicOptions pluginOptions,
      ListMultimap in,
      HttpServletRequest req,
      HttpServletResponse res)
      throws IOException {
    if (param.getClass().getAnnotation(Singleton.class) != null) {
      // Command-line parsing mutates the object, so we can't have options on @Singleton.
      return true;
    }
    CmdLineParser clp = parserFactory.create(param);
    pluginOptions.setBean(param);
    pluginOptions.startLifecycleListeners();
    pluginOptions.parseDynamicBeans(clp);
    pluginOptions.setDynamicBeans();
    pluginOptions.onBeanParseStart();
    try {
      clp.parseOptionMap(in);
    } catch (CmdLineException | NumberFormatException e) {
      if (!clp.wasHelpRequestedByOption()) {
        replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
        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;
    }
    pluginOptions.onBeanParseEnd();

    return true;
  }

  private static Set query(HttpServletRequest req) {
    Set params = new HashSet<>();
    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.
   *
   * 

Given an input such as: * *

   * message=Does+not+compile.&labels.Verified=-1
   * 
* * which is easily created using the curl command line tool: * *
   * curl --data 'message=Does not compile.' --data labels.Verified=-1
   * 
* * converts to a JSON object structure that is normally expected: * *
   * {
   *   "message": "Does not compile.",
   *   "labels": {
   *     "Verified": "-1"
   *   }
   * }
   * 
* * 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. * *

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 map = req.getParameterMap(); return formToJson(map, query(req)); } @VisibleForTesting static JsonObject formToJson(Map map, Set query) throws BadRequestException { JsonObject inputObject = new JsonObject(); for (Map.Entry 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy