]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8172 add steroid support for optional parameters in Request
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 7 Nov 2016 16:03:44 +0000 (17:03 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 8 Nov 2016 17:02:54 +0000 (18:02 +0100)
also adds support for validation of parameter parameter value when it is retrieved from request
also adds support for customized extraction and validation parameter value from the request

sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java
sonar-plugin-api/src/test/java/org/sonar/api/server/ws/RequestTest.java

index c296d2c48ecc0d7caa3b7ca9ab7ec3a503a27a6b..1a1fc175f8d9b75c97f7865c3771d2f8c85c5eb1 100644 (file)
@@ -26,12 +26,17 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.SonarException;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
 import static org.sonar.api.utils.DateUtils.parseDateQuietly;
 import static org.sonar.api.utils.DateUtils.parseDateTimeQuietly;
 
@@ -277,11 +282,37 @@ public abstract class Request {
   private static long parseLong(String key, String value) {
     try {
       return Long.parseLong(value);
-    } catch (NumberFormatException expection) {
+    } catch (NumberFormatException exception) {
       throw new IllegalArgumentException(String.format("The '%s' parameter cannot be parsed as a long value: %s", key, value));
     }
   }
 
+  @Beta
+  public <T> Param<T> getParam(String key, BiFunction<Request, String, T> retrieveAndValidate) {
+    if (hasParam(key)) {
+      return GenericParam.present(retrieveAndValidate.apply(this, key));
+    }
+    return AbsentParam.absent();
+  }
+
+  @Beta
+  public StringParam getParam(String key, Consumer<String> validate) {
+    if (hasParam(key)) {
+      String value = this.param(key);
+      validate.accept(value);
+      return StringParamImpl.present(value);
+    }
+    return AbsentStringParam.absent();
+  }
+
+  @Beta
+  public StringParam getParam(String key) {
+    if (hasParam(key)) {
+      return StringParamImpl.present(this.param(key));
+    }
+    return AbsentStringParam.absent();
+  }
+
   /**
    * Allows a web service to call another web service.
    * @see LocalConnector
@@ -304,4 +335,218 @@ public abstract class Request {
 
     String getFileName();
   }
+
+  /**
+   * Represents a Request parameter, provides information whether is was specified or not (check {@link #isPresent()})
+   * and utility method to nicely handles cases where the parameter is not present.
+   */
+  @Beta
+  public interface Param<T> {
+    boolean isPresent();
+
+    /**
+     * @return the value of the parameter
+     *
+     * @throws IllegalStateException if param is not present.
+     */
+    @CheckForNull
+    T getValue();
+
+    @CheckForNull
+    T or(Supplier<T> defaultValueSupplier);
+  }
+
+  /**
+   * Implementation of {@link Param} where the param is not present.
+   */
+  private enum AbsentParam implements Param<Object> {
+    INSTANCE;
+
+    @SuppressWarnings("unchecked")
+    protected static <T> Param<T> absent() {
+      return (Param<T>) INSTANCE;
+    }
+
+    /**
+     * Always returns true.
+     */
+    @Override
+    public boolean isPresent() {
+      return false;
+    }
+
+    /**
+     * Always throws a {@link IllegalStateException}.
+     */
+    @Override
+    public Object getValue() {
+      throw createGetValueISE();
+    }
+
+    /**
+     * Always returns the value supplied by {@code defaultValueSupplier}.
+     */
+    @Override
+    @CheckForNull
+    public Object or(Supplier<Object> defaultValueSupplier) {
+      return checkDefaultValueSupplier(defaultValueSupplier).get();
+    }
+  }
+
+  /**
+   * Implementation of {@link Param} where the param is present.
+   */
+  private static final class GenericParam<T> implements Param<T> {
+    private final T value;
+
+    private GenericParam(T value) {
+      this.value = value;
+    }
+
+    static <T> Param<T> present(T value) {
+      return new GenericParam<>(value);
+    }
+
+    /**
+     * Always returns true.
+     */
+    @Override
+    public boolean isPresent() {
+      return true;
+    }
+
+    /**
+     * @return the value of the parameter
+     *
+     * @throws IllegalStateException if param is not present.
+     */
+    @Override
+    @CheckForNull
+    public T getValue() {
+      return value;
+    }
+
+    /**
+     * Always returns value of the parameter.
+     *
+     * @throws NullPointerException As per the inherited contract, {@code defaultValueSupplier} can't be null
+     */
+    @Override
+    @CheckForNull
+    public T or(Supplier<T> defaultValueSupplier) {
+      checkDefaultValueSupplier(defaultValueSupplier);
+      return value;
+    }
+  }
+
+  /**
+   * Extends {@link Param} with convenience methods specific to the type {@link String}.
+   */
+  interface StringParam extends Param<String> {
+    /**
+     * Returns a {@link StringParam} object which methods {@link #getValue()} and {@link #or(Supplier)} will
+     * return {@code null} rather than an empty String when the param is present and its value is an empty String.
+     */
+    StringParam emptyAsNull();
+  }
+
+  /**
+   * Implementation of {@link StringParam} where the param is not present.
+   */
+  private enum AbsentStringParam implements StringParam {
+    INSTANCE;
+
+    protected static StringParam absent() {
+      return INSTANCE;
+    }
+
+    /**
+     * Always returns false.
+     */
+    @Override
+    public boolean isPresent() {
+      return false;
+    }
+
+    /**
+     * Always throws a {@link IllegalStateException}.
+     */
+    @Override
+    public String getValue() {
+      throw createGetValueISE();
+    }
+
+    /**
+     * Always returns the value supplied by {@code defaultValueSupplier}.
+     */
+    @Override
+    public String or(Supplier<String> defaultValueSupplier) {
+      return checkDefaultValueSupplier(defaultValueSupplier).get();
+    }
+
+    /**
+     * Returns itself.
+     */
+    @Override
+    public StringParam emptyAsNull() {
+      return this;
+    }
+  }
+
+  /**
+   * Implementation of {@link StringParam} where the param is present.
+   */
+  private static final class StringParamImpl implements StringParam {
+    @CheckForNull
+    private final String value;
+    private final boolean emptyAsNull;
+
+    private StringParamImpl(@Nullable String value, boolean emptyAsNull) {
+      this.value = value;
+      this.emptyAsNull = emptyAsNull;
+    }
+
+    static StringParam present(String value) {
+      return new StringParamImpl(value, false);
+    }
+
+    @Override
+    public boolean isPresent() {
+      return true;
+    }
+
+    @Override
+    public String getValue() {
+      if (emptyAsNull && value != null && value.isEmpty()) {
+        return null;
+      }
+      return value;
+    }
+
+    @Override
+    @CheckForNull
+    public String or(Supplier<String> defaultValueSupplier) {
+      checkDefaultValueSupplier(defaultValueSupplier);
+      if (emptyAsNull && value != null && value.isEmpty()) {
+        return null;
+      }
+      return value;
+    }
+
+    @Override
+    public StringParam emptyAsNull() {
+      if (emptyAsNull || (value != null && !value.isEmpty())) {
+        return this;
+      }
+      return new StringParamImpl(value, true);
+    }
+  }
+
+  private static <T> Supplier<T> checkDefaultValueSupplier(Supplier<T> defaultValueSupplier) {
+    return requireNonNull(defaultValueSupplier, "default value supplier can't be null");
+  }
+
+  private static IllegalStateException createGetValueISE() {
+    return new IllegalStateException("Param has no value. Use isPresent() before calling getValue()");
+  }
 }
index 4c4fbf5f028ce5675e4fa8880d3979145292ac35..08dc23b85356c9d01b4c257c9af2a27eee248cd7 100644 (file)
@@ -29,6 +29,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 import javax.annotation.Nullable;
 import org.apache.commons.io.IOUtils;
 import org.junit.Before;
@@ -43,6 +44,7 @@ import org.sonar.api.utils.DateUtils;
 
 import static com.google.common.collect.Lists.newArrayList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
 import static org.mockito.Mockito.mock;
 import static org.sonar.api.utils.DateUtils.parseDate;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
@@ -222,6 +224,214 @@ public class RequestTest {
     assertThat(underTest.setParam("a_date", "2014-05-27").paramAsDate("a_date")).isEqualTo(DateUtils.parseDate("2014-05-27"));
   }
 
+  @Test
+  public void getParam_of_missing_string_parameter() {
+    Request.StringParam stringParam = underTest.getParam("boo");
+
+    assertThat(stringParam.isPresent()).isFalse();
+    expectSupplierCanNotBeNullNPE(() -> stringParam.or(null));
+    assertThat(stringParam.or(() -> "foo")).isEqualTo("foo");
+    expectGetValueFailureWithISE(stringParam::getValue);
+
+    Request.StringParam emptyAsNull = stringParam.emptyAsNull();
+    assertThat(emptyAsNull).isSameAs(stringParam);
+    assertThat(emptyAsNull.isPresent()).isFalse();
+    expectSupplierCanNotBeNullNPE(() -> emptyAsNull.or(null));
+    assertThat(emptyAsNull.or(() -> "bar")).isEqualTo("bar");
+    expectGetValueFailureWithISE(emptyAsNull::getValue);
+  }
+
+  @Test
+  public void getParam_of_existing_string_parameter_with_non_empty_value() {
+    underTest.setParam("a_string", "sorry");
+
+    Request.StringParam stringParam = underTest.getParam("a_string");
+
+    assertThat(stringParam.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> stringParam.or(null));
+    assertThat(stringParam.or(() -> "foo")).isEqualTo("sorry");
+    assertThat(stringParam.getValue()).isEqualTo("sorry");
+
+    Request.StringParam emptyAsNull = stringParam.emptyAsNull();
+    assertThat(emptyAsNull).isSameAs(stringParam);
+    assertThat(emptyAsNull.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> emptyAsNull.or(null));
+    assertThat(emptyAsNull.or(() -> "bar")).isEqualTo("sorry");
+    assertThat(emptyAsNull.getValue()).isEqualTo("sorry");
+  }
+
+  @Test
+  public void getParam_of_existing_string_parameter_with_empty_value() {
+    underTest.setParam("a_string", "");
+
+    Request.StringParam stringParam = underTest.getParam("a_string");
+
+    assertThat(stringParam.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> stringParam.or(null));
+    assertThat(stringParam.or(() -> "foo")).isEqualTo("");
+    assertThat(stringParam.getValue()).isEqualTo("");
+
+    Request.StringParam emptyAsNull = stringParam.emptyAsNull();
+    assertThat(emptyAsNull).isNotSameAs(stringParam);
+    assertThat(emptyAsNull.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> emptyAsNull.or(null));
+    assertThat(emptyAsNull.or(() -> "bar")).isEqualTo(null);
+    assertThat(emptyAsNull.getValue()).isEqualTo(null);
+  }
+
+  @Test
+  public void getParam_with_validation_of_missing_string_parameter() {
+    Request.StringParam stringParam = underTest.getParam("boo", (str) -> {
+      throw new IllegalStateException("validator should not be called");
+    });
+
+    assertThat(stringParam.isPresent()).isFalse();
+    expectSupplierCanNotBeNullNPE(() -> stringParam.or(null));
+    assertThat(stringParam.or(() -> "foo")).isEqualTo("foo");
+    expectGetValueFailureWithISE(stringParam::getValue);
+
+    Request.StringParam emptyAsNull = stringParam.emptyAsNull();
+    assertThat(emptyAsNull).isSameAs(stringParam);
+    assertThat(emptyAsNull.isPresent()).isFalse();
+    expectSupplierCanNotBeNullNPE(() -> emptyAsNull.or(null));
+    assertThat(emptyAsNull.or(() -> "bar")).isEqualTo("bar");
+    expectGetValueFailureWithISE(emptyAsNull::getValue);
+  }
+
+  @Test
+  public void getParam_with_validation_of_existing_string_parameter_with_non_empty_value() {
+    underTest.setParam("a_string", "sorry");
+    AtomicInteger calls = new AtomicInteger();
+
+    Request.StringParam stringParam = underTest.getParam("a_string", (str) -> calls.incrementAndGet());
+
+    assertThat(calls.get()).isEqualTo(1);
+    assertThat(stringParam.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> stringParam.or(null));
+    assertThat(stringParam.or(() -> "foo")).isEqualTo("sorry");
+    assertThat(stringParam.getValue()).isEqualTo("sorry");
+
+    Request.StringParam emptyAsNull = stringParam.emptyAsNull();
+    assertThat(emptyAsNull).isSameAs(stringParam);
+    assertThat(emptyAsNull.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> emptyAsNull.or(null));
+    assertThat(emptyAsNull.or(() -> "bar")).isEqualTo("sorry");
+    assertThat(emptyAsNull.getValue()).isEqualTo("sorry");
+  }
+
+  @Test
+  public void getParam_with_validation_of_existing_string_parameter_with_empty_value() {
+    underTest.setParam("a_string", "");
+    AtomicInteger calls = new AtomicInteger();
+
+    Request.StringParam stringParam = underTest.getParam("a_string", (str) -> calls.incrementAndGet());
+
+    assertThat(calls.get()).isEqualTo(1);
+    assertThat(stringParam.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> stringParam.or(null));
+    assertThat(stringParam.or(() -> "foo")).isEqualTo("");
+    assertThat(stringParam.getValue()).isEqualTo("");
+
+    Request.StringParam emptyAsNull = stringParam.emptyAsNull();
+    assertThat(emptyAsNull).isNotSameAs(stringParam);
+    assertThat(emptyAsNull.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> emptyAsNull.or(null));
+    assertThat(emptyAsNull.or(() -> "bar")).isEqualTo(null);
+    assertThat(emptyAsNull.getValue()).isEqualTo(null);
+  }
+
+  @Test
+  public void getParam_with_validation_of_existing_string_parameter_does_not_catch_unchecked_exception_throws_by_validator() {
+    underTest.setParam("a_string", "boo");
+    IllegalArgumentException expected = new IllegalArgumentException("Faking validation of parameter value failed");
+
+    try {
+      underTest.getParam("a_string", (str) -> {throw expected; });
+      fail("an IllegalStateException should have been raised");
+    } catch (IllegalArgumentException e) {
+      assertThat(e).isSameAs(expected);
+    }
+  }
+
+  @Test
+  public void getParam_of_missing_parameter_of_unspecified_type() {
+    Request.Param<Object> param = underTest.getParam("baa", (rqt, key) -> {
+      throw new IllegalStateException("retrieveAndValidate BiConsumer should not be called");
+    });
+
+    assertThat(param.isPresent()).isFalse();
+    expectSupplierCanNotBeNullNPE(() -> param.or(null));
+    assertThat(param.or(() -> "foo")).isEqualTo("foo");
+    expectGetValueFailureWithISE(param::getValue);
+  }
+
+  @Test
+  public void getParam_of_existing_parameter_of_unspecified_type_with_null_value() {
+    underTest.setParam("a_string", "value in fake request actually does not matter");
+
+    Request.Param<Object> param = underTest.getParam("a_string", (rqt, key) -> null);
+
+    assertThat(param.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> param.or(null));
+    assertThat(param.or(() -> "foo")).isNull();
+    assertThat(param.getValue()).isNull();
+  }
+
+  @Test
+  public void getParam_of_existing_parameter_of_unspecified_type_with_empty_string() {
+    underTest.setParam("a_string", "value in fake request actually does not matter");
+
+    Request.Param<Object> param = underTest.getParam("a_string", (rqt, key) -> "");
+
+    assertThat(param.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> param.or(null));
+    assertThat(param.or(() -> "foo")).isEqualTo("");
+    assertThat(param.getValue()).isEqualTo("");
+  }
+
+  @Test
+  public void getParam_of_existing_parameter_of_unspecified_type_with_object() {
+    underTest.setParam("a_string", "value in fake request actually does not matter");
+    Object value = new Object();
+    Request.Param<Object> param = underTest.getParam("a_string", (rqt, key) -> value);
+
+    assertThat(param.isPresent()).isTrue();
+    expectSupplierCanNotBeNullNPE(() -> param.or(null));
+    assertThat(param.or(() -> "foo")).isSameAs(value);
+    assertThat(param.getValue()).isSameAs(value);
+  }
+
+  @Test
+  public void getParam_of_existing_parameter_of_unspecified_type_does_not_catch_unchecked_exception_thrown_by_BiConsumer() {
+    underTest.setParam("a_string", "value in fake request actually does not matter");
+    RuntimeException expected = new RuntimeException("Faking BiConsumer throwing unchecked exception");
+
+    try {
+      underTest.getParam("a_string", (rqt, key) -> { throw expected; });
+      fail("an RuntimeException should have been raised");
+    } catch (RuntimeException e) {
+      assertThat(e).isSameAs(expected);
+    }
+  }
+
+  private void expectGetValueFailureWithISE(Runnable runnable) {
+    try {
+      runnable.run();
+      fail("An IllegalStateException should have been raised");
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessage("Param has no value. Use isPresent() before calling getValue()");
+    }
+  }
+
+  private void expectSupplierCanNotBeNullNPE(Runnable runnable) {
+    try {
+      runnable.run();
+      fail("A NullPointerException should have been raised");
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessage("default value supplier can't be null");
+    }
+  }
+
   @DataProvider
   public static Object[][] date_times() {
     return new Object[][] {