Skip to content

Commit 9f8311f

Browse files
authored
API review fixes (#2205)
- Return CompletableFuture from resolve() method - Add @deprecated to deprecated params - Move endpoints classes to endpoint-spi - Make providers implement marker interface - Move to the .endpoints package - Move the endpoints interceptors to be the first in the list - Add javadocs for the resolver interfaces - Declare mockito compile dependency for codegen plugin This is required in order to get Mockito on compile classpath. `codegen` needs to reference Mockito classes at generation time, and it's normally only on the testing classpath. - Unwrap exception if necessary
1 parent 0801f38 commit 9f8311f

File tree

57 files changed

+458
-199
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+458
-199
lines changed

codegen-maven-plugin/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
<artifactId>junit-jupiter</artifactId>
6363
<scope>compile</scope>
6464
</dependency>
65+
<dependency>
66+
<groupId>org.mockito</groupId>
67+
<artifactId>mockito-core</artifactId>
68+
<scope>compile</scope>
69+
</dependency>
6570
<dependency>
6671
<artifactId>maven-plugin-api</artifactId>
6772
<groupId>org.apache.maven</groupId>

codegen/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
<artifactId>metrics-spi</artifactId>
6363
<version>${awsjavasdk.version}</version>
6464
</dependency>
65+
<dependency>
66+
<groupId>software.amazon.awssdk</groupId>
67+
<artifactId>endpoints-spi</artifactId>
68+
<version>${awsjavasdk.version}</version>
69+
</dependency>
6570
<dependency>
6671
<groupId>software.amazon.awssdk</groupId>
6772
<artifactId>regions</artifactId>

codegen/src/main/java/software/amazon/awssdk/codegen/internal/Constant.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public final class Constant {
6868

6969
public static final String PACKAGE_NAME_WAITERS_PATTERN = "%s.waiters";
7070

71-
public static final String PACKAGE_NAME_RULES_PATTERN = "%s.rules";
71+
public static final String PACKAGE_NAME_RULES_PATTERN = "%s.endpoints";
7272

7373
public static final String PACKAGE_NAME_SMOKE_TEST_PATTERN = "%s.smoketests";
7474

codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,14 @@ private MethodSpec finalizeServiceConfigurationMethod() {
232232

233233
// Initialize configuration values
234234

235+
builder.addStatement("$T endpointInterceptors = new $T<>()",
236+
ParameterizedTypeName.get(List.class, ExecutionInterceptor.class),
237+
ArrayList.class);
238+
239+
builder.addStatement("endpointInterceptors.add(new $T())", endpointRulesSpecUtils.resolverInterceptorName());
240+
builder.addStatement("endpointInterceptors.add(new $T())", endpointRulesSpecUtils.authSchemesInterceptorName());
241+
builder.addStatement("endpointInterceptors.add(new $T())", endpointRulesSpecUtils.requestModifierInterceptorName());
242+
235243
builder.addCode("$1T interceptorFactory = new $1T();\n", ClasspathInterceptorChainFactory.class)
236244
.addCode("$T<$T> interceptors = interceptorFactory.getInterceptors($S);\n",
237245
List.class, ExecutionInterceptor.class, requestHandlerPath);
@@ -241,14 +249,12 @@ private MethodSpec finalizeServiceConfigurationMethod() {
241249
ExecutionInterceptor.class),
242250
ArrayList.class);
243251

244-
builder.addStatement("additionalInterceptors.add(new $T())", endpointRulesSpecUtils.resolverInterceptorName());
245-
builder.addStatement("additionalInterceptors.add(new $T())", endpointRulesSpecUtils.authSchemesInterceptorName());
246-
builder.addStatement("additionalInterceptors.add(new $T())", endpointRulesSpecUtils.requestModifierInterceptorName());
247-
248252
if (model.getMetadata().isQueryProtocol()) {
249253
builder.addStatement("additionalInterceptors.add(new $T())", QueryParametersToBodyInterceptor.class);
250254
}
251255

256+
builder.addStatement("interceptors = $T.mergeLists(endpointInterceptors, interceptors)",
257+
CollectionUtils.class);
252258
builder.addStatement("interceptors = $T.mergeLists(interceptors, additionalInterceptors)",
253259
CollectionUtils.class);
254260

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointAuthSchemeInterceptorClassSpec.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
import software.amazon.awssdk.auth.signer.AwsS3V4Signer;
2828
import software.amazon.awssdk.auth.signer.SignerLoader;
2929
import software.amazon.awssdk.awscore.AwsRequest;
30-
import software.amazon.awssdk.awscore.rules.AwsEndpointAttribute;
31-
import software.amazon.awssdk.awscore.rules.authscheme.EndpointAuthScheme;
30+
import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute;
31+
import software.amazon.awssdk.awscore.endpoints.authscheme.EndpointAuthScheme;
3232
import software.amazon.awssdk.awscore.util.SignerOverrideUtils;
3333
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
3434
import software.amazon.awssdk.codegen.poet.ClassSpec;
@@ -39,8 +39,8 @@
3939
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
4040
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
4141
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
42-
import software.amazon.awssdk.core.rules.model.Endpoint;
4342
import software.amazon.awssdk.core.signer.Signer;
43+
import software.amazon.awssdk.endpoints.Endpoint;
4444

4545
/**
4646
* Generates the Endpoint Interceptor responsible for applying the {@link AwsEndpointAttribute#AUTH_SCHEMES} property on the

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParametersClassSpec.java

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -129,30 +129,28 @@ private FieldSpec fieldSpec(String name, ParameterModel model) {
129129
}
130130

131131
private MethodSpec setterMethodDeclaration(String name, ParameterModel model) {
132-
return MethodSpec.methodBuilder(endpointRulesSpecUtils.paramMethodName(name))
133-
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
134-
.addParameter(parameterSpec(name, model))
135-
.returns(builderInterfaceName())
136-
.build();
132+
MethodSpec.Builder b = paramMethodBuilder(name, model);
133+
b.addModifiers(Modifier.ABSTRACT);
134+
b.addParameter(parameterSpec(name, model));
135+
b.returns(builderInterfaceName());
136+
return b.build();
137137
}
138138

139139
private MethodSpec accessorMethod(String name, ParameterModel model) {
140-
return MethodSpec.methodBuilder(endpointRulesSpecUtils.paramMethodName(name))
141-
.returns(endpointRulesSpecUtils.parameterType(model))
142-
.addModifiers(Modifier.PUBLIC)
143-
.addStatement("return $N", variableName(name))
144-
.build();
140+
MethodSpec.Builder b = paramMethodBuilder(name, model);
141+
b.returns(endpointRulesSpecUtils.parameterType(model));
142+
b.addStatement("return $N", variableName(name));
143+
return b.build();
145144
}
146145

147146
private MethodSpec builderSetterMethod(String name, ParameterModel model) {
148147
String memberName = variableName(name);
149148

150-
MethodSpec.Builder b = MethodSpec.methodBuilder(endpointRulesSpecUtils.paramMethodName(name))
151-
.addParameter(parameterSpec(name, model))
152-
.addAnnotation(Override.class)
153-
.addModifiers(Modifier.PUBLIC)
154-
.returns(builderInterfaceName())
155-
.addStatement("this.$1N = $1N", memberName);
149+
MethodSpec.Builder b = paramMethodBuilder(name, model)
150+
.addAnnotation(Override.class)
151+
.addParameter(parameterSpec(name, model))
152+
.returns(builderInterfaceName())
153+
.addStatement("this.$1N = $1N", memberName);
156154

157155
TreeNode defaultValue = model.getDefault();
158156
if (defaultValue != null) {
@@ -217,4 +215,13 @@ private CodeBlock defaultValueCode(ParameterModel parameterModel) {
217215
}
218216
return b.build();
219217
}
218+
219+
private MethodSpec.Builder paramMethodBuilder(String name, ParameterModel model) {
220+
MethodSpec.Builder b = MethodSpec.methodBuilder(endpointRulesSpecUtils.paramMethodName(name));
221+
b.addModifiers(Modifier.PUBLIC);
222+
if (model.getDeprecated() != null) {
223+
b.addAnnotation(Deprecated.class);
224+
}
225+
return b;
226+
}
220227
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderInterfaceSpec.java

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,67 @@
1616
package software.amazon.awssdk.codegen.poet.rules;
1717

1818
import com.squareup.javapoet.ClassName;
19+
import com.squareup.javapoet.CodeBlock;
1920
import com.squareup.javapoet.MethodSpec;
21+
import com.squareup.javapoet.ParameterizedTypeName;
22+
import com.squareup.javapoet.TypeName;
2023
import com.squareup.javapoet.TypeSpec;
24+
import java.util.function.Consumer;
2125
import javax.lang.model.element.Modifier;
2226
import software.amazon.awssdk.annotations.SdkPublicApi;
2327
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
2428
import software.amazon.awssdk.codegen.poet.ClassSpec;
25-
import software.amazon.awssdk.core.rules.model.Endpoint;
29+
import software.amazon.awssdk.codegen.poet.PoetUtils;
30+
import software.amazon.awssdk.endpoints.Endpoint;
31+
import software.amazon.awssdk.endpoints.EndpointProvider;
2632

2733
public class EndpointProviderInterfaceSpec implements ClassSpec {
34+
private final IntermediateModel intermediateModel;
2835
private final EndpointRulesSpecUtils endpointRulesSpecUtils;
2936

3037
public EndpointProviderInterfaceSpec(IntermediateModel intermediateModel) {
38+
this.intermediateModel = intermediateModel;
3139
this.endpointRulesSpecUtils = new EndpointRulesSpecUtils(intermediateModel);
3240
}
3341

3442
@Override
3543
public TypeSpec poetSpec() {
36-
return TypeSpec.interfaceBuilder(className())
37-
.addModifiers(Modifier.PUBLIC)
38-
.addAnnotation(SdkPublicApi.class)
39-
.addMethod(resolveEndpointMethod())
40-
.addMethod(defaultProviderMethod())
41-
.build();
44+
return PoetUtils.createInterfaceBuilder(className())
45+
.addSuperinterface(EndpointProvider.class)
46+
.addModifiers(Modifier.PUBLIC)
47+
.addAnnotation(SdkPublicApi.class)
48+
.addJavadoc(interfaceJavadoc())
49+
.addMethod(resolveEndpointMethod())
50+
.addMethod(resolveEndpointConsumerBuilderMethod())
51+
.addMethod(defaultProviderMethod())
52+
.build();
4253
}
4354

4455
private MethodSpec resolveEndpointMethod() {
45-
return MethodSpec.methodBuilder("resolveEndpoint")
46-
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
47-
.addParameter(endpointRulesSpecUtils.parametersClassName(), "endpointParams")
48-
.returns(Endpoint.class)
49-
.build();
56+
MethodSpec.Builder b = MethodSpec.methodBuilder("resolveEndpoint");
57+
b.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
58+
b.addParameter(endpointRulesSpecUtils.parametersClassName(), "endpointParams");
59+
b.returns(endpointRulesSpecUtils.resolverReturnType());
60+
b.addJavadoc(resolveMethodJavadoc());
61+
return b.build();
62+
}
63+
64+
private MethodSpec resolveEndpointConsumerBuilderMethod() {
65+
ClassName parametersClass = endpointRulesSpecUtils.parametersClassName();
66+
ClassName parametersBuilderClass = parametersClass.nestedClass("Builder");
67+
TypeName consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), parametersBuilderClass);
68+
69+
MethodSpec.Builder b = MethodSpec.methodBuilder("resolveEndpoint");
70+
b.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT);
71+
b.addParameter(consumerType, "endpointParamsConsumer");
72+
b.returns(endpointRulesSpecUtils.resolverReturnType());
73+
b.addJavadoc(resolveMethodJavadoc());
74+
75+
b.addStatement("$T paramsBuilder = $T.builder()", parametersBuilderClass, parametersClass);
76+
b.addStatement("endpointParamsConsumer.accept(paramsBuilder)");
77+
b.addStatement("return resolveEndpoint(paramsBuilder.build())");
78+
79+
return b.build();
5080
}
5181

5282
private MethodSpec defaultProviderMethod() {
@@ -61,4 +91,24 @@ private MethodSpec defaultProviderMethod() {
6191
public ClassName className() {
6292
return endpointRulesSpecUtils.providerInterfaceName();
6393
}
94+
95+
private CodeBlock interfaceJavadoc() {
96+
CodeBlock.Builder b = CodeBlock.builder();
97+
98+
b.add("An endpoint provider for $N. The endpoint provider takes a set of parameters using {@link $T}, and resolves an "
99+
+ "{@link $T} base on the given parameters.",
100+
intermediateModel.getMetadata().getServiceName(),
101+
endpointRulesSpecUtils.parametersClassName(),
102+
Endpoint.class);
103+
104+
return b.build();
105+
}
106+
107+
private CodeBlock resolveMethodJavadoc() {
108+
CodeBlock.Builder b = CodeBlock.builder();
109+
110+
b.add("Compute the endpoint based on the given set of parameters.");
111+
112+
return b.build();
113+
}
64114
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderSpec.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.squareup.javapoet.TypeSpec;
2424
import java.util.HashMap;
2525
import java.util.Map;
26+
import java.util.concurrent.CompletableFuture;
2627
import javax.lang.model.element.Modifier;
2728
import software.amazon.awssdk.annotations.SdkInternalApi;
2829
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
@@ -31,7 +32,7 @@
3132
import software.amazon.awssdk.codegen.model.rules.endpoints.ParameterModel;
3233
import software.amazon.awssdk.codegen.poet.ClassSpec;
3334
import software.amazon.awssdk.codegen.poet.PoetUtils;
34-
import software.amazon.awssdk.core.rules.model.Endpoint;
35+
import software.amazon.awssdk.utils.CompletableFutureUtils;
3536

3637
public class EndpointProviderSpec implements ClassSpec {
3738
private static final String RULE_SET_FIELD_NAME = "ENDPOINT_RULE_SET";
@@ -127,7 +128,7 @@ private MethodSpec resolveEndpointMethod() {
127128

128129
MethodSpec.Builder b = MethodSpec.methodBuilder("resolveEndpoint")
129130
.addModifiers(Modifier.PUBLIC)
130-
.returns(Endpoint.class)
131+
.returns(endpointRulesSpecUtils.resolverReturnType())
131132
.addAnnotation(Override.class)
132133
.addParameter(endpointRulesSpecUtils.parametersClassName(), paramsName);
133134

@@ -137,9 +138,15 @@ private MethodSpec resolveEndpointMethod() {
137138
RULE_SET_FIELD_NAME,
138139
paramsName);
139140

140-
b.addStatement("return $T.valueAsEndpointOrThrow($N)",
141+
b.beginControlFlow("try");
142+
b.addStatement("return $T.completedFuture($T.valueAsEndpointOrThrow($N))",
143+
CompletableFuture.class,
141144
endpointRulesSpecUtils.rulesRuntimeClassName("AwsEndpointProviderUtils"),
142145
"res");
146+
b.endControlFlow();
147+
b.beginControlFlow("catch ($T error)", Exception.class);
148+
b.addStatement("return $T.failedFuture(error)", CompletableFutureUtils.class);
149+
b.endControlFlow();
143150

144151
return b.build();
145152
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderTestSpec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ private CodeBlock createTestCase(EndpointTestModel test) {
134134
}
135135
});
136136
}
137-
b.add("return $N.resolveEndpoint(builder.build());", PROVIDER_NAME);
137+
b.add("return $N.resolveEndpoint(builder.build()).join();", PROVIDER_NAME);
138138
b.endControlFlow();
139139
return b.build();
140140
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.squareup.javapoet.TypeSpec;
2424
import java.util.Map;
2525
import java.util.Optional;
26+
import java.util.concurrent.CompletionException;
2627
import javax.lang.model.element.Modifier;
2728
import software.amazon.awssdk.annotations.SdkInternalApi;
2829
import software.amazon.awssdk.awscore.AwsExecutionAttribute;
@@ -36,11 +37,12 @@
3637
import software.amazon.awssdk.codegen.poet.PoetExtension;
3738
import software.amazon.awssdk.codegen.poet.PoetUtils;
3839
import software.amazon.awssdk.core.SdkRequest;
40+
import software.amazon.awssdk.core.exception.SdkClientException;
3941
import software.amazon.awssdk.core.interceptor.Context;
4042
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
4143
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
4244
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
43-
import software.amazon.awssdk.core.rules.model.Endpoint;
45+
import software.amazon.awssdk.endpoints.Endpoint;
4446
import software.amazon.awssdk.utils.AttributeMap;
4547

4648
public class EndpointResolverInterceptorSpec implements ClassSpec {
@@ -101,9 +103,21 @@ private MethodSpec modifyRequestMethod() {
101103

102104
b.addStatement("$1T $2N = ($1T) executionAttributes.getAttribute($3T.ENDPOINT_PROVIDER)",
103105
endpointRulesSpecUtils.providerInterfaceName(), providerVar, SdkInternalExecutionAttribute.class);
104-
b.addStatement("$T result = $N.resolveEndpoint(ruleParams(context, executionAttributes))", Endpoint.class, providerVar);
106+
b.beginControlFlow("try");
107+
b.addStatement("$T result = $N.resolveEndpoint(ruleParams(context, executionAttributes)).join()", Endpoint.class,
108+
providerVar);
105109
b.addStatement("executionAttributes.putAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT, result)");
106110
b.addStatement("return context.request()");
111+
b.endControlFlow();
112+
b.beginControlFlow("catch ($T e)", CompletionException.class);
113+
b.addStatement("$T cause = e.getCause()", Throwable.class);
114+
b.beginControlFlow("if (cause instanceof $T)", SdkClientException.class);
115+
b.addStatement("throw ($T) cause", SdkClientException.class);
116+
b.endControlFlow();
117+
b.beginControlFlow("else");
118+
b.addStatement("throw $T.create($S, cause)", SdkClientException.class, "Endpoint resolution failed");
119+
b.endControlFlow();
120+
b.endControlFlow();
107121
return b.build();
108122
}
109123

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesSpecUtils.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
import com.fasterxml.jackson.jr.stree.JrsString;
2121
import com.squareup.javapoet.ClassName;
2222
import com.squareup.javapoet.CodeBlock;
23+
import com.squareup.javapoet.ParameterizedTypeName;
2324
import com.squareup.javapoet.TypeName;
2425
import java.util.Locale;
26+
import java.util.concurrent.CompletableFuture;
2527
import software.amazon.awssdk.codegen.internal.Utils;
2628
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
2729
import software.amazon.awssdk.codegen.model.intermediate.Metadata;
2830
import software.amazon.awssdk.codegen.model.rules.endpoints.BuiltInParameter;
2931
import software.amazon.awssdk.codegen.model.rules.endpoints.ParameterModel;
32+
import software.amazon.awssdk.endpoints.Endpoint;
3033
import software.amazon.awssdk.regions.Region;
3134
import software.amazon.awssdk.utils.Validate;
3235
import software.amazon.awssdk.utils.internal.CodegenNamingUtils;
@@ -195,4 +198,8 @@ public boolean isS3() {
195198
public boolean isS3Control() {
196199
return "S3Control".equals(intermediateModel.getMetadata().getServiceName());
197200
}
201+
202+
public TypeName resolverReturnType() {
203+
return ParameterizedTypeName.get(CompletableFuture.class, Endpoint.class);
204+
}
198205
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/RequestEndpointInterceptorSpec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
2929
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
3030
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
31-
import software.amazon.awssdk.core.rules.model.Endpoint;
31+
import software.amazon.awssdk.endpoints.Endpoint;
3232
import software.amazon.awssdk.http.SdkHttpRequest;
3333

3434
public class RequestEndpointInterceptorSpec implements ClassSpec {

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/TestGeneratorUtils.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
import java.util.Iterator;
2727
import java.util.List;
2828
import java.util.Map;
29-
import software.amazon.awssdk.awscore.rules.AwsEndpointAttribute;
30-
import software.amazon.awssdk.awscore.rules.authscheme.SigV4AuthScheme;
31-
import software.amazon.awssdk.awscore.rules.authscheme.SigV4aAuthScheme;
29+
import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute;
30+
import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme;
31+
import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme;
3232
import software.amazon.awssdk.codegen.model.rules.endpoints.ExpectModel;
33-
import software.amazon.awssdk.core.rules.model.Endpoint;
3433
import software.amazon.awssdk.core.rules.testing.model.Expect;
34+
import software.amazon.awssdk.endpoints.Endpoint;
3535

3636
public final class TestGeneratorUtils {
3737
private TestGeneratorUtils() {

0 commit comments

Comments
 (0)