diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/QueryBuilder.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/QueryBuilder.java index 453231fa69d..d053e056946 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/QueryBuilder.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/QueryBuilder.java @@ -18,74 +18,45 @@ import org.cloudfoundry.QueryParameter; import org.cloudfoundry.reactor.util.AnnotationUtils; -import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.Exceptions; +import org.cloudfoundry.reactor.util.AnnotationUtils.AnnotatedValue; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameterBuilder; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Collection; -import java.util.Optional; -import java.util.function.Consumer; +import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A builder for Cloud Foundry queries */ -public final class QueryBuilder { +public final class QueryBuilder implements UriQueryParameterBuilder { - private QueryBuilder() { + public Stream build(Object instance) { + return AnnotationUtils.streamAnnotatedValues(instance, QueryParameter.class) + .map(QueryBuilder::processValue) + .filter(Objects::nonNull); } - /** - * Augments a {@link UriComponentsBuilder} with queries based on the methods annotated with {@link QueryParameter} - * - * @param builder the builder to augment - * @param instance the instance to inspect and invoke - */ - public static void augment(UriComponentsBuilder builder, Object instance) { - Arrays.stream(instance.getClass().getMethods()) - .sorted(MethodNameComparator.INSTANCE) - .forEach(processMethod(builder, instance)); + private static UriQueryParameter processCollection(QueryParameter queryParameter, Object value) { + return processValue(queryParameter.value(), ((Collection) value).stream() + .map(Object::toString) + .map(String::trim) + .collect(Collectors.joining(queryParameter.delimiter()))); } - private static Optional getValue(Method method, Object instance) { - try { - return Optional.ofNullable(method.invoke(instance)); - } catch (IllegalAccessException | InvocationTargetException e) { - throw Exceptions.propagate(e); + private static UriQueryParameter processValue(AnnotatedValue annotatedValue) { + QueryParameter queryParameter = annotatedValue.getAnnotation(); + Object value = annotatedValue.getValue(); + if (value instanceof Collection) { + return processCollection(queryParameter, value); + } else { + return processValue(queryParameter.value(), value.toString()); } } - private static Consumer processAnnotation(UriComponentsBuilder builder, Method method, Object instance) { - return queryParameter -> getValue(method, instance) - .ifPresent(processValue(builder, queryParameter)); - } - - private static void processCollection(UriComponentsBuilder builder, QueryParameter queryParameter, Object value) { - processValue(builder, queryParameter.value(), - ((Collection) value).stream() - .map(o -> o.toString().trim()) - .collect(Collectors.joining(queryParameter.delimiter()))); - } - - private static Consumer processMethod(UriComponentsBuilder builder, Object instance) { - return method -> AnnotationUtils.findAnnotation(method, QueryParameter.class) - .ifPresent(processAnnotation(builder, method, instance)); - } - - private static void processValue(UriComponentsBuilder builder, String name, String value) { - builder.queryParam(name, value); - } - - private static Consumer processValue(UriComponentsBuilder builder, QueryParameter queryParameter) { - return value -> { - if (value instanceof Collection) { - processCollection(builder, queryParameter, value); - } else { - processValue(builder, queryParameter.value(), value.toString()); - } - }; + private static UriQueryParameter processValue(String name, String value) { + return UriQueryParameter.of(name, value); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java index 64449c39617..e38e4fadf62 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java @@ -20,9 +20,13 @@ import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.QueryBuilder; import org.cloudfoundry.reactor.util.AbstractReactorOperations; +import org.cloudfoundry.reactor.util.DelegatingUriQueryParameterBuilder; import org.cloudfoundry.reactor.util.ErrorPayloadMappers; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; import org.cloudfoundry.reactor.util.Operator; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameterBuilder; +import org.cloudfoundry.reactor.util.UriQueryParameters; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -34,6 +38,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Stream; public abstract class AbstractClientV2Operations extends AbstractReactorOperations { @@ -101,14 +106,18 @@ protected final Mono put(Object requestPayload, Class responseType, Fu .parseBody(responseType)); } - private static Function queryTransformer(Object requestPayload) { + private Function queryTransformer(Object requestPayload) { return builder -> { - FilterBuilder.augment(builder, requestPayload); - QueryBuilder.augment(builder, requestPayload); + Stream parameters = getUriQueryParameterBuilder().build(requestPayload); + UriQueryParameters.set(builder, parameters); return builder; }; } + private UriQueryParameterBuilder getUriQueryParameterBuilder() { + return DelegatingUriQueryParameterBuilder.builder().builders(new FilterBuilder(), new QueryBuilder()).build(); + } + private Operator attachErrorPayloadMapper(Operator operator) { return operator.withErrorPayloadMapper(ErrorPayloadMappers.clientV2(this.connectionContext.getObjectMapper())); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/FilterBuilder.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/FilterBuilder.java index cd7d41ff0e5..3313c8ac623 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/FilterBuilder.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/FilterBuilder.java @@ -17,89 +17,64 @@ package org.cloudfoundry.reactor.client.v2; import org.cloudfoundry.client.v2.FilterParameter; -import org.cloudfoundry.reactor.client.MethodNameComparator; import org.cloudfoundry.reactor.util.AnnotationUtils; -import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.Exceptions; +import org.cloudfoundry.reactor.util.AnnotationUtils.AnnotatedValue; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameterBuilder; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Optional; -import java.util.function.Consumer; +import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A builder for Cloud Foundry V2 filters */ -final class FilterBuilder { +final class FilterBuilder implements UriQueryParameterBuilder { - private FilterBuilder() { + public Stream build(Object instance) { + return AnnotationUtils.streamAnnotatedValues(instance, FilterParameter.class) + .map(FilterBuilder::processValue) + .filter(Objects::nonNull); } - /** - * Augments a {@link UriComponentsBuilder} with queries based on the methods annotated with {@link FilterParameter} - * - * @param builder the builder to augment - * @param instance the instance to inspect and invoke - */ - public static void augment(UriComponentsBuilder builder, Object instance) { - Arrays.stream(instance.getClass().getMethods()) - .sorted(MethodNameComparator.INSTANCE) - .forEach(processMethod(builder, instance)); - } - - private static Optional getValue(Method method, Object instance) { - try { - return Optional.ofNullable(method.invoke(instance)); - } catch (IllegalAccessException | InvocationTargetException e) { - throw Exceptions.propagate(e); - } - } - - private static Consumer processAnnotation(UriComponentsBuilder builder, Method method, Object instance) { - return filterParameter -> getValue(method, instance) - .ifPresent(processValue(builder, filterParameter)); - } - - private static void processCollection(UriComponentsBuilder builder, FilterParameter filterParameter, Object value) { + private static UriQueryParameter processCollection(FilterParameter filterParameter, Object value) { List collection = ((Collection) value).stream() - .map(o -> o.toString().trim()) + .map(Object::toString) + .map(String::trim) .collect(Collectors.toList()); if (collection.size() == 1) { - processValue(builder, filterParameter.value(), filterParameter.operation(), collection.get(0)); + return processValue(filterParameter.value(), filterParameter.operation(), collection.get(0)); } else if (collection.size() > 1) { - processValue(builder, filterParameter.value(), filterParameter.collectionOperation(), collection); + return processValue(filterParameter.value(), filterParameter.collectionOperation(), collection); + } else { + return null; } } - private static Consumer processMethod(UriComponentsBuilder builder, Object instance) { - return method -> AnnotationUtils.findAnnotation(method, FilterParameter.class) - .ifPresent(processAnnotation(builder, method, instance)); - } - - private static Consumer processValue(UriComponentsBuilder builder, FilterParameter filterParameter) { - return value -> { - if (value instanceof Collection) { - processCollection(builder, filterParameter, value); - } else { - processValue(builder, filterParameter.value(), filterParameter.operation(), value.toString().trim()); - } - }; + private static UriQueryParameter processValue(AnnotatedValue annotatedValue) { + FilterParameter filterParameter = annotatedValue.getAnnotation(); + Object value = annotatedValue.getValue(); + if (value instanceof Collection) { + return processCollection(filterParameter, value); + } else { + return processValue(filterParameter.value(), filterParameter.operation(), value.toString() + .trim()); + } } - private static void processValue(UriComponentsBuilder builder, String name, FilterParameter.Operation operation, Collection collection) { + private static UriQueryParameter processValue(String name, FilterParameter.Operation operation, Collection collection) { String value = String.join(",", collection); if (!value.isEmpty()) { - processValue(builder, name, operation, value); + return processValue(name, operation, value); } + return null; } - private static void processValue(UriComponentsBuilder builder, String name, FilterParameter.Operation operation, String value) { - builder.queryParam("q", String.format("%s%s%s", name, operation, value)); + private static UriQueryParameter processValue(String name, FilterParameter.Operation operation, String value) { + return UriQueryParameter.of("q", String.format("%s%s%s", name, operation, value)); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java index 31ce486b86f..029ab44c849 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java @@ -21,9 +21,13 @@ import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.QueryBuilder; import org.cloudfoundry.reactor.util.AbstractReactorOperations; +import org.cloudfoundry.reactor.util.DelegatingUriQueryParameterBuilder; import org.cloudfoundry.reactor.util.ErrorPayloadMappers; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; import org.cloudfoundry.reactor.util.Operator; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameterBuilder; +import org.cloudfoundry.reactor.util.UriQueryParameters; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -37,6 +41,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Stream; public abstract class AbstractClientV3Operations extends AbstractReactorOperations { @@ -129,15 +134,18 @@ private static String extractJobId(HttpClientResponse response) { return pathSegments.get(pathSegments.size() - 1); } - private static Function queryTransformer(Object requestPayload) { + private Function queryTransformer(Object requestPayload) { return builder -> { - FilterBuilder.augment(builder, requestPayload); - QueryBuilder.augment(builder, requestPayload); - + Stream parameters = getUriQueryParameterBuilder().build(requestPayload); + UriQueryParameters.set(builder, parameters); return builder; }; } + private UriQueryParameterBuilder getUriQueryParameterBuilder() { + return DelegatingUriQueryParameterBuilder.builder().builders(new FilterBuilder(), new QueryBuilder()).build(); + } + private Operator attachErrorPayloadMapper(Operator operator) { return operator.withErrorPayloadMapper(ErrorPayloadMappers.clientV3(this.connectionContext.getObjectMapper())); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/FilterBuilder.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/FilterBuilder.java index 367b2b144a0..2e2c545ff79 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/FilterBuilder.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/FilterBuilder.java @@ -17,80 +17,51 @@ package org.cloudfoundry.reactor.client.v3; import org.cloudfoundry.client.v3.FilterParameter; -import org.cloudfoundry.reactor.client.MethodNameComparator; import org.cloudfoundry.reactor.util.AnnotationUtils; -import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.Exceptions; +import org.cloudfoundry.reactor.util.AnnotationUtils.AnnotatedValue; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameterBuilder; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Collection; -import java.util.Optional; -import java.util.function.Consumer; +import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Stream; -final class FilterBuilder { +final class FilterBuilder implements UriQueryParameterBuilder { - private FilterBuilder() { + public Stream build(Object instance) { + return AnnotationUtils.streamAnnotatedValues(instance, FilterParameter.class) + .map(FilterBuilder::processValue) + .filter(Objects::nonNull); } - /** - * Augments a {@link UriComponentsBuilder} with queries based on the methods annotated with {@link FilterParameter} - * - * @param builder the builder to augment - * @param instance the instance to inspect and invoke - */ - public static void augment(UriComponentsBuilder builder, Object instance) { - Arrays.stream(instance.getClass().getMethods()) - .sorted(MethodNameComparator.INSTANCE) - .forEach(processMethod(builder, instance)); + private static UriQueryParameter processCollection(String name, Object value) { + return processValue(name, ((Collection) value).stream() + .map(Object::toString) + .map(String::trim) + .collect(Collectors.toList())); } - private static Optional getValue(Method method, Object instance) { - try { - return Optional.ofNullable(method.invoke(instance)); - } catch (IllegalAccessException | InvocationTargetException e) { - throw Exceptions.propagate(e); + private static UriQueryParameter processValue(AnnotatedValue annotatedValue) { + FilterParameter filterParameter = annotatedValue.getAnnotation(); + Object value = annotatedValue.getValue(); + if (value instanceof Collection) { + return processCollection(filterParameter.value(), value); + } else { + return processValue(filterParameter.value(), value.toString()); } } - private static Consumer processAnnotation(UriComponentsBuilder builder, Method method, Object instance) { - return filterParameter -> getValue(method, instance) - .ifPresent(processValue(builder, filterParameter)); - } - - private static void processCollection(UriComponentsBuilder builder, String name, Object value) { - processValue(builder, name, - ((Collection) value).stream() - .map(o -> o.toString().trim()) - .collect(Collectors.toList())); - } - - private static Consumer processMethod(UriComponentsBuilder builder, Object instance) { - return method -> AnnotationUtils.findAnnotation(method, FilterParameter.class) - .ifPresent(processAnnotation(builder, method, instance)); - } - - private static void processValue(UriComponentsBuilder builder, String name, Collection collection) { + private static UriQueryParameter processValue(String name, Collection collection) { String value = String.join(",", collection); if (!value.isEmpty()) { - processValue(builder, name, value); + return processValue(name, value); } + return null; } - private static void processValue(UriComponentsBuilder builder, String name, String value) { - builder.queryParam(name, value); - } - - private static Consumer processValue(UriComponentsBuilder builder, FilterParameter filterParameter) { - return value -> { - if (value instanceof Collection) { - processCollection(builder, filterParameter.value(), value); - } else { - processValue(builder, filterParameter.value(), value.toString()); - } - }; + private static UriQueryParameter processValue(String name, String value) { + return UriQueryParameter.of(name, value); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java index 537806cc803..5f306994ba5 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java @@ -20,11 +20,14 @@ import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.QueryBuilder; import org.cloudfoundry.reactor.util.AbstractReactorOperations; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameters; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import java.util.Map; import java.util.function.Function; +import java.util.stream.Stream; public abstract class AbstractNetworkingOperations extends AbstractReactorOperations { @@ -66,9 +69,10 @@ protected final Mono put(Object requestPayload, Class responseType, Fu .parseBody(responseType)); } - private static Function queryTransformer(Object requestPayload) { + private Function queryTransformer(Object requestPayload) { return builder -> { - QueryBuilder.augment(builder, requestPayload); + Stream parameters = new QueryBuilder().build(requestPayload); + UriQueryParameters.set(builder, parameters); return builder; }; } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java index c2f45c5036c..bca0c9c59f7 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java @@ -23,6 +23,8 @@ import org.cloudfoundry.reactor.util.AbstractReactorOperations; import org.cloudfoundry.reactor.util.ErrorPayloadMappers; import org.cloudfoundry.reactor.util.Operator; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameters; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClientResponse; @@ -30,7 +32,7 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.UnaryOperator; +import java.util.stream.Stream; public abstract class AbstractUaaOperations extends AbstractReactorOperations { @@ -160,9 +162,10 @@ private static void addHeaders(HttpHeaders httpHeaders, Object requestPayload) { VersionBuilder.augment(httpHeaders, requestPayload); } - private static UnaryOperator queryTransformer(Object requestPayload) { + private Function queryTransformer(Object requestPayload) { return builder -> { - QueryBuilder.augment(builder, requestPayload); + Stream parameters = new QueryBuilder().build(requestPayload); + UriQueryParameters.set(builder, parameters); return builder; }; } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AnnotationUtils.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AnnotationUtils.java index d89ac4cfa46..3be2f9618b6 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AnnotationUtils.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AnnotationUtils.java @@ -16,18 +16,47 @@ package org.cloudfoundry.reactor.util; +import org.cloudfoundry.reactor.client.MethodNameComparator; +import reactor.core.Exceptions; + import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; public final class AnnotationUtils { private AnnotationUtils() { } - public static Optional findAnnotation(Method method, Class annotationType) { - Class clazz = method.getDeclaringClass(); - T annotation = method.getAnnotation(annotationType); + public static class AnnotatedValue { + + private final T annotation; + + private final Object value; + + public AnnotatedValue(T annotation, Object value) { + this.annotation = annotation; + this.value = value; + } + + public T getAnnotation() { + return annotation; + } + + public Object getValue() { + return value; + } + + } + + public static Optional findAnnotation(Class type, Class annotationType) { + Class clazz = type; + T annotation = clazz.getAnnotation(annotationType); while (annotation == null) { clazz = clazz.getSuperclass(); @@ -36,19 +65,23 @@ public static Optional findAnnotation(Method method, C break; } - try { - annotation = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()).getAnnotation(annotationType); - } catch (NoSuchMethodException e) { - // No equivalent method found - } + annotation = clazz.getAnnotation(annotationType); } return Optional.ofNullable(annotation); } - public static Optional findAnnotation(Class type, Class annotationType) { - Class clazz = type; - T annotation = clazz.getAnnotation(annotationType); + public static Stream> streamAnnotatedValues(Object instance, Class annotationClass) { + Class instanceClass = instance.getClass(); + return Arrays.stream(instanceClass.getMethods()) + .sorted(MethodNameComparator.INSTANCE) + .map(processMethod(instance, annotationClass)) + .filter(Objects::nonNull); + } + + private static Optional findAnnotation(Method method, Class annotationType) { + Class clazz = method.getDeclaringClass(); + T annotation = method.getAnnotation(annotationType); while (annotation == null) { clazz = clazz.getSuperclass(); @@ -57,10 +90,32 @@ public static Optional findAnnotation(Class type, C break; } - annotation = clazz.getAnnotation(annotationType); + try { + annotation = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()) + .getAnnotation(annotationType); + } catch (NoSuchMethodException e) { + // No equivalent method found + } } return Optional.ofNullable(annotation); } + private static Optional getValue(Method method, Object instance) { + try { + return Optional.ofNullable(method.invoke(instance)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw Exceptions.propagate(e); + } + } + + private static Function>> processAnnotation(Method method, Object instance) { + return annotation -> getValue(method, instance).map(value -> new AnnotatedValue(annotation, value)); + } + + private static Function> processMethod(Object instance, Class annotationClass) { + return method -> findAnnotation(method, annotationClass).flatMap(processAnnotation(method, instance)) + .orElse(null); + } + } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java index 7a9a01cc28f..d2078ba1fcb 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java @@ -315,8 +315,8 @@ class OperatorContextAware { protected String transformRoot(Function uriTransformer) { UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(this.context.getRoot()); return uriTransformer.apply(uriComponentsBuilder) - .build() .encode() + .build() .toUriString(); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriQueryParameterBuilder.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriQueryParameterBuilder.java new file mode 100644 index 00000000000..c47096512a6 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriQueryParameterBuilder.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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.cloudfoundry.reactor.util; + +import java.util.stream.Stream; + +public interface UriQueryParameterBuilder { + + Stream build(Object request); + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriQueryParameters.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriQueryParameters.java new file mode 100644 index 00000000000..f1362e59348 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriQueryParameters.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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.cloudfoundry.reactor.util; + +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.List; +import java.util.stream.Stream; + +public class UriQueryParameters { + + public static void set(UriComponentsBuilder builder, Stream uriQueryParameters) { + // Replace all literal values with URI variables to apply more strict encoding: + UriVariablesRegistry uriVariablesRegistry = new UriVariablesRegistry(); + uriQueryParameters.forEach(uriQueryParameter -> { + UriVariable uriVariable = uriVariablesRegistry.register(uriQueryParameter.getValue()); + builder.queryParam(uriQueryParameter.getKey(), uriVariable.getPlaceholder()); + }); + builder.uriVariables(uriVariablesRegistry.getUriVariablesMap()); + } + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriVariablesRegistry.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriVariablesRegistry.java new file mode 100644 index 00000000000..edde7f808c0 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UriVariablesRegistry.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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.cloudfoundry.reactor.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class UriVariablesRegistry { + + private final List uriVariables = new ArrayList<>(); + + public Map getUriVariablesMap() { + return uriVariables.stream().collect(Collectors.toMap(UriVariable::getKey, UriVariable::getValue)); + } + + public UriVariable register(Object value) { + UriVariable uriVariable = UriVariable.of(getNextUriVariableKey(), value); + uriVariables.add(uriVariable); + + return uriVariable; + } + + private String getNextUriVariableKey() { + return Integer.toString(uriVariables.size()); + } + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_DelegatingUriQueryParameterBuilder.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_DelegatingUriQueryParameterBuilder.java new file mode 100644 index 00000000000..fe0b7ac7c33 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_DelegatingUriQueryParameterBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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.cloudfoundry.reactor.util; + +import org.immutables.value.Value; + +import java.util.List; +import java.util.stream.Stream; + +@Value.Immutable +public abstract class _DelegatingUriQueryParameterBuilder implements UriQueryParameterBuilder { + + public abstract List getBuilders(); + + @Override + public Stream build(Object request) { + return getBuilders().stream().flatMap(extractor -> extractor.build(request)); + } + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_UriQueryParameter.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_UriQueryParameter.java new file mode 100644 index 00000000000..2ecf3e4116a --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_UriQueryParameter.java @@ -0,0 +1,14 @@ +package org.cloudfoundry.reactor.util; + +import org.immutables.value.Value; + +@Value.Immutable +public interface _UriQueryParameter { + + @Value.Parameter + String getKey(); + + @Value.Parameter + Object getValue(); + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_UriVariable.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_UriVariable.java new file mode 100644 index 00000000000..1756800c548 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_UriVariable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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.cloudfoundry.reactor.util; + +import org.immutables.value.Value; + +@Value.Immutable +public interface _UriVariable { + + String PLACEHOLDER_PATTERN = "{%s}"; + + @Value.Parameter + String getKey(); + + @Value.Parameter + Object getValue(); + + @Value.Derived + default String getPlaceholder() { + return String.format(PLACEHOLDER_PATTERN, getKey()); + } + +} diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/QueryBuilderTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/QueryBuilderTest.java index a6270b3d9a9..803c1da091e 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/QueryBuilderTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/QueryBuilderTest.java @@ -17,6 +17,8 @@ package org.cloudfoundry.reactor.client; import org.cloudfoundry.QueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameters; import org.junit.Test; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; @@ -24,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -33,16 +36,18 @@ public final class QueryBuilderTest { public void test() { UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); - QueryBuilder.augment(builder, new StubQueryParamsSubClass()); + Stream parameters = new QueryBuilder().build(new StubQueryParamsSubClass()); + UriQueryParameters.set(builder, parameters); - MultiValueMap queryParams = builder.build().encode().getQueryParams(); + MultiValueMap queryParams = builder.encode().build().getQueryParams(); - assertThat(queryParams).hasSize(7); + assertThat(queryParams).hasSize(8); assertThat(queryParams.getFirst("test-single")).isEqualTo("test-value-1"); - assertThat(queryParams.getFirst("test-collection")).isEqualTo("test-value-2,test-value-3"); + assertThat(queryParams.getFirst("test-collection")).isEqualTo("test-value-2%2Ctest-value-3"); assertThat(queryParams.getFirst("test-collection-custom-delimiter")).isEqualTo("test-value-4%20test-value-5"); assertThat(queryParams.getFirst("test-subclass")).isEqualTo("test-value-6"); assertThat(queryParams.getFirst("test-override")).isEqualTo("test-value-7"); + assertThat(queryParams.getFirst("test-reserved-characters")).isEqualTo("%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D"); } public static abstract class StubQueryParams { @@ -77,6 +82,11 @@ public final String getSingle() { return "test-value-1"; } + @QueryParameter("test-reserved-characters") + public final String getReservedCharacters() { + return ":/?#[]@!$&'()*+,;="; + } + @QueryParameter("test-override") abstract String getOverride(); diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/FilterBuilderTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/FilterBuilderTest.java index 4b951d09b63..20c7058307f 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/FilterBuilderTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/FilterBuilderTest.java @@ -17,6 +17,8 @@ package org.cloudfoundry.reactor.client.v2; import org.cloudfoundry.client.v2.FilterParameter; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameters; import org.junit.Test; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; @@ -24,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.cloudfoundry.client.v2.FilterParameter.Operation.GREATER_THAN; @@ -37,22 +40,24 @@ public final class FilterBuilderTest { public void test() { UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); - FilterBuilder.augment(builder, new StubFilterParamsSubClass()); + Stream parameters = new FilterBuilder().build(new StubFilterParamsSubClass()); + UriQueryParameters.set(builder, parameters); - MultiValueMap queryParams = builder.build().encode().getQueryParams(); + MultiValueMap queryParams = builder.encode().build().getQueryParams(); List q = queryParams.get("q"); assertThat(q) - .hasSize(9) - .containsOnly("test-empty-value:", + .hasSize(10) + .containsOnly("test-empty-value%3A", "test-greater-than%3Etest-value-1", "test-greater-than-or-equal-to%3E%3Dtest-value-2", - "test-in%20IN%20test-value-3,test-value-4", - "test-is:test-value-5", + "test-in%20IN%20test-value-3%2Ctest-value-4", + "test-is%3Atest-value-5", "test-less-than%3Ctest-value-6", "test-less-than-or-equal-to%3C%3Dtest-value-7", - "test-default%20IN%20test-value-8,test-value-9", - "test-override:test-value-10"); + "test-default%20IN%20test-value-8%2Ctest-value-9", + "test-override%3Atest-value-10", + "test-reserved-characters%3A%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D"); } public static abstract class StubFilterParams { @@ -97,6 +102,11 @@ public final String getLessThanOrEqualTo() { return "test-value-7"; } + @FilterParameter("test-reserved-characters") + public final String getReservedCharacters() { + return ":/?#[]@!$&'()*+,;="; + } + @FilterParameter("test-null") public final String getNull() { return null; diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java index c99581dba4b..1f20aaffabd 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java @@ -477,7 +477,7 @@ public void instances() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/apps?q=name:test-name&page=-1") + .method(GET).path("/apps?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) @@ -649,7 +649,7 @@ public void listRoutes() { public void listServiceBindings() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/apps/test-application-id/service_bindings?q=service_instance_guid:test-instance-id&page=-1") + .method(GET).path("/apps/test-application-id/service_bindings?q=service_instance_guid%3Atest-instance-id&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java index 3620f7e6481..2c1a90debd9 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java @@ -168,7 +168,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/buildpacks?q=name:test-name&page=-1") + .method(GET).path("/buildpacks?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/events/ReactorEventsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/events/ReactorEventsTest.java index f5fa2ad595e..565d2e9165b 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/events/ReactorEventsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/events/ReactorEventsTest.java @@ -85,7 +85,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/events?q=actee:test-actee&page=-1") + .method(GET).path("/events?q=actee%3Atest-actee&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/organizations/ReactorOrganizationsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/organizations/ReactorOrganizationsTest.java index deb934e161d..639b0d6f879 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/organizations/ReactorOrganizationsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/organizations/ReactorOrganizationsTest.java @@ -776,7 +776,7 @@ public void getUserRoles() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/organizations?q=name:test-name&page=-1") + .method(GET).path("/organizations?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/privatedomains/ReactorPrivateDomainsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/privatedomains/ReactorPrivateDomainsTest.java index 6006b1abad1..3f8f3445cfc 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/privatedomains/ReactorPrivateDomainsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/privatedomains/ReactorPrivateDomainsTest.java @@ -180,7 +180,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/private_domains?q=name:test-name.com&page=-1") + .method(GET).path("/private_domains?q=name%3Atest-name.com&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebindings/ReactorServiceBindingsV2Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebindings/ReactorServiceBindingsV2Test.java index afea177b092..c3bb52333c9 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebindings/ReactorServiceBindingsV2Test.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebindings/ReactorServiceBindingsV2Test.java @@ -232,7 +232,7 @@ public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(GET) - .path("/service_bindings?q=app_guid:dd44fd4f-5e20-4c52-b66d-7af6e201f01e&page=-1") + .path("/service_bindings?q=app_guid%3Add44fd4f-5e20-4c52-b66d-7af6e201f01e&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebrokers/ReactorServiceBrokersTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebrokers/ReactorServiceBrokersTest.java index 8439cacfb2e..51c3322a85b 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebrokers/ReactorServiceBrokersTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicebrokers/ReactorServiceBrokersTest.java @@ -146,7 +146,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/service_brokers?q=name:test-name&page=-1") + .method(GET).path("/service_brokers?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceinstances/ReactorServiceInstancesTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceinstances/ReactorServiceInstancesTest.java index b9488b6f150..581473603ba 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceinstances/ReactorServiceInstancesTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceinstances/ReactorServiceInstancesTest.java @@ -398,7 +398,7 @@ public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(GET) - .path("/service_instances?q=name:test-name&page=-1") + .path("/service_instances?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) @@ -498,7 +498,7 @@ public void listServiceBindings() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(GET) - .path("/service_instances/test-service-instance-id/service_bindings?q=app_guid:test-application-id&page=-1") + .path("/service_instances/test-service-instance-id/service_bindings?q=app_guid%3Atest-application-id&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicekeys/ReactorServiceKeysTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicekeys/ReactorServiceKeysTest.java index 1fe5c5dd822..a0829220929 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicekeys/ReactorServiceKeysTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/servicekeys/ReactorServiceKeysTest.java @@ -143,7 +143,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/service_keys?q=name:test-name&page=-1") + .method(GET).path("/service_keys?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplans/ReactorServicePlansTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplans/ReactorServicePlansTest.java index 3bbbd165699..172e215d858 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplans/ReactorServicePlansTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplans/ReactorServicePlansTest.java @@ -178,7 +178,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/service_plans?q=service_guid:test-service-id&page=-1") + .method(GET).path("/service_plans?q=service_guid%3Atest-service-id&page=-1") .build()) .response(TestResponse.builder() .status(OK) @@ -222,7 +222,7 @@ public void list() { public void listServiceInstances() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/service_plans/test-service-plan-id/service_instances?q=space_guid:test-space-id&page=-1") + .method(GET).path("/service_plans/test-service-plan-id/service_instances?q=space_guid%3Atest-space-id&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplanvisibilities/ReactorServicePlanVisibilitiesTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplanvisibilities/ReactorServicePlanVisibilitiesTest.java index 2fa7c768280..ebc385e13b8 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplanvisibilities/ReactorServicePlanVisibilitiesTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/serviceplanvisibilities/ReactorServicePlanVisibilitiesTest.java @@ -181,7 +181,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/service_plan_visibilities?q=organization_guid:test-organization-id&page=-1") + .method(GET).path("/service_plan_visibilities?q=organization_guid%3Atest-organization-id&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/services/ReactorServicesTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/services/ReactorServicesTest.java index 35166810484..ae1879acae6 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/services/ReactorServicesTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/services/ReactorServicesTest.java @@ -149,7 +149,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/services?q=label:test-label&page=-1") + .method(GET).path("/services?q=label%3Atest-label&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/spaces/ReactorSpacesTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/spaces/ReactorSpacesTest.java index 2639daeb402..f72e3e095eb 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/spaces/ReactorSpacesTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/spaces/ReactorSpacesTest.java @@ -697,7 +697,7 @@ public void getSummary() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/spaces?q=name:test-name&page=-1") + .method(GET).path("/spaces?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) @@ -747,7 +747,7 @@ public void list() { public void listApplications() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/spaces/test-space-id/apps?q=name:test-name&page=-1") + .method(GET).path("/spaces/test-space-id/apps?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/stacks/ReactorStacksTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/stacks/ReactorStacksTest.java index f0e2444a10e..55178f81812 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/stacks/ReactorStacksTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/stacks/ReactorStacksTest.java @@ -172,7 +172,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/stacks?q=name:test-name&page=-1") + .method(GET).path("/stacks?q=name%3Atest-name&page=-1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/FilterBuilderTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/FilterBuilderTest.java index a3fd507acb7..9723e620ffb 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/FilterBuilderTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/FilterBuilderTest.java @@ -17,6 +17,8 @@ package org.cloudfoundry.reactor.client.v3; import org.cloudfoundry.client.v3.FilterParameter; +import org.cloudfoundry.reactor.util.UriQueryParameter; +import org.cloudfoundry.reactor.util.UriQueryParameters; import org.junit.Test; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; @@ -24,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -33,15 +36,17 @@ public final class FilterBuilderTest { public void test() { UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); - FilterBuilder.augment(builder, new StubFilterParamsSubClass()); + Stream parameters = new FilterBuilder().build(new StubFilterParamsSubClass()); + UriQueryParameters.set(builder, parameters); - MultiValueMap queryParams = builder.build().encode().getQueryParams(); + MultiValueMap queryParams = builder.encode().build().getQueryParams(); - assertThat(queryParams).hasSize(5); + assertThat(queryParams).hasSize(6); assertThat(queryParams.getFirst("test-single")).isEqualTo("test-value-1"); - assertThat(queryParams.getFirst("test-collection")).isEqualTo("test-value-2,test-value-3"); + assertThat(queryParams.getFirst("test-collection")).isEqualTo("test-value-2%2Ctest-value-3"); assertThat(queryParams.getFirst("test-subclass")).isEqualTo("test-value-4"); assertThat(queryParams.getFirst("test-override")).isEqualTo("test-value-7"); + assertThat(queryParams.getFirst("test-reserved-characters")).isEqualTo("%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D"); } public static abstract class StubFilterParams { @@ -71,6 +76,11 @@ public final String getSingle() { return "test-value-1"; } + @FilterParameter("test-reserved-characters") + public final String getReservedCharacters() { + return ":/?#[]@!$&'()*+,;="; + } + @FilterParameter("test-override") abstract String getOverride(); diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/servicebindings/ReactorServiceBindingsV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/servicebindings/ReactorServiceBindingsV3Test.java index 70c4e810299..1760fb6b20e 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/servicebindings/ReactorServiceBindingsV3Test.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/servicebindings/ReactorServiceBindingsV3Test.java @@ -166,7 +166,7 @@ public void get() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/service_bindings?app_guids=test-application-id&order_by=+created_at&page=1") + .method(GET).path("/service_bindings?app_guids=test-application-id&order_by=%2Bcreated_at&page=1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizationsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizationsTest.java index f87b56ce3f4..46a4c192bc5 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizationsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizationsTest.java @@ -48,7 +48,7 @@ public final class ReactorAuthorizationsTest extends AbstractUaaApiTest { public void authorizeByAuthorizationCodeGrantApi() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/authorize?client_id=login&redirect_uri=https://uaa.cloudfoundry.com/redirect/cf&state=v4LpFF&response_type=code") + .method(GET).path("/oauth/authorize?client_id=login&redirect_uri=https%3A%2F%2Fuaa.cloudfoundry.com%2Fredirect%2Fcf&state=v4LpFF&response_type=code") .build()) .response(TestResponse.builder() .status(FOUND) @@ -72,7 +72,7 @@ public void authorizeByAuthorizationCodeGrantApi() { public void authorizeByAuthorizationCodeGrantBrowser() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/authorize?client_id=login&redirect_uri=https://uaa.cloudfoundry.com/redirect/cf&scope=openid%20oauth.approvals&response_type=code") + .method(GET).path("/oauth/authorize?client_id=login&redirect_uri=https%3A%2F%2Fuaa.cloudfoundry.com%2Fredirect%2Fcf&scope=openid%20oauth.approvals&response_type=code") .build()) .response(TestResponse.builder() .status(FOUND) @@ -97,7 +97,7 @@ public void authorizeByAuthorizationCodeGrantBrowser() { public void authorizeByAuthorizationCodeGrantHybrid() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http://localhost:8080/app/&scope=openid&response_type=code%20id_token") + .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fapp%2F&scope=openid&response_type=code%20id_token") .build()) .response(TestResponse.builder() .status(FOUND) @@ -133,7 +133,7 @@ public void authorizeByAuthorizationCodeGrantHybrid() { public void authorizeByImplicitGrantBrowser() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http://localhost:8080/app/&scope=openid&response_type=token") + .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fapp%2F&scope=openid&response_type=token") .build()) .response(TestResponse.builder() .status(FOUND) @@ -163,7 +163,7 @@ public void authorizeByImplicitGrantBrowser() { public void authorizeByOpenIdWithAuthorizationCodeGrant() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http://localhost:8080/app/&scope=openid&response_type=code%20id_token") + .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fapp%2F&scope=openid&response_type=code%20id_token") .build()) .response(TestResponse.builder() .status(FOUND) @@ -187,7 +187,7 @@ public void authorizeByOpenIdWithAuthorizationCodeGrant() { public void authorizeByOpenIdWithToken() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http://localhost:8080/app/&scope=openid&response_type=id_token") + .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fapp%2F&scope=openid&response_type=id_token") .build()) .response(TestResponse.builder() .status(FOUND) @@ -224,7 +224,7 @@ public void authorizeByOpenIdWithToken() { public void authorizeByOpenIdWithimplicitGrant() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http://localhost:8080/app/&scope=openid&response_type=token%20id_token") + .method(GET).path("/oauth/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fapp%2F&scope=openid&response_type=token%20id_token") .build()) .response(TestResponse.builder() .status(FOUND) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java index 7aec14efb4e..cccd853c252 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java @@ -554,7 +554,7 @@ public void getMetadata() { public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/oauth/clients?count=10&filter=client_id+eq+%22EGgNW3%22&sortBy=client_id&sortOrder=descending&startIndex=1") + .method(GET).path("/oauth/clients?count=10&filter=client_id%2Beq%2B%22EGgNW3%22&sortBy=client_id&sortOrder=descending&startIndex=1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/groups/ReactorGroupsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/groups/ReactorGroupsTest.java index 713ceae6a04..690ef32368b 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/groups/ReactorGroupsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/groups/ReactorGroupsTest.java @@ -261,7 +261,7 @@ public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(GET) - .path("/Groups?count=50&filter=id+eq+%22f87c557a-8ddc-43d3-98fb-e420ebc7f0f1%22+or+displayName+eq+%22Cooler%20Group%20Name%20for%20List%22" + + .path("/Groups?count=50&filter=id%2Beq%2B%22f87c557a-8ddc-43d3-98fb-e420ebc7f0f1%22%2Bor%2BdisplayName%2Beq%2B%22Cooler%20Group%20Name%20for%20List%22" + "&sortBy=email&sortOrder=ascending&startIndex=1") .build()) .response(TestResponse.builder() @@ -312,7 +312,7 @@ public void listExternalGroupMappings() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(GET) - .path("/Groups/External?count=50&filter=group_id+eq+%220480db7f-d1bc-4d2b-b723-febc684c0ee9%22&startIndex=1") + .path("/Groups/External?count=50&filter=group_id%2Beq%2B%220480db7f-d1bc-4d2b-b723-febc684c0ee9%22&startIndex=1") .build()) .response(TestResponse.builder() .status(OK) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokensTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokensTest.java index 532781ae0aa..4c292a168b1 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokensTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokensTest.java @@ -60,7 +60,7 @@ public final class ReactorTokensTest extends AbstractUaaApiTest { public void check() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(POST).path("/check_token?scopes=password.write,scim.userids&token=f9f2f98d88e04ff7bb1f69041d3c0346") + .method(POST).path("/check_token?scopes=password.write%2Cscim.userids&token=f9f2f98d88e04ff7bb1f69041d3c0346") .header("Authorization", "Basic YXBwOmFwcGNsaWVudHNlY3JldA==") .build()) .response(TestResponse.builder() @@ -146,7 +146,7 @@ public void getKey() { public void getTokenByAuthorizationCode() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(POST).path("/oauth/token?code=zI6Z1X&client_id=login&client_secret=loginsecret&redirect_uri=https://uaa.cloudfoundry.com/redirect/cf" + + .method(POST).path("/oauth/token?code=zI6Z1X&client_id=login&client_secret=loginsecret&redirect_uri=https%3A%2F%2Fuaa.cloudfoundry.com%2Fredirect%2Fcf" + "&token_format=opaque&grant_type=authorization_code&response_type=token") .header("Authorization", null) .header("Content-Type", "application/x-www-form-urlencoded") @@ -248,7 +248,7 @@ public void getTokenByOneTimePasscode() { public void getTokenByOpenId() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(POST).path("/oauth/token?code=NAlA1d&client_id=app&client_secret=appclientsecret&redirect_uri=https://uaa.cloudfoundry.com/redirect/cf&token_format=opaque" + + .method(POST).path("/oauth/token?code=NAlA1d&client_id=app&client_secret=appclientsecret&redirect_uri=https%3A%2F%2Fuaa.cloudfoundry.com%2Fredirect%2Fcf&token_format=opaque" + "&grant_type=authorization_code&response_type=id_token") .header("Authorization", null) .header("Content-Type", "application/x-www-form-urlencoded") @@ -286,7 +286,7 @@ public void getTokenByPassword() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(POST).path("/oauth/token?client_id=app&client_secret=appclientsecret&password=secr3T&token_format=opaque&" + - "username=jENeJj@test.org&grant_type=password&response_type=token") + "username=jENeJj%40test.org&grant_type=password&response_type=token") .header("Authorization", null) .header("Content-Type", "application/x-www-form-urlencoded") .build()) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java index afc5238960b..7706923b577 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java @@ -389,7 +389,7 @@ public void expirePassword() { public void getVerificationLink() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/Users/1faa46a0-0c6f-4e13-8334-d1f6e5f2e1dd/verify-link?redirect_uri=http://redirect.to/app") + .method(GET).path("/Users/1faa46a0-0c6f-4e13-8334-d1f6e5f2e1dd/verify-link?redirect_uri=http%3A%2F%2Fredirect.to%2Fapp") .build()) .response(TestResponse.builder() .status(OK) @@ -455,7 +455,7 @@ public void list() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(GET).path( - "/Users?count=50&filter=id+eq+%22a94534d5-de08-41eb-8712-a51314e6a484%22+or+email+eq+%22Da63pG@test.org%22&sortBy=email&sortOrder=ascending&startIndex=1") + "/Users?count=50&filter=id%2Beq%2B%22a94534d5-de08-41eb-8712-a51314e6a484%22%2Bor%2Bemail%2Beq%2B%22Da63pG%40test.org%22&sortBy=email&sortOrder=ascending&startIndex=1") .build()) .response(TestResponse.builder() .status(OK) @@ -584,7 +584,7 @@ public void lookup() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() .method(GET).path( - "/ids/Users?count=10&filter=userName+eq+%22bobOu38vE@test.org%22+or+id+eq+%22c1476587-5ec9-4b7e-9ed2-381e3133f07a%22" + + "/ids/Users?count=10&filter=userName%2Beq%2B%22bobOu38vE%40test.org%22%2Bor%2Bid%2Beq%2B%22c1476587-5ec9-4b7e-9ed2-381e3133f07a%22" + "&includeInactive=true&sortOrder=descending&startIndex=1") .build()) .response(TestResponse.builder() diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java index 032972c2088..c66648fae7f 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java @@ -18,12 +18,13 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.operations.spaces.CreateSpaceRequest; +import org.cloudfoundry.operations.spaces.GetSpaceRequest; +import org.cloudfoundry.operations.spaces.SpaceDetail; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.test.StepVerifier; import java.time.Duration; -import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; @@ -53,6 +54,26 @@ public void create() { .verify(Duration.ofMinutes(5)); } + @Test + public void getWithURLReservedCharacterInName() { + String spaceName = this.nameFactory.getSpaceName() + "+test"; + + this.cloudFoundryOperations.spaces() + .create(CreateSpaceRequest.builder() + .name(spaceName) + .organization(this.organizationName) + .build()) + .then(this.cloudFoundryOperations.spaces() + .get(GetSpaceRequest.builder() + .name(spaceName) + .build())) + .map(SpaceDetail::getName) + .as(StepVerifier::create) + .expectNext(spaceName) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @Test public void list() { this.cloudFoundryOperations.spaces()