Browse Source

Reduce API surface, expose interfaces only.

See #3270
Original pull request: #3271
pull/3304/head
Mark Paluch 7 months ago
parent
commit
a8c779c26b
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 48
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java
  2. 49
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryClassBuilder.java
  3. 71
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java
  4. 124
      src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java
  5. 9
      src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java
  6. 9
      src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java
  7. 7
      src/test/java/org/springframework/data/repository/aot/generate/MethodCapturingRepositoryContributor.java
  8. 6
      src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java

48
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java

@ -22,6 +22,7 @@ import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
import javax.lang.model.element.Modifier; import javax.lang.model.element.Modifier;
@ -43,6 +44,7 @@ import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec; import org.springframework.javapoet.TypeSpec;
import org.springframework.util.Assert;
/** /**
* Builder for AOT repository fragments. * Builder for AOT repository fragments.
@ -59,9 +61,9 @@ class AotRepositoryBuilder {
private final ProjectionFactory projectionFactory; private final ProjectionFactory projectionFactory;
private final AotRepositoryFragmentMetadata generationMetadata; private final AotRepositoryFragmentMetadata generationMetadata;
private @Nullable ConstructorCustomizer constructorCustomizer; private @Nullable Consumer<AotRepositoryConstructorBuilder> constructorCustomizer;
private @Nullable MethodContributorFactory methodContributorFactory; private @Nullable MethodContributorFactory methodContributorFactory;
private ClassCustomizer customizer; private Consumer<AotRepositoryClassBuilder> classCustomizer;
private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName, private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName,
ProjectionFactory projectionFactory) { ProjectionFactory projectionFactory) {
@ -76,7 +78,7 @@ class AotRepositoryBuilder {
.initializer("$T.getLog($T.class)", TypeName.get(LogFactory.class), this.generationMetadata.getTargetTypeName()) .initializer("$T.getLog($T.class)", TypeName.get(LogFactory.class), this.generationMetadata.getTargetTypeName())
.build()); .build());
this.customizer = (info, builder) -> {}; this.classCustomizer = (builder) -> {};
} }
/** /**
@ -93,14 +95,14 @@ class AotRepositoryBuilder {
} }
/** /**
* Configure a {@link ClassCustomizer} customizer. * Configure a {@link AotRepositoryConstructorBuilder} customizer.
* *
* @param classCustomizer must not be {@literal null}. * @param classCustomizer must not be {@literal null}.
* @return {@code this}. * @return {@code this}.
*/ */
public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) { public AotRepositoryBuilder withClassCustomizer(Consumer<AotRepositoryClassBuilder> classCustomizer) {
this.customizer = classCustomizer; this.classCustomizer = classCustomizer;
return this; return this;
} }
@ -110,7 +112,8 @@ class AotRepositoryBuilder {
* @param constructorCustomizer must not be {@literal null}. * @param constructorCustomizer must not be {@literal null}.
* @return {@code this}. * @return {@code this}.
*/ */
public AotRepositoryBuilder withConstructorCustomizer(ConstructorCustomizer constructorCustomizer) { public AotRepositoryBuilder withConstructorCustomizer(
Consumer<AotRepositoryConstructorBuilder> constructorCustomizer) {
this.constructorCustomizer = constructorCustomizer; this.constructorCustomizer = constructorCustomizer;
return this; return this;
@ -162,7 +165,12 @@ class AotRepositoryBuilder {
generationMetadata.getFields().values().forEach(builder::addField); generationMetadata.getFields().values().forEach(builder::addField);
// finally customize the file itself // finally customize the file itself
this.customizer.customize(repositoryInformation, builder); this.classCustomizer.accept(customizer -> {
Assert.notNull(customizer, "ClassCustomizer must not be null");
customizer.customize(builder);
});
JavaFile javaFile = JavaFile.builder(packageName(), builder.build()).build(); JavaFile javaFile = JavaFile.builder(packageName(), builder.build()).build();
AotRepositoryMetadata metadata = getAotRepositoryMetadata(methodMetadata); AotRepositoryMetadata metadata = getAotRepositoryMetadata(methodMetadata);
@ -171,11 +179,11 @@ class AotRepositoryBuilder {
private MethodSpec buildConstructor() { private MethodSpec buildConstructor() {
AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation, RepositoryConstructorBuilder constructorBuilder = new RepositoryConstructorBuilder(
generationMetadata); generationMetadata);
if (constructorCustomizer != null) { if (constructorCustomizer != null) {
constructorCustomizer.customize(constructorBuilder); constructorCustomizer.accept(constructorBuilder);
} }
return constructorBuilder.buildConstructor(); return constructorBuilder.buildConstructor();
@ -213,8 +221,7 @@ class AotRepositoryBuilder {
if (repositoryInformation.isQueryMethod(method) && methodContributorFactory != null) { if (repositoryInformation.isQueryMethod(method) && methodContributorFactory != null) {
MethodContributor<? extends QueryMethod> contributor = methodContributorFactory.create(method, MethodContributor<? extends QueryMethod> contributor = methodContributorFactory.create(method);
repositoryInformation);
if (contributor != null) { if (contributor != null) {
@ -273,20 +280,6 @@ class AotRepositoryBuilder {
return projectionFactory; return projectionFactory;
} }
/**
* Customizer interface to customize the AOT repository fragment class after it has been defined.
*/
public interface ClassCustomizer {
/**
* Apply customization ot the AOT repository fragment class after it has been defined.
*
* @param information the repository information that is used for the AOT fragment.
* @param builder the class builder to be customized.
*/
void customize(RepositoryInformation information, TypeSpec.Builder builder);
}
/** /**
* Customizer interface to customize the AOT repository fragment constructor through * Customizer interface to customize the AOT repository fragment constructor through
@ -313,12 +306,11 @@ class AotRepositoryBuilder {
* Apply customization ot the AOT repository fragment constructor. * Apply customization ot the AOT repository fragment constructor.
* *
* @param method the method to be contributed. * @param method the method to be contributed.
* @param information repository information.
* @return the {@link MethodContributor} to be used. Can be {@literal null} if the method and method metadata should * @return the {@link MethodContributor} to be used. Can be {@literal null} if the method and method metadata should
* not be contributed. * not be contributed.
*/ */
@Nullable @Nullable
MethodContributor<? extends QueryMethod> create(Method method, RepositoryInformation information); MethodContributor<? extends QueryMethod> create(Method method);
} }

49
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryClassBuilder.java

@ -0,0 +1,49 @@
/*
* Copyright 2025 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
*
* https://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.springframework.data.repository.aot.generate;
import org.springframework.javapoet.TypeSpec;
/**
* Builder for AOT repository fragment classes.
*
* @author Mark Paluch
* @since 4.0
*/
public interface AotRepositoryClassBuilder {
/**
* Add a class customizer. Customizer is invoked after building the class.
*
* @param customizer the customizer with direct access to the {@link TypeSpec.Builder type builder}.
*/
void customize(ClassCustomizer customizer);
/**
* Customizer interface to customize the AOT repository fragment class after it has been defined.
*/
interface ClassCustomizer {
/**
* Apply customization ot the AOT repository fragment class after it has been defined.
*
* @param builder the class builder to be customized.
*/
void customize(TypeSpec.Builder builder);
}
}

71
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java

@ -15,15 +15,7 @@
*/ */
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import java.util.Map.Entry;
import javax.lang.model.element.Modifier;
import org.springframework.core.ResolvableType;
import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata.ConstructorArgument;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterizedTypeName;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
/** /**
@ -33,18 +25,7 @@ import org.springframework.javapoet.TypeName;
* @author Mark Paluch * @author Mark Paluch
* @since 4.0 * @since 4.0
*/ */
public class AotRepositoryConstructorBuilder { public interface AotRepositoryConstructorBuilder {
private final RepositoryInformation repositoryInformation;
private final AotRepositoryFragmentMetadata metadata;
private ConstructorCustomizer customizer = (info, builder) -> {};
AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformation, AotRepositoryFragmentMetadata metadata) {
this.repositoryInformation = repositoryInformation;
this.metadata = metadata;
}
/** /**
* Add constructor parameter and create a field storing its value. * Add constructor parameter and create a field storing its value.
@ -52,16 +33,7 @@ public class AotRepositoryConstructorBuilder {
* @param parameterName name of the parameter. * @param parameterName name of the parameter.
* @param type parameter type. * @param type parameter type.
*/ */
public void addParameter(String parameterName, Class<?> type) { void addParameter(String parameterName, Class<?> type);
ResolvableType resolvableType = ResolvableType.forClass(type);
if (!resolvableType.hasGenerics() || !resolvableType.hasResolvableGenerics()) {
addParameter(parameterName, TypeName.get(type));
return;
}
addParameter(parameterName, ParameterizedTypeName.get(type, resolvableType.resolveGenerics()));
}
/** /**
* Add constructor parameter and create a field storing its value. * Add constructor parameter and create a field storing its value.
@ -69,7 +41,7 @@ public class AotRepositoryConstructorBuilder {
* @param parameterName name of the parameter. * @param parameterName name of the parameter.
* @param type parameter type. * @param type parameter type.
*/ */
public void addParameter(String parameterName, TypeName type) { default void addParameter(String parameterName, TypeName type) {
addParameter(parameterName, type, true); addParameter(parameterName, type, true);
} }
@ -80,14 +52,7 @@ public class AotRepositoryConstructorBuilder {
* @param type parameter type. * @param type parameter type.
* @param createField whether to create a field for the parameter and assign its value to the field. * @param createField whether to create a field for the parameter and assign its value to the field.
*/ */
public void addParameter(String parameterName, TypeName type, boolean createField) { void addParameter(String parameterName, TypeName type, boolean createField);
this.metadata.addConstructorArgument(parameterName, type, createField ? parameterName : null);
if (createField) {
this.metadata.addField(parameterName, type, Modifier.PRIVATE, Modifier.FINAL);
}
}
/** /**
* Add constructor customizer. Customizer is invoked after adding constructor arguments and before assigning * Add constructor customizer. Customizer is invoked after adding constructor arguments and before assigning
@ -95,41 +60,19 @@ public class AotRepositoryConstructorBuilder {
* *
* @param customizer the customizer with direct access to the {@link MethodSpec.Builder constructor builder}. * @param customizer the customizer with direct access to the {@link MethodSpec.Builder constructor builder}.
*/ */
public void customize(ConstructorCustomizer customizer) { void customize(ConstructorCustomizer customizer);
this.customizer = customizer;
}
MethodSpec buildConstructor() {
MethodSpec.Builder builder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);
for (Entry<String, ConstructorArgument> parameter : this.metadata.getConstructorArguments().entrySet()) {
builder.addParameter(parameter.getValue().typeName(), parameter.getKey());
}
customizer.customize(repositoryInformation, builder);
for (Entry<String, ConstructorArgument> parameter : this.metadata.getConstructorArguments().entrySet()) {
if (parameter.getValue().isForLocalField()) {
builder.addStatement("this.$N = $N", parameter.getKey(), parameter.getKey());
}
}
return builder.build();
}
/** /**
* Customizer for the AOT repository constructor. * Customizer for the AOT repository constructor.
*/ */
public interface ConstructorCustomizer { interface ConstructorCustomizer {
/** /**
* Customize the constructor. * Customize the constructor.
* *
* @param information the repository information that is used for the AOT fragment.
* @param builder the constructor builder to be customized. * @param builder the constructor builder to be customized.
*/ */
void customize(RepositoryInformation information, MethodSpec.Builder builder); void customize(MethodSpec.Builder builder);
} }

124
src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java

@ -0,0 +1,124 @@
/*
* Copyright 2024 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
*
* https://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.springframework.data.repository.aot.generate;
import java.util.Map.Entry;
import javax.lang.model.element.Modifier;
import org.springframework.core.ResolvableType;
import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata.ConstructorArgument;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterizedTypeName;
import org.springframework.javapoet.TypeName;
import org.springframework.util.Assert;
/**
* Builder for AOT Repository Constructors.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.0
*/
class RepositoryConstructorBuilder implements AotRepositoryConstructorBuilder {
private final AotRepositoryFragmentMetadata metadata;
private ConstructorCustomizer customizer = (builder) -> {};
RepositoryConstructorBuilder(AotRepositoryFragmentMetadata metadata) {
this.metadata = metadata;
}
/**
* Add constructor parameter and create a field storing its value.
*
* @param parameterName name of the parameter.
* @param type parameter type.
*/
@Override
public void addParameter(String parameterName, Class<?> type) {
ResolvableType resolvableType = ResolvableType.forClass(type);
if (!resolvableType.hasGenerics() || !resolvableType.hasResolvableGenerics()) {
addParameter(parameterName, TypeName.get(type));
return;
}
addParameter(parameterName, ParameterizedTypeName.get(type, resolvableType.resolveGenerics()));
}
/**
* Add constructor parameter and create a field storing its value.
*
* @param parameterName name of the parameter.
* @param type parameter type.
*/
@Override
public void addParameter(String parameterName, TypeName type) {
addParameter(parameterName, type, true);
}
/**
* Add constructor parameter.
*
* @param parameterName name of the parameter.
* @param type parameter type.
* @param createField whether to create a field for the parameter and assign its value to the field.
*/
@Override
public void addParameter(String parameterName, TypeName type, boolean createField) {
this.metadata.addConstructorArgument(parameterName, type, createField ? parameterName : null);
if (createField) {
this.metadata.addField(parameterName, type, Modifier.PRIVATE, Modifier.FINAL);
}
}
/**
* Add constructor customizer. Customizer is invoked after adding constructor arguments and before assigning
* constructor arguments to fields.
*
* @param customizer the customizer with direct access to the {@link MethodSpec.Builder constructor builder}.
*/
@Override
public void customize(ConstructorCustomizer customizer) {
Assert.notNull(customizer, "ConstructorCustomizer must not be null");
this.customizer = customizer;
}
public MethodSpec buildConstructor() {
MethodSpec.Builder builder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);
for (Entry<String, ConstructorArgument> parameter : this.metadata.getConstructorArguments().entrySet()) {
builder.addParameter(parameter.getValue().typeName(), parameter.getKey());
}
customizer.customize(builder);
for (Entry<String, ConstructorArgument> parameter : this.metadata.getConstructorArguments().entrySet()) {
if (parameter.getValue().isForLocalField()) {
builder.addStatement("this.$N = $N", parameter.getKey(), parameter.getKey());
}
}
return builder.build();
}
}

9
src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java

@ -31,6 +31,7 @@ import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
import org.springframework.util.StringUtils;
import org.springframework.javapoet.TypeSpec; import org.springframework.javapoet.TypeSpec;
/** /**
@ -136,23 +137,21 @@ public class RepositoryContributor {
/** /**
* Customization hook for store implementations to customize class after building the entire class. * Customization hook for store implementations to customize class after building the entire class.
*/ */
protected void customizeClass(RepositoryInformation information, protected void customizeClass(AotRepositoryClassBuilder builder) {
TypeSpec.Builder builder) {
} }
/** /**
* Customization hook for store implementations to customize the fragment constructor after building the constructor. * Customization hook for store implementations to customize the fragment constructor after building the constructor.
*/ */
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { protected void customizeConstructor(AotRepositoryConstructorBuilder builder) {
} }
/** /**
* Customization hook for store implementations to contribute a query method. * Customization hook for store implementations to contribute a query method.
*/ */
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method, protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method) {
RepositoryInformation repositoryInformation) {
return null; return null;
} }

9
src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java

@ -96,7 +96,7 @@ class AotRepositoryBuilderUnitTests {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory()); new SpelAwareProxyProjectionFactory());
repoBuilder.withConstructorCustomizer(ctor -> { repoBuilder.withConstructorCustomizer(ctor -> {
ctor.customize((info, code) -> { ctor.customize((code) -> {
code.addStatement("throw new $T($S)", IllegalStateException.class, "initialization error"); code.addStatement("throw new $T($S)", IllegalStateException.class, "initialization error");
}); });
}); });
@ -110,7 +110,9 @@ class AotRepositoryBuilderUnitTests {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory()); new SpelAwareProxyProjectionFactory());
repoBuilder.withClassCustomizer((info, metadata, clazz) -> { repoBuilder.withClassCustomizer((builder) -> {
builder.customize(clazz -> {
clazz.addField(Float.class, "f", Modifier.PRIVATE, Modifier.STATIC); clazz.addField(Float.class, "f", Modifier.PRIVATE, Modifier.STATIC);
clazz.addField(Double.class, "d", Modifier.PUBLIC); clazz.addField(Double.class, "d", Modifier.PUBLIC);
@ -119,6 +121,7 @@ class AotRepositoryBuilderUnitTests {
clazz.addAnnotation(Repository.class); clazz.addAnnotation(Repository.class);
clazz.addMethod(MethodSpec.methodBuilder("oops").build()); clazz.addMethod(MethodSpec.methodBuilder("oops").build());
});
}); });
assertThat(repoBuilder.build().javaFile().toString()) // assertThat(repoBuilder.build().javaFile().toString()) //
@ -138,7 +141,7 @@ class AotRepositoryBuilderUnitTests {
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons",
new SpelAwareProxyProjectionFactory()); new SpelAwareProxyProjectionFactory());
repoBuilder.withQueryMethodContributor((method, info) -> { repoBuilder.withQueryMethodContributor((method) -> {
return new MethodContributor<>(mock(QueryMethod.class), null) { return new MethodContributor<>(mock(QueryMethod.class), null) {

7
src/test/java/org/springframework/data/repository/aot/generate/MethodCapturingRepositoryContributor.java

@ -15,15 +15,15 @@
*/ */
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import org.assertj.core.api.MapAssert; import org.assertj.core.api.MapAssert;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethod;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -41,8 +41,7 @@ public class MethodCapturingRepositoryContributor extends RepositoryContributor
} }
@Override @Override
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method, protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method) {
RepositoryInformation repositoryInformation) {
capturedInvocations.add(method.getName(), method); capturedInvocations.add(method.getName(), method);
return null; return null;
} }

6
src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java

@ -58,10 +58,10 @@ class RepositoryContributorUnitTests {
RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) {
@Override @Override
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method, protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method) {
RepositoryInformation repositoryInformation) {
return MethodContributor.forQueryMethod(new QueryMethod(method, repositoryInformation, getProjectionFactory())) return MethodContributor
.forQueryMethod(new QueryMethod(method, getRepositoryInformation(), getProjectionFactory()))
.withMetadata(new QueryMetadata() { .withMetadata(new QueryMetadata() {
@Override @Override

Loading…
Cancel
Save