From 44a744de9e9d9d4bd108cfcd20ec77407e1b17d9 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Tue, 9 Feb 2010 12:38:44 -0800 Subject: [PATCH] Issue 140: initial validator support from Alex --- .../org/jclouds/predicates/Validator.java | 37 ++++ .../validators/AllLowerCaseValidator.java | 40 ++++ .../org/jclouds/rest/InputParamValidator.java | 152 +++++++++++++++ .../rest/annotations/ParamValidators.java | 42 +++++ .../internal/RestAnnotationProcessor.java | 6 + .../jclouds/rest/InputParamValidatorTest.java | 177 ++++++++++++++++++ 6 files changed, 454 insertions(+) create mode 100644 core/src/main/java/org/jclouds/predicates/Validator.java create mode 100644 core/src/main/java/org/jclouds/predicates/validators/AllLowerCaseValidator.java create mode 100644 core/src/main/java/org/jclouds/rest/InputParamValidator.java create mode 100644 core/src/main/java/org/jclouds/rest/annotations/ParamValidators.java create mode 100644 core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java diff --git a/core/src/main/java/org/jclouds/predicates/Validator.java b/core/src/main/java/org/jclouds/predicates/Validator.java new file mode 100644 index 0000000000..b1e2af8b6b --- /dev/null +++ b/core/src/main/java/org/jclouds/predicates/Validator.java @@ -0,0 +1,37 @@ +package org.jclouds.predicates; + +import com.google.common.base.Predicate; + +import javax.annotation.Nullable; + +/** + * Abstract class that creates a bridge between {@link com.google.common.base.Predicate} + * and {@link org.jclouds.rest.annotations.ParamValidators}s. + * + * @param Type of object to be validated. For generic + * validation (where object's class is determined in {@link #validate(Object)}, + * use {@link Object}. + * + * @see com.google.common.base.Predicate + * + * @author Oleksiy Yarmula + */ +public abstract class Validator implements Predicate { + + @Override + public boolean apply(@Nullable T t) { + try { + validate(t); + return true; // by contract + } catch(IllegalArgumentException iae) { + return false; // by contract + } + } + + /** + * Validates the parameter + * @param t parameter to be validated + * @throws IllegalArgumentException if validation failed + */ + public abstract void validate(@Nullable T t) throws IllegalArgumentException; +} diff --git a/core/src/main/java/org/jclouds/predicates/validators/AllLowerCaseValidator.java b/core/src/main/java/org/jclouds/predicates/validators/AllLowerCaseValidator.java new file mode 100644 index 0000000000..cd72048071 --- /dev/null +++ b/core/src/main/java/org/jclouds/predicates/validators/AllLowerCaseValidator.java @@ -0,0 +1,40 @@ +/** + * + * Copyright (C) 2009 Cloud Conscious, LLC. + * + * ==================================================================== + * 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 org.jclouds.predicates.validators; + +import org.jclouds.predicates.Validator; + +import javax.annotation.Nullable; + +/** + * Validates that the string paremeter doesn't have any uppercase letters. + * + * @see org.jclouds.rest.InputParamValidator + * @see org.jclouds.predicates.Validator + */ +public class AllLowerCaseValidator extends Validator { + + public void validate(@Nullable String s) { + if (!(s == null || s.toLowerCase().equals(s))) { + throw new IllegalArgumentException(String.format( + "Object '%s' doesn't match the lower case", s)); + } + } + +} diff --git a/core/src/main/java/org/jclouds/rest/InputParamValidator.java b/core/src/main/java/org/jclouds/rest/InputParamValidator.java new file mode 100644 index 0000000000..cbd7bdde91 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/InputParamValidator.java @@ -0,0 +1,152 @@ +/** + * + * Copyright (C) 2009 Cloud Conscious, LLC. + * + * ==================================================================== + * 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 org.jclouds.rest; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import javax.inject.Inject; + +import org.jclouds.predicates.Validator; +import org.jclouds.rest.annotations.ParamValidators; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.inject.Injector; + +/** + * Validates method parameters. + * + * Checks the {@link ParamValidators} annotation for validators. There can be method-level + * validators that apply to all parameters, and parameter-level validators. When validation on at + * least one parameter doesn't pass, throws {@link IllegalStateException}. + * + * @author Oleksiy Yarmula + */ +public class InputParamValidator { + + private final Injector injector; + + @Inject + public InputParamValidator(Injector injector) { + this.injector = injector; + } + + public InputParamValidator() { + injector = null; + } + + /** + * Validates that method parameters are correct, according to {@link ParamValidators}. + * + * @param method + * method with optionally set {@link ParamValidators} + * @param args + * method arguments with optionally set {@link ParamValidators} + * @see ParamValidators + * @see Validator + * + * @throws IllegalStateException + * if validation failed + */ + public void validateMethodParametersOrThrow(Method method, Object... args) { + + if (!passesMethodValidation(method, args) + || !passesParameterValidation(method.getParameterAnnotations(), args)) { + + String argsString = Iterables.toString(Arrays.asList(args)); + throw new IllegalArgumentException(String.format( + "Validation on '%s#%s' didn't pass for arguments: " + "%s", method + .getDeclaringClass().getName(), method.getName(), argsString)); + } + } + + /** + * Returns true if all the method parameters passed all of the method-level validators. + * + * @param method + * method with optionally set {@link ParamValidators}. This can not be null. + * @param args + * method's parameters + * @return true if all the method's parameters pass all method-level validators + */ + private boolean passesMethodValidation(Method method, Object... args) { + ParamValidators paramValidatorsAnnotation = checkNotNull(method).getAnnotation( + ParamValidators.class); + if (paramValidatorsAnnotation == null) + return true; // by contract + + List> methodValidators = getValidatorsFromAnnotation(paramValidatorsAnnotation); + + return runPredicatesAgainstArgs(methodValidators, args); + } + + /** + * Returns true if all the method parameters passed all of their corresponding validators. + * + * @param annotations + * annotations for method's arguments + * @param args + * arguments that correspond to the array of annotations + * @return true if all the method parameters passed all of their corresponding validators. + */ + private boolean passesParameterValidation(Annotation[][] annotations, Object... args) { + boolean allPreducatesTrue = true; + for (int currentParameterIndex = 0; currentParameterIndex < args.length; currentParameterIndex++) { + ParamValidators annotation = findParamValidatorsAnnotationOrReturnNull(annotations[currentParameterIndex]); + if (annotation == null) + continue; + List> parameterValidators = getValidatorsFromAnnotation(annotation); + allPreducatesTrue &= runPredicatesAgainstArgs(parameterValidators, + args[currentParameterIndex]); + } + return allPreducatesTrue; + } + + private List> getValidatorsFromAnnotation(ParamValidators paramValidatorsAnnotation) { + List> validators = Lists.newArrayList(); + for (Class> validator : paramValidatorsAnnotation.value()) { + validators.add(checkNotNull(injector.getInstance(validator))); + } + return validators; + } + + @SuppressWarnings("unchecked") + private boolean runPredicatesAgainstArgs(List> predicates, Object... args) { + boolean allPredicatesTrue = true; + for (Validator validator : predicates) { + allPredicatesTrue &= Iterables.all(Arrays.asList(args), validator); + } + return allPredicatesTrue; + } + + private ParamValidators findParamValidatorsAnnotationOrReturnNull( + Annotation[] parameterAnnotations) { + for (Annotation annotation : parameterAnnotations) { + if (annotation instanceof ParamValidators) + return (ParamValidators) annotation; + } + return null; + } + +} diff --git a/core/src/main/java/org/jclouds/rest/annotations/ParamValidators.java b/core/src/main/java/org/jclouds/rest/annotations/ParamValidators.java new file mode 100644 index 0000000000..2b2a9a660e --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/annotations/ParamValidators.java @@ -0,0 +1,42 @@ +/** + * + * Copyright (C) 2009 Cloud Conscious, LLC. + * + * ==================================================================== + * 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 org.jclouds.rest.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.jclouds.predicates.Validator; + +/** + * Marks the validation for method/parameter value(s). + * + * @see org.jclouds.rest.internal.RestAnnotationProcessor + * @see com.google.common.base.Predicate + * + * @author Oleksiy Yarmula + */ +@Target( { METHOD, PARAMETER }) +@Retention(RUNTIME) +public @interface ParamValidators { + Class>[] value(); +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java index 188c9014aa..ac423026a5 100755 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -72,6 +72,7 @@ import org.jclouds.http.options.HttpRequestOptions; import org.jclouds.logging.Logger; import org.jclouds.rest.Binder; import org.jclouds.rest.InvocationContext; +import org.jclouds.rest.InputParamValidator; import org.jclouds.rest.annotations.BinderParam; import org.jclouds.rest.annotations.Endpoint; import org.jclouds.rest.annotations.EndpointParam; @@ -193,6 +194,9 @@ public class RestAnnotationProcessor { private char[] skips; + @Inject + private InputParamValidator inputParamValidator; + @VisibleForTesting public Function createResponseParser(Method method, GeneratedHttpRequest request) { @@ -312,6 +316,8 @@ public class RestAnnotationProcessor { final Injector injector; public GeneratedHttpRequest createRequest(Method method, Object... args) { + inputParamValidator.validateMethodParametersOrThrow(method, args); + URI endpoint = getEndpointFor(method, args); String httpMethod = getHttpMethodOrConstantOrThrowException(method); diff --git a/core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java b/core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java new file mode 100644 index 0000000000..929e3a3a7c --- /dev/null +++ b/core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java @@ -0,0 +1,177 @@ +/** + * + * Copyright (C) 2009 Cloud Conscious, LLC. + * + * ==================================================================== + * 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 org.jclouds.rest; + +import static com.google.common.util.concurrent.Executors.sameThreadExecutor; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.POST; +import javax.ws.rs.PathParam; + +import org.jclouds.PropertiesBuilder; +import org.jclouds.concurrent.Timeout; +import org.jclouds.concurrent.config.ExecutorServiceModule; +import org.jclouds.http.config.JavaUrlHttpCommandExecutorServiceModule; +import org.jclouds.predicates.validators.AllLowerCaseValidator; +import org.jclouds.rest.annotations.Endpoint; +import org.jclouds.rest.annotations.ParamValidators; +import org.jclouds.rest.annotations.SkipEncoding; +import org.jclouds.rest.config.RestModule; +import org.jclouds.rest.internal.RestAnnotationProcessor; +import org.jclouds.rest.internal.RestAnnotationProcessorTest; +import org.jclouds.util.Jsr330; +import org.testng.TestException; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.util.Types; + +@Test(groups = "unit", testName = "rest.InputParamValidator") +public class InputParamValidatorTest { + + @Timeout(duration = 1000, timeUnit = TimeUnit.SECONDS) + @SkipEncoding('/') + @Endpoint(RestAnnotationProcessorTest.Localhost.class) + class InputParamValidatorForm { + @POST + @ParamValidators( { AllLowerCaseValidator.class }) + public void allParamsValidated(@PathParam("param1") String param1, + @PathParam("param2") String param2) { + } + + @POST + public void oneParamValidated(@PathParam("param1") String param1, + @ParamValidators( { AllLowerCaseValidator.class }) @PathParam("param2") String param2) { + } + } + + /** + * Tests {@link AllLowerCaseValidator} against lowercase and uppercase inputs, both on method + * level and parameter level. + * + * @throws Exception + * if methods aren't found + */ + @Test + public void testInputParamsValidation() throws Exception { + Method allParamsValidatedMethod = InputParamValidatorForm.class.getMethod( + "allParamsValidated", String.class, String.class); + Method oneParamValidatedMethod = InputParamValidatorForm.class.getMethod("oneParamValidated", + String.class, String.class); + RestAnnotationProcessor restAnnotationProcessor = factory(InputParamValidatorForm.class); + restAnnotationProcessor.createRequest(allParamsValidatedMethod, "blah", "blah"); + restAnnotationProcessor.createRequest(oneParamValidatedMethod, "blah", "blah"); + + try { + restAnnotationProcessor.createRequest(allParamsValidatedMethod, "BLAH", "blah"); + throw new TestException( + "AllLowerCaseValidator shouldn't have passed 'BLAH' as a parameter because it's uppercase."); + } catch (IllegalArgumentException e) { + // supposed to happen - continue + } + + restAnnotationProcessor.createRequest(oneParamValidatedMethod, "BLAH", "blah"); + + try { + restAnnotationProcessor.createRequest(oneParamValidatedMethod, "blah", "BLAH"); + throw new TestException( + "AllLowerCaseValidator shouldn't have passed 'BLAH' as the second parameter because it's uppercase."); + } catch (IllegalArgumentException e) { + // supposed to happen - continue + } + } + + @Test + public void testNullParametersForAllLowerCaseValidator() { + new AllLowerCaseValidator().validate(null); + } + + /** + * Tries to use Validator on Integer parameter. Expected result: ClassCastException + * + * @throws Exception + * if method isn't found + */ + @Test + public void testWrongPredicateTypeLiteral() throws Exception { + @Timeout(duration = 1000, timeUnit = TimeUnit.SECONDS) + @SkipEncoding('/') + @Endpoint(RestAnnotationProcessorTest.Localhost.class) + class WrongValidator { + @SuppressWarnings("unused") + @POST + @ParamValidators( { AllLowerCaseValidator.class }) + public void method(@PathParam("param1") Integer param1) { + } + } + WrongValidator validator = new WrongValidator(); + Method method = validator.getClass().getMethod("method", Integer.class); + + try { + new InputParamValidator(injector).validateMethodParametersOrThrow(method, 55); + throw new TestException("ClassCastException expected, but wasn't thrown"); + } catch (ClassCastException e) { + // supposed to happen - continue + } + } + + @SuppressWarnings("unchecked") + private RestAnnotationProcessor factory(Class clazz) { + return ((RestAnnotationProcessor) injector.getInstance(Key.get(TypeLiteral.get(Types + .newParameterizedType(RestAnnotationProcessor.class, clazz))))); + } + + Injector injector; + + @BeforeClass + void setupFactory() { + injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bindConstant().annotatedWith(Jsr330.named("testaccount")).to("ralphie"); + bind(URI.class).annotatedWith(RestAnnotationProcessorTest.Localhost.class).toInstance( + URI.create("http://localhost:8080")); + Jsr330.bindProperties(binder(), new PropertiesBuilder() { + + @Override + public PropertiesBuilder withCredentials(String account, String key) { + return null; + } + + @Override + public PropertiesBuilder withEndpoint(URI endpoint) { + return null; + } + }.build()); + } + + }, new RestModule(), new ExecutorServiceModule(sameThreadExecutor(), sameThreadExecutor()), + new JavaUrlHttpCommandExecutorServiceModule()); + + } + +}