diff --git a/git b/git new file mode 100644 index 0000000..e69de29 diff --git a/jax-rs-linker-integration-tests/no-configuration-webapp/src/test/java/fr/vidal/oss/jax_rs_linker/it/BrandResourceLinkerTest.java b/jax-rs-linker-integration-tests/no-configuration-webapp/src/test/java/fr/vidal/oss/jax_rs_linker/it/BrandResourceLinkerTest.java new file mode 100644 index 0000000..bb06c6d --- /dev/null +++ b/jax-rs-linker-integration-tests/no-configuration-webapp/src/test/java/fr/vidal/oss/jax_rs_linker/it/BrandResourceLinkerTest.java @@ -0,0 +1,27 @@ +package fr.vidal.oss.jax_rs_linker.it; + +import fr.vidal.oss.jax_rs_linker.Linkers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class BrandResourceLinkerTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void no_exception_is_thrown_when_argument_matches_regex() { + Linkers.brandResourceLinker() + .relatedBrandResourceByZipCode() + .replace(BrandResourcePathParameters.CODE, "aBc"); + } + + @Test + public void illegal_argument_exception_is_thrown_when_regex_is_not_matched() { + thrown.expect(IllegalArgumentException.class); + Linkers.brandResourceLinker() + .relatedBrandResourceByZipCode() + .replace(BrandResourcePathParameters.CODE, "aBD2"); + } +} diff --git a/jax-rs-linker/pom.xml b/jax-rs-linker/pom.xml index e1a08f3..85c053d 100644 --- a/jax-rs-linker/pom.xml +++ b/jax-rs-linker/pom.xml @@ -84,6 +84,11 @@ com.google hidden.com.google + + com.google.common.base.Optional + com.google.common.base.Present + com.google.common.base.Absent + true diff --git a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/NoPathParameters.java b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/NoPathParameters.java index 00e97ee..028ec31 100644 --- a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/NoPathParameters.java +++ b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/NoPathParameters.java @@ -1,5 +1,9 @@ package fr.vidal.oss.jax_rs_linker.api; +import com.google.common.base.Optional; + +import java.util.regex.Pattern; + public enum NoPathParameters implements PathParameters { ; @@ -7,4 +11,9 @@ public enum NoPathParameters implements PathParameters { public String placeholder() { throw new UnsupportedOperationException(); } + + @Override + public Optional regex() { + throw new UnsupportedOperationException(); + } } diff --git a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/PathParameters.java b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/PathParameters.java index cfc0871..c829aa0 100644 --- a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/PathParameters.java +++ b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/api/PathParameters.java @@ -1,5 +1,10 @@ package fr.vidal.oss.jax_rs_linker.api; +import com.google.common.base.Optional; + +import java.util.regex.Pattern; + public interface PathParameters { String placeholder(); + Optional regex(); } diff --git a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/ApiPath.java b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/ApiPath.java index 06efecd..a93693a 100644 --- a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/ApiPath.java +++ b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/ApiPath.java @@ -6,6 +6,7 @@ import java.util.Collection; +import static fr.vidal.oss.jax_rs_linker.parser.ApiPaths.decorate; import static fr.vidal.oss.jax_rs_linker.parser.ApiPaths.sanitize; public class ApiPath { @@ -15,7 +16,7 @@ public class ApiPath { public ApiPath(String path, Collection pathParameters) { this.path = sanitize(path); - this.pathParameters = pathParameters; + this.pathParameters = decorate(pathParameters, path); } public String getPath() { diff --git a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/PathParameter.java b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/PathParameter.java index 6b395a9..3b0aa0c 100644 --- a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/PathParameter.java +++ b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/PathParameter.java @@ -1,19 +1,38 @@ package fr.vidal.oss.jax_rs_linker.model; -import com.google.common.base.Objects; +import com.google.common.base.*; +import java.util.regex.Pattern; + +import static com.google.common.base.Optional.absent; +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; public class PathParameter { private final ClassName type; private final String name; + private final Optional regex; public PathParameter(ClassName type, String name) { + this(type, name, Optional.absent()); + } + + public PathParameter(ClassName type, String name, String regex) { + this.type = type; + this.name = name; + this.regex = Optional.of(Pattern.compile(regex)); + } + + private PathParameter(ClassName type, String name, Optional regex) { this.type = type; this.name = name; + this.regex = regex; } + + public ClassName getType() { return type; } @@ -22,6 +41,11 @@ public String getName() { return name; } + public Optional getRegex() { + return regex; + } + + @Override public int hashCode() { return Objects.hashCode(type, name); diff --git a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrl.java b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrl.java index 7e59822..d702c43 100644 --- a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrl.java +++ b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrl.java @@ -1,6 +1,7 @@ package fr.vidal.oss.jax_rs_linker.model; import com.google.common.base.Joiner; +import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -12,6 +13,7 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import java.util.regex.Pattern; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Predicates.not; @@ -41,6 +43,7 @@ private TemplatedUrl(String path, Collection pathParameters, Map< public TemplatedUrl replace(T parameter, String value) { checkState(!pathParameters.isEmpty(), "No more path parameters to replace"); + validateParamValue(parameter.regex(), value); return new TemplatedUrl<>( path.replace(placeholder(parameter.placeholder()), value), @@ -61,6 +64,14 @@ public String value() { return path + TO_QUERY_STRING.apply(queryParameters); } + private void validateParamValue(Optional regex, String value) { + if (regex.isPresent()) { + if (!regex.get().matcher(value).matches()) { + throw new IllegalArgumentException(String.format("The given value doesn't match the parameter regex: %s", regex.get())); + } + } + } + private String parameterNames() { return FluentIterable.from(pathParameters) .transform(PathParameterToName.TO_NAME) diff --git a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/parser/ApiPaths.java b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/parser/ApiPaths.java index b09515f..7851921 100644 --- a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/parser/ApiPaths.java +++ b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/parser/ApiPaths.java @@ -1,5 +1,20 @@ package fr.vidal.oss.jax_rs_linker.parser; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import fr.vidal.oss.jax_rs_linker.model.PathParameter; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +import static com.google.common.base.Optional.absent; +import static com.google.common.base.Optional.of; +import static com.google.common.collect.Maps.newHashMap; + public final class ApiPaths { public static String sanitize(String path) { @@ -12,4 +27,41 @@ public static String sanitize(String path) { return path.substring(0, colonPosition).trim() + sanitize(path.substring(closingBracePosition)); } + + public static Collection decorate(Collection pathParameters, String path) { + final Map parametersRegex = extractParametersRegex(path); + + return FluentIterable.from(pathParameters).transform(new Function() { + @Nullable + @Override + public PathParameter apply(PathParameter pathParameter) { + if (parametersRegex.containsKey(pathParameter.getName())) { + return new PathParameter(pathParameter.getType(), + pathParameter.getName(), + parametersRegex.get(pathParameter.getName()) + ); + } + return pathParameter; + } + }).toList(); + } + + private static Map extractParametersRegex(String path) { + Map parameterAndRegex = newHashMap(); + + StringTokenizer st = new StringTokenizer(path, "/"); + while (st.hasMoreTokens()) { + String pathParam = st.nextToken(); + if (pathParam.contains("{")) { + int colonPosition = pathParam.indexOf(':'); + if (colonPosition != -1) { + String name = pathParam.substring(1, colonPosition).trim(); + String regex = pathParam.substring(colonPosition + 1, pathParam.length() - 1).trim(); + parameterAndRegex.put(name, regex); + } + } + + } + return parameterAndRegex; + } } diff --git a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/writer/PathParamsEnumWriter.java b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/writer/PathParamsEnumWriter.java index 2680dc6..212c745 100644 --- a/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/writer/PathParamsEnumWriter.java +++ b/jax-rs-linker/src/main/java/fr/vidal/oss/jax_rs_linker/writer/PathParamsEnumWriter.java @@ -1,10 +1,8 @@ package fr.vidal.oss.jax_rs_linker.writer; +import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; -import com.squareup.javapoet.AnnotationSpec; -import com.squareup.javapoet.JavaFile; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.*; import fr.vidal.oss.jax_rs_linker.LinkerAnnotationProcessor; import fr.vidal.oss.jax_rs_linker.api.PathParameters; import fr.vidal.oss.jax_rs_linker.functions.MappingToPathParameters; @@ -17,6 +15,7 @@ import java.io.IOException; import java.util.Collection; import java.util.Comparator; +import java.util.regex.Pattern; import static javax.lang.model.element.Modifier.*; @@ -40,17 +39,33 @@ public void write(ClassName generatedClass, Collection mappings) throws writeEnumeration(mappings, typeBuilder); + TypeName optionalOfString = + ParameterizedTypeName.get( + com.squareup.javapoet.ClassName.get(Optional.class), + com.squareup.javapoet.ClassName.get(Pattern.class) + ); + typeBuilder.addField(String.class, "placeholder", PRIVATE, FINAL) - .addMethod(MethodSpec.constructorBuilder() - .addParameter(String.class, "placeholder") - .addCode("this.$L = $L;\n", "placeholder", "placeholder") - .build()) - .addMethod(MethodSpec.methodBuilder("placeholder") - .addAnnotation(Override.class) - .addModifiers(PUBLIC, FINAL) - .returns(String.class) - .addCode("return this.$L;\n", "placeholder") - .build()); + .addField(optionalOfString, "regex", PRIVATE, FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(String.class, "placeholder") + .addParameter(optionalOfString, "regex") + .addCode("this.$L = $L;\n", "placeholder", "placeholder") + .addCode("this.$L = $L;\n", "regex", "regex") + .build()) + .addMethod(MethodSpec.methodBuilder("placeholder") + .addAnnotation(Override.class) + .addModifiers(PUBLIC, FINAL) + .returns(String.class) + .addCode("return this.$L;\n", "placeholder") + .build()) + .addMethod(MethodSpec.methodBuilder("regex") + .addAnnotation(Override.class) + .addModifiers(PUBLIC, FINAL) + .returns(optionalOfString) + .addCode("return this.$L;\n", "regex") + .build()); + ; JavaFile.builder(generatedClass.packageName(), typeBuilder.build()) @@ -62,10 +77,21 @@ public void write(ClassName generatedClass, Collection mappings) throws private void writeEnumeration(Collection mappings, TypeSpec.Builder typeBuilder) throws IOException { for (PathParameter parameter : enumConstants(mappings)) { - typeBuilder.addEnumConstant( - EnumConstants.constantName(parameter.getName()), - TypeSpec.anonymousClassBuilder("$S", parameter.getName()).build() - ); + Optional regex = parameter.getRegex(); + String name = parameter.getName(); + if (regex.isPresent()) { + typeBuilder.addEnumConstant( + EnumConstants.constantName(name), + TypeSpec.anonymousClassBuilder("$S, Optional.of(Pattern.compile($S))", name, regex.get()) + .build() + ); + } else { + typeBuilder.addEnumConstant( + EnumConstants.constantName(name), + TypeSpec.anonymousClassBuilder("$S, Optional.absent()", name) + .build() + ); + } } } diff --git a/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrlTest.java b/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrlTest.java index 2bc1f7c..eae456d 100644 --- a/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrlTest.java +++ b/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/model/TemplatedUrlTest.java @@ -1,5 +1,6 @@ package fr.vidal.oss.jax_rs_linker.model; +import com.google.common.base.Optional; import com.google.common.collect.Lists; import fr.vidal.oss.jax_rs_linker.api.NoQueryParameters; import fr.vidal.oss.jax_rs_linker.api.PathParameters; @@ -8,6 +9,7 @@ import org.junit.rules.ExpectedException; import java.util.Collection; +import java.util.regex.Pattern; import static com.google.common.collect.Lists.newArrayList; import static org.assertj.core.api.Assertions.assertThat; @@ -67,12 +69,16 @@ private ClassName className(String type) { } } -enum ProductParameters implements PathParameters -{ +enum ProductParameters implements PathParameters { ID; @Override public String placeholder() { return "id"; } + + @Override + public Optional regex() { + return Optional.absent(); + } } diff --git a/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/parser/ApiPathsTest.java b/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/parser/ApiPathsTest.java index cebbfbc..743475a 100644 --- a/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/parser/ApiPathsTest.java +++ b/jax-rs-linker/src/test/java/fr/vidal/oss/jax_rs_linker/parser/ApiPathsTest.java @@ -1,7 +1,18 @@ package fr.vidal.oss.jax_rs_linker.parser; +import com.google.common.base.Optional; +import fr.vidal.oss.jax_rs_linker.model.ClassName; +import fr.vidal.oss.jax_rs_linker.model.PathParameter; +import org.assertj.core.api.iterable.Extractor; import org.junit.Test; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import static com.google.common.collect.Sets.newHashSet; +import static fr.vidal.oss.jax_rs_linker.parser.ApiPaths.decorate; import static fr.vidal.oss.jax_rs_linker.parser.ApiPaths.sanitize; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +53,70 @@ public void keeps_regexless_parameterized_paths_untouched() { assertThat(sanitize("/api/boring/{parameterized}/path")) .isEqualTo("/api/boring/{parameterized}/path"); } + + @Test + public void adds_regex_to_path_parameters() { + Set pathParameters = newHashSet(new PathParameter(ClassName.valueOf(String.class.getName()), "param")); + Collection result = decorate(pathParameters, "/api/boring/{param:([a-zA-Z0-9])+}"); + + assertThat(result).extracting(patternToOptionalOfString()) + .containsExactly(Optional.of("([a-zA-Z0-9])+")); + } + + @Test + public void adds_correct_regex_to_multiple_path_parameters() { + Set pathParameters = newHashSet( + new PathParameter(ClassName.valueOf(String.class.getName()), "param"), + new PathParameter(ClassName.valueOf(String.class.getName()), "other") + ); + Collection result = decorate(pathParameters, + "/api/boring/{param:([a-zA-Z0-9])+}/{other:[1-9]}"); + + assertThat(result).extracting(patternToOptionalOfString()) + .contains(Optional.of("([a-zA-Z0-9])+"), Optional.of("[1-9]")); + } + + @Test + public void keeps_regexless_path_parameters_untouched() { + Set pathParameters = newHashSet(new PathParameter(ClassName.valueOf(String.class.getName()), "param")); + Collection result = decorate(pathParameters, "/api/boring/{param}"); + + assertThat(result).extracting(patternToOptionalOfString()).containsExactly(Optional.absent()); + } + + @Test + public void keeps_regexless_multiple_path_parameters_untouched() { + Set pathParameters = newHashSet( + new PathParameter(ClassName.valueOf(String.class.getName()), "param"), + new PathParameter(ClassName.valueOf(String.class.getName()), "other") + ); + Collection result = decorate(pathParameters, "/api/boring/{param}/{other}"); + + assertThat(result).extracting(patternToOptionalOfString()) + .containsExactly(Optional.absent(), Optional.absent()); + } + + @Test + public void adds_regex_to_corresponding_path_parameter_and_keeps_regexless_path_parameter_untouched() { + Set pathParameters = newHashSet( + new PathParameter(ClassName.valueOf(String.class.getName()), "param"), + new PathParameter(ClassName.valueOf(String.class.getName()), "other"), + new PathParameter(ClassName.valueOf(String.class.getName()), "last") + ); + Collection result = decorate(pathParameters, "/api/boring/{param}/{other:[1-9]}/{last}"); + + assertThat(result).extracting(patternToOptionalOfString()) + .contains(Optional.absent(), Optional.of("[1-9]"), Optional.absent()); + } + + private Extractor> patternToOptionalOfString() { + return new Extractor>() { + @Override + public Optional extract(PathParameter pathParameter) { + return pathParameter.getRegex().isPresent()? + Optional.of(pathParameter.getRegex().get().pattern()): + Optional.absent(); + } + }; + } } diff --git a/jax-rs-linker/src/test/resources/BrandResourcePathParameters.java b/jax-rs-linker/src/test/resources/BrandResourcePathParameters.java index c5192bb..ac46c1c 100644 --- a/jax-rs-linker/src/test/resources/BrandResourcePathParameters.java +++ b/jax-rs-linker/src/test/resources/BrandResourcePathParameters.java @@ -1,27 +1,37 @@ package fr.vidal.oss.jax_rs_linker.parser; +import com.google.common.base.Optional; import fr.vidal.oss.jax_rs_linker.api.PathParameters; import java.lang.Override; import java.lang.String; +import java.util.regex.Pattern; import javax.annotation.Generated; @Generated("fr.vidal.oss.jax_rs_linker.LinkerAnnotationProcessor") public enum BrandResourcePathParameters implements PathParameters { - CODE("code"), + CODE("code", Optional.absent()), - ID("id"), + ID("id", Optional.absent()), - ZIP("zip"); + ZIP("zip", Optional.absent()); private final String placeholder; - BrandResourcePathParameters(String placeholder) { + private final Optional regex; + + BrandResourcePathParameters(String placeholder, Optional regex) { this.placeholder = placeholder; + this.regex = regex; } @Override public final String placeholder() { return this.placeholder; } + + @Override + public final Optional regex() { + return this.regex; + } } diff --git a/jax-rs-linker/src/test/resources/PersonResourcePathParameters.java b/jax-rs-linker/src/test/resources/PersonResourcePathParameters.java index 3d9f5f7..9286cad 100644 --- a/jax-rs-linker/src/test/resources/PersonResourcePathParameters.java +++ b/jax-rs-linker/src/test/resources/PersonResourcePathParameters.java @@ -1,25 +1,34 @@ package fr.vidal.oss.jax_rs_linker.parser; +import com.google.common.base.Optional; import fr.vidal.oss.jax_rs_linker.api.PathParameters; import java.lang.Override; import java.lang.String; +import java.util.regex.Pattern; import javax.annotation.Generated; @Generated("fr.vidal.oss.jax_rs_linker.LinkerAnnotationProcessor") public enum PersonResourcePathParameters implements PathParameters { - FIRST_NAME("firstName"), + FIRST_NAME("firstName", Optional.of(Pattern.compile(".*"))), - ID("id"); + ID("id", Optional.absent()); private final String placeholder; + private final Optional regex; - PersonResourcePathParameters(String placeholder) { + PersonResourcePathParameters(String placeholder, Optional regex) { this.placeholder = placeholder; + this.regex = regex; } @Override public final String placeholder() { return this.placeholder; } + + @Override + public final Optional regex() { + return this.regex; + } } diff --git a/jax-rs-linker/src/test/resources/ProductResourcePathParameters.java b/jax-rs-linker/src/test/resources/ProductResourcePathParameters.java index 767811c..fdee8c1 100644 --- a/jax-rs-linker/src/test/resources/ProductResourcePathParameters.java +++ b/jax-rs-linker/src/test/resources/ProductResourcePathParameters.java @@ -1,23 +1,33 @@ package fr.vidal.oss.jax_rs_linker.parser; +import com.google.common.base.Optional; import fr.vidal.oss.jax_rs_linker.api.PathParameters; import java.lang.Override; import java.lang.String; +import java.util.regex.Pattern; import javax.annotation.Generated; @Generated("fr.vidal.oss.jax_rs_linker.LinkerAnnotationProcessor") public enum ProductResourcePathParameters implements PathParameters { - ID("id"); + ID("id", Optional.absent()); private final String placeholder; - ProductResourcePathParameters(String placeholder) { + private final Optional regex; + + ProductResourcePathParameters(String placeholder, Optional regex) { this.placeholder = placeholder; + this.regex = regex; } @Override public final String placeholder() { return this.placeholder; } + + @Override + public final Optional regex() { + return this.regex; + } }