Browse Source

Add support for repository JSON metadata.

See #3265
pull/3304/head
Mark Paluch 9 months ago
parent
commit
9c37a13c25
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 42
      src/main/java/org/springframework/data/repository/aot/generate/AotFragmentTarget.java
  2. 114
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java
  3. 54
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMetadata.java
  4. 44
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethod.java
  5. 97
      src/main/java/org/springframework/data/repository/aot/generate/MethodContributor.java
  6. 40
      src/main/java/org/springframework/data/repository/aot/generate/QueryMetadata.java
  7. 39
      src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java
  8. 121
      src/main/java/org/springframework/data/repository/aot/generate/json/JSON.java
  9. 696
      src/main/java/org/springframework/data/repository/aot/generate/json/JSONArray.java
  10. 52
      src/main/java/org/springframework/data/repository/aot/generate/json/JSONException.java
  11. 836
      src/main/java/org/springframework/data/repository/aot/generate/json/JSONObject.java
  12. 421
      src/main/java/org/springframework/data/repository/aot/generate/json/JSONStringer.java
  13. 539
      src/main/java/org/springframework/data/repository/aot/generate/json/JSONTokener.java
  14. 5
      src/main/java/org/springframework/data/repository/aot/generate/json/package-info.java
  15. 5
      src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java
  16. 12
      src/main/java/org/springframework/data/repository/core/RepositoryInformation.java
  17. 5
      src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java
  18. 29
      src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java
  19. 8
      src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java
  20. 6
      src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java
  21. 6
      src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java

42
src/main/java/org/springframework/data/repository/aot/generate/AotFragmentTarget.java

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
/*
* 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.jspecify.annotations.Nullable;
import org.springframework.data.repository.aot.generate.json.JSONException;
import org.springframework.data.repository.aot.generate.json.JSONObject;
/**
* @author Mark Paluch
* @since 4.0
*/
record AotFragmentTarget(String signature, @Nullable String implementation) {
public JSONObject toJson() throws JSONException {
JSONObject fragment = new JSONObject();
if (implementation() != null) {
fragment.put("interface", signature());
fragment.put("fragment", implementation());
} else {
fragment.put("fragment", signature());
}
return fragment;
}
}

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

@ -16,8 +16,10 @@ @@ -16,8 +16,10 @@
package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@ -26,11 +28,16 @@ import javax.lang.model.element.Modifier; @@ -26,11 +28,16 @@ import javax.lang.model.element.Modifier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.Generated;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.aot.generate.json.JSONException;
import org.springframework.data.repository.aot.generate.json.JSONObject;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec;
@ -50,8 +57,8 @@ class AotRepositoryBuilder { @@ -50,8 +57,8 @@ class AotRepositoryBuilder {
private final ProjectionFactory projectionFactory;
private final AotRepositoryFragmentMetadata generationMetadata;
private Consumer<AotRepositoryConstructorBuilder> constructorCustomizer;
private BiFunction<Method, RepositoryInformation, MethodContributor<? extends QueryMethod>> methodContributorFunction;
private @Nullable Consumer<AotRepositoryConstructorBuilder> constructorCustomizer;
private @Nullable BiFunction<Method, RepositoryInformation, MethodContributor<? extends QueryMethod>> methodContributorFunction;
private ClassCustomizer customizer;
private AotRepositoryBuilder(RepositoryInformation repositoryInformation, ProjectionFactory projectionFactory) {
@ -92,7 +99,7 @@ class AotRepositoryBuilder { @@ -92,7 +99,7 @@ class AotRepositoryBuilder {
return this;
}
public JavaFile build() {
public AotBundle build() {
// start creating the type
TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) //
@ -104,51 +111,91 @@ class AotRepositoryBuilder { @@ -104,51 +111,91 @@ class AotRepositoryBuilder {
// create the constructor
AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation,
generationMetadata);
constructorCustomizer.accept(constructorBuilder);
if (constructorCustomizer != null) {
constructorCustomizer.accept(constructorBuilder);
}
builder.addMethod(constructorBuilder.buildConstructor());
List<AotRepositoryMethod> methodMetadata = new ArrayList<>();
AotRepositoryMetadata.RepositoryType repositoryType = repositoryInformation.isReactiveRepository()
? AotRepositoryMetadata.RepositoryType.REACTIVE
: AotRepositoryMetadata.RepositoryType.IMPERATIVE;
RepositoryComposition repositoryComposition = repositoryInformation.getRepositoryComposition();
Arrays.stream(repositoryInformation.getRepositoryInterface().getMethods())
.sorted(Comparator.<Method, String> comparing(it -> {
return it.getDeclaringClass().getName();
}).thenComparing(Method::getName).thenComparing(Method::getParameterCount).thenComparing(Method::toString))
.forEach(method -> {
contributeMethod(method, repositoryComposition, methodMetadata, builder);
});
if (repositoryInformation.isCustomMethod(method)) {
// TODO: fragment
return;
}
// write fields at the end so we make sure to capture things added by methods
generationMetadata.getFields().values().forEach(builder::addField);
if (repositoryInformation.isBaseClassMethod(method)) {
// TODO: base
return;
}
// finally customize the file itself
this.customizer.customize(repositoryInformation, generationMetadata, builder);
JavaFile javaFile = JavaFile.builder(packageName(), builder.build()).build();
if (method.isBridge() || method.isDefault() || java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
// TODO: report what we've skipped
return;
}
// TODO: module identifier
AotRepositoryMetadata metadata = new AotRepositoryMetadata(repositoryInformation.getRepositoryInterface().getName(),
"", repositoryType, methodMetadata);
if (repositoryInformation.isQueryMethod(method)) {
try {
return new AotBundle(javaFile, metadata.toJson());
} catch (JSONException e) {
throw new IllegalStateException(e);
}
}
MethodContributor<? extends QueryMethod> contributor = methodContributorFunction.apply(method,
repositoryInformation);
private void contributeMethod(Method method, RepositoryComposition repositoryComposition,
List<AotRepositoryMethod> methodMetadata, TypeSpec.Builder builder) {
if (contributor != null) {
if (repositoryInformation.isCustomMethod(method) || repositoryInformation.isBaseClassMethod(method)) {
AotQueryMethodGenerationContext context = new AotQueryMethodGenerationContext(repositoryInformation,
method, contributor.getQueryMethod(), generationMetadata);
RepositoryFragment<?> fragment = repositoryComposition.findFragment(method);
builder.addMethod(contributor.contribute(context));
}
}
});
if (fragment != null) {
methodMetadata.add(getFragmentMetadata(method, fragment));
}
return;
}
// write fields at the end so we make sure to capture things added by methods
generationMetadata.getFields().values().forEach(builder::addField);
if (method.isBridge() || method.isDefault() || java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
return;
}
// finally customize the file itself
this.customizer.customize(repositoryInformation, generationMetadata, builder);
return JavaFile.builder(packageName(), builder.build()).build();
if (repositoryInformation.isQueryMethod(method) && methodContributorFunction != null) {
MethodContributor<? extends QueryMethod> contributor = methodContributorFunction.apply(method,
repositoryInformation);
if (contributor != null) {
if (contributor.contributesMethodSpec() && !repositoryInformation.isReactiveRepository()) {
AotQueryMethodGenerationContext context = new AotQueryMethodGenerationContext(repositoryInformation, method,
contributor.getQueryMethod(), generationMetadata);
builder.addMethod(contributor.contribute(context));
}
methodMetadata
.add(new AotRepositoryMethod(method.getName(), method.toGenericString(), contributor.getMetadata(), null));
}
}
}
private AotRepositoryMethod getFragmentMetadata(Method method, RepositoryFragment<?> fragment) {
String signature = fragment.getSignatureContributor().getName();
String implementation = fragment.getImplementation().map(it -> it.getClass().getName()).orElse(null);
AotFragmentTarget fragmentTarget = new AotFragmentTarget(signature, implementation);
return new AotRepositoryMethod(method.getName(), method.toGenericString(), null, fragmentTarget);
}
public AotRepositoryFragmentMetadata getGenerationMetadata() {
@ -193,5 +240,10 @@ class AotRepositoryBuilder { @@ -193,5 +240,10 @@ class AotRepositoryBuilder {
*/
void customize(RepositoryInformation information, AotRepositoryFragmentMetadata metadata,
TypeSpec.Builder builder);
}
record AotBundle(JavaFile javaFile, JSONObject metadata) {
}
}

54
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMetadata.java

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
/*
* 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 java.util.List;
import org.springframework.data.repository.aot.generate.json.JSONArray;
import org.springframework.data.repository.aot.generate.json.JSONException;
import org.springframework.data.repository.aot.generate.json.JSONObject;
/**
* @author Mark Paluch
* @since 4.0
*/
record AotRepositoryMetadata(String name, String moduleName,
org.springframework.data.repository.aot.generate.AotRepositoryMetadata.RepositoryType type,
List<AotRepositoryMethod> methods) {
enum RepositoryType {
IMPERATIVE, REACTIVE
}
JSONObject toJson() throws JSONException {
JSONObject metadata = new JSONObject();
metadata.put("name", name());
metadata.put("moduleName", moduleName());
metadata.put("type", type().name());
JSONArray methods = new JSONArray();
for (AotRepositoryMethod method : methods()) {
methods.put(method.toJson());
}
metadata.put("methods", methods);
return metadata;
}
}

44
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethod.java

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
/*
* 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.jspecify.annotations.Nullable;
import org.springframework.data.repository.aot.generate.json.JSONException;
import org.springframework.data.repository.aot.generate.json.JSONObject;
/**
* @author Mark Paluch
* @since 4.0
*/
record AotRepositoryMethod(String name, String signature, @Nullable QueryMetadata query,
@Nullable AotFragmentTarget fragment) {
public JSONObject toJson() throws JSONException {
JSONObject method = new JSONObject();
method.put("name", name());
method.put("signature", signature());
if (query() != null) {
method.put("query", query().toJson());
} else if (fragment() != null) {
method.put("fragment", fragment().toJson());
}
return method;
}
}

97
src/main/java/org/springframework/data/repository/aot/generate/MethodContributor.java

@ -17,6 +17,9 @@ package org.springframework.data.repository.aot.generate; @@ -17,6 +17,9 @@ package org.springframework.data.repository.aot.generate;
import java.util.function.Consumer;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.MethodSpec;
@ -30,9 +33,11 @@ import org.springframework.javapoet.MethodSpec; @@ -30,9 +33,11 @@ import org.springframework.javapoet.MethodSpec;
public abstract class MethodContributor<M extends QueryMethod> {
private final M queryMethod;
private final QueryMetadata metadata;
private MethodContributor(M queryMethod) {
private MethodContributor(M queryMethod, QueryMetadata metadata) {
this.queryMethod = queryMethod;
this.metadata = metadata;
}
/**
@ -42,15 +47,18 @@ public abstract class MethodContributor<M extends QueryMethod> { @@ -42,15 +47,18 @@ public abstract class MethodContributor<M extends QueryMethod> {
* @return the new builder.
* @param <M> query method type.
*/
public static <M extends QueryMethod> QueryMethodContributorBuilder<M> forQueryMethod(M queryMethod) {
public static <M extends QueryMethod> QueryMethodMetadataContributorBuilder<M> forQueryMethod(M queryMethod) {
return new QueryMethodMetadataContributorBuilder<M>() {
return builderConsumer -> new MethodContributor<>(queryMethod) {
@Override
public MethodContributor<M> metadataOnly(QueryMetadata metadata) {
return new MetadataMethodContributor<>(queryMethod, metadata);
}
@Override
public MethodSpec contribute(AotQueryMethodGenerationContext context) {
AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(context);
builderConsumer.accept(builder);
return builder.buildMethod();
public QueryMethodContributorBuilder<M> withMetadata(QueryMetadata metadata) {
return builderConsumer -> new AotMethodContributor<>(queryMethod, metadata, builderConsumer);
}
};
}
@ -59,13 +67,86 @@ public abstract class MethodContributor<M extends QueryMethod> { @@ -59,13 +67,86 @@ public abstract class MethodContributor<M extends QueryMethod> {
return queryMethod;
}
public QueryMetadata getMetadata() {
return metadata;
}
/**
* @return whether {@code MethodContributor} can contribute a {@link MethodSpec} implementing the actual query method.
*/
public boolean contributesMethodSpec() {
return false;
}
/**
* Contribute the actual method specification to be added to the repository fragment.
*
* @param context generation context.
* @return
*/
public abstract MethodSpec contribute(AotQueryMethodGenerationContext context);
public abstract @Nullable MethodSpec contribute(AotQueryMethodGenerationContext context);
private static class MetadataMethodContributor<M extends QueryMethod> extends MethodContributor<M> {
private MetadataMethodContributor(M queryMethod, QueryMetadata metadata) {
super(queryMethod, metadata);
}
@Override
public @Nullable MethodSpec contribute(AotQueryMethodGenerationContext context) {
return null;
}
}
private static class AotMethodContributor<M extends QueryMethod> extends MethodContributor<M> {
private final Consumer<AotRepositoryMethodBuilder> builderConsumer;
private AotMethodContributor(M queryMethod, QueryMetadata metadata,
Consumer<AotRepositoryMethodBuilder> builderConsumer) {
super(queryMethod, metadata);
this.builderConsumer = builderConsumer;
}
@Override
public boolean contributesMethodSpec() {
return true;
}
@Override
public @NonNull MethodSpec contribute(AotQueryMethodGenerationContext context) {
AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(context);
builderConsumer.accept(builder);
return builder.buildMethod();
}
}
/**
* Initial builder for a query method contributor. This builder allows returning a {@link MethodContributor} using
* metadata-only (i.e. no code contribution) or a {@link QueryMethodContributorBuilder} accepting code contributions.
*
* @param <M> query method type.
*/
public interface QueryMethodMetadataContributorBuilder<M extends QueryMethod> {
/**
* Terminal method accepting {@link QueryMetadata} to build the {@link MethodContributor}.
*
* @param metadata the query metadata describing queries used by the query method.
* @return the method contributor to use.
*/
MethodContributor<M> metadataOnly(QueryMetadata metadata);
/**
* Builder method accepting {@link QueryMetadata} to enrich query method metadata.
*
* @param metadata the query metadata describing queries used by the query method.
* @return the method contributor builder.
*/
QueryMethodContributorBuilder<M> withMetadata(QueryMetadata metadata);
}
/**
* Builder for a query method contributor.

40
src/main/java/org/springframework/data/repository/aot/generate/QueryMetadata.java

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
/*
* 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 java.util.Map;
import org.springframework.data.repository.aot.generate.json.JSONException;
import org.springframework.data.repository.aot.generate.json.JSONObject;
/**
* @author Mark Paluch
*/
public interface QueryMetadata {
Map<String, Object> serialize();
public default JSONObject toJson() throws JSONException {
JSONObject query = new JSONObject();
for (Map.Entry<String, Object> entry : serialize().entrySet()) {
query.put(entry.getKey(), entry.getValue());
}
return query;
}
}

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

@ -26,12 +26,14 @@ import org.springframework.aot.hint.MemberCategory; @@ -26,12 +26,14 @@ import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.TypeReference;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.aot.generate.json.JSONException;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec;
import org.springframework.util.StringUtils;
/**
* Contributor for AOT repository fragments.
@ -78,25 +80,52 @@ public class RepositoryContributor { @@ -78,25 +80,52 @@ public class RepositoryContributor {
builder.withConstructorCustomizer(this::customizeConstructor);
builder.withQueryMethodContributor(this::contributeQueryMethod);
JavaFile file = builder.build();
String typeName = "%s.%s".formatted(file.packageName, file.typeSpec.name);
AotRepositoryBuilder.AotBundle aotBundle = builder.build();
Class<?> repositoryInterface = getRepositoryInformation().getRepositoryInterface();
String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface);
JavaFile javaFile = aotBundle.javaFile();
String typeName = "%s.%s".formatted(javaFile.packageName, javaFile.typeSpec.name);
String repositoryJson;
try {
repositoryJson = aotBundle.metadata().toString(2);
} catch (JSONException e) {
throw new RuntimeException(e);
}
if (logger.isTraceEnabled()) {
logger.trace("""
------ AOT Repository.json: %s ------
%s
-------------------
""".formatted(repositoryJsonFileName, repositoryJson));
logger.trace("""
------ AOT Generated Repository: %s ------
%s
-------------------
""".formatted(typeName, file));
""".formatted(typeName, javaFile));
}
// generate the file itself
generationContext.getGeneratedFiles().addSourceFile(file);
// generate the files
generationContext.getGeneratedFiles().addSourceFile(javaFile);
generationContext.getGeneratedFiles().addResourceFile(repositoryJsonFileName, repositoryJson);
// generate native runtime hints - needed cause we're using the repository proxy
generationContext.getRuntimeHints().reflection().registerType(TypeReference.of(typeName),
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
}
private static String getRepositoryJsonFileName(Class<?> repositoryInterface) {
String repositoryJsonName = repositoryInterface.getSimpleName() + ".json";
String repositoryJsonPath = repositoryInterface.getPackageName().replace('.', '/');
return StringUtils.hasText(repositoryJsonPath) ? repositoryJsonPath + "/" + repositoryJsonName : repositoryJsonName;
}
/**
* Customization hook for store implementations to customize class after building the entire class.
*/

121
src/main/java/org/springframework/data/repository/aot/generate/json/JSON.java

@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
/*
* 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.json;
class JSON {
static double checkDouble(double d) throws JSONException {
if (Double.isInfinite(d) || Double.isNaN(d)) {
throw new JSONException("Forbidden numeric value: " + d);
}
return d;
}
static Boolean toBoolean(Object value) {
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof String stringValue) {
if ("true".equalsIgnoreCase(stringValue)) {
return true;
}
if ("false".equalsIgnoreCase(stringValue)) {
return false;
}
}
return null;
}
static Double toDouble(Object value) {
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
if (value instanceof String) {
try {
return Double.valueOf((String) value);
} catch (NumberFormatException ex) {
// Ignore
}
}
return null;
}
static Integer toInteger(Object value) {
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
if (value instanceof String) {
try {
return (int) Double.parseDouble((String) value);
} catch (NumberFormatException ex) {
// Ignore
}
}
return null;
}
static Long toLong(Object value) {
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
if (value instanceof String) {
try {
return (long) Double.parseDouble((String) value);
} catch (NumberFormatException ex) {
// Ignore
}
}
return null;
}
static String toString(Object value) {
if (value instanceof String) {
return (String) value;
}
if (value != null) {
return String.valueOf(value);
}
return null;
}
public static JSONException typeMismatch(Object indexOrName, Object actual, String requiredType)
throws JSONException {
if (actual == null) {
throw new JSONException("Value at " + indexOrName + " is null.");
}
throw new JSONException("Value " + actual + " at " + indexOrName + " of type " + actual.getClass().getName()
+ " cannot be converted to " + requiredType);
}
public static JSONException typeMismatch(Object actual, String requiredType) throws JSONException {
if (actual == null) {
throw new JSONException("Value is null.");
}
throw new JSONException(
"Value " + actual + " of type " + actual.getClass().getName() + " cannot be converted to " + requiredType);
}
}

696
src/main/java/org/springframework/data/repository/aot/generate/json/JSONArray.java

@ -0,0 +1,696 @@ @@ -0,0 +1,696 @@
/*
* 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.json;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
// Note: this class was written without inspecting the non-free org.json source code.
/**
* A dense indexed sequence of values. Values may be any mix of {@link JSONObject JSONObjects}, other {@link JSONArray
* JSONArrays}, Strings, Booleans, Integers, Longs, Doubles, {@code null} or {@link JSONObject#NULL}. Values may not be
* {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities}, or of any type not listed here.
* <p>
* {@code JSONArray} has the same type coercion behavior and optional/mandatory accessors as {@link JSONObject}. See
* that class' documentation for details.
* <p>
* <strong>Warning:</strong> this class represents null in two incompatible ways: the standard Java {@code null}
* reference, and the sentinel value {@link JSONObject#NULL}. In particular, {@code get} fails if the requested index
* holds the null reference, but succeeds if it holds {@code JSONObject.NULL}.
* <p>
* Instances of this class are not thread safe. Although this class is nonfinal, it was not designed for inheritance and
* should not be subclassed. In particular, self-use by overridable methods is not specified. See <i>Effective Java</i>
* Item 17, "Design and Document or inheritance or else prohibit it" for further information.
*/
public class JSONArray {
private final List<Object> values;
/**
* Creates a {@code JSONArray} with no values.
*/
public JSONArray() {
this.values = new ArrayList<>();
}
/**
* Creates a new {@code JSONArray} by copying all values from the given collection.
*
* @param copyFrom a collection whose values are of supported types. Unsupported values are not permitted and will
* yield an array in an inconsistent state.
*/
/* Accept a raw type for API compatibility */
@SuppressWarnings("rawtypes")
public JSONArray(Collection copyFrom) {
this();
if (copyFrom != null) {
for (Iterator it = copyFrom.iterator(); it.hasNext();) {
put(JSONObject.wrap(it.next()));
}
}
}
/**
* Creates a new {@code JSONArray} with values from the next array in the tokener.
*
* @param readFrom a tokener whose nextValue() method will yield a {@code JSONArray}.
* @throws JSONException if the parse fails or doesn't yield a {@code JSONArray}.
* @throws JSONException if processing of json failed
*/
public JSONArray(JSONTokener readFrom) throws JSONException {
/*
* Getting the parser to populate this could get tricky. Instead, just parse to
* temporary JSONArray and then steal the data from that.
*/
Object object = readFrom.nextValue();
if (object instanceof JSONArray) {
this.values = ((JSONArray) object).values;
} else {
throw JSON.typeMismatch(object, "JSONArray");
}
}
/**
* Creates a new {@code JSONArray} with values from the JSON string.
*
* @param json a JSON-encoded string containing an array.
* @throws JSONException if the parse fails or doesn't yield a {@code
* JSONArray}.
*/
public JSONArray(String json) throws JSONException {
this(new JSONTokener(json));
}
/**
* Creates a new {@code JSONArray} with values from the given primitive array.
*
* @param array a primitive array
* @throws JSONException if processing of json failed
*/
public JSONArray(Object array) throws JSONException {
if (!array.getClass().isArray()) {
throw new JSONException("Not a primitive array: " + array.getClass());
}
final int length = Array.getLength(array);
this.values = new ArrayList<>(length);
for (int i = 0; i < length; ++i) {
put(JSONObject.wrap(Array.get(array, i)));
}
}
/**
* Returns the number of values in this array.
*
* @return the length of this array
*/
public int length() {
return this.values.size();
}
/**
* Appends {@code value} to the end of this array.
*
* @param value the value
* @return this array.
*/
public JSONArray put(boolean value) {
this.values.add(value);
return this;
}
/**
* Appends {@code value} to the end of this array.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this array.
* @throws JSONException if processing of json failed
*/
public JSONArray put(double value) throws JSONException {
this.values.add(JSON.checkDouble(value));
return this;
}
/**
* Appends {@code value} to the end of this array.
*
* @param value the value
* @return this array.
*/
public JSONArray put(int value) {
this.values.add(value);
return this;
}
/**
* Appends {@code value} to the end of this array.
*
* @param value the value
* @return this array.
*/
public JSONArray put(long value) {
this.values.add(value);
return this;
}
/**
* Appends {@code value} to the end of this array.
*
* @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double,
* {@link JSONObject#NULL}, or {@code null}. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}. Unsupported values are not permitted and will cause the array to
* be in an inconsistent state.
* @return this array.
*/
public JSONArray put(Object value) {
this.values.add(value);
return this;
}
/**
* Sets the value at {@code index} to {@code value}, null padding this array to the required length if necessary. If a
* value already exists at {@code
* index}, it will be replaced.
*
* @param index the index to set the value to
* @param value the value
* @return this array.
* @throws JSONException if processing of json failed
*/
public JSONArray put(int index, boolean value) throws JSONException {
return put(index, (Boolean) value);
}
/**
* Sets the value at {@code index} to {@code value}, null padding this array to the required length if necessary. If a
* value already exists at {@code
* index}, it will be replaced.
*
* @param index the index to set the value to
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this array.
* @throws JSONException if processing of json failed
*/
public JSONArray put(int index, double value) throws JSONException {
return put(index, (Double) value);
}
/**
* Sets the value at {@code index} to {@code value}, null padding this array to the required length if necessary. If a
* value already exists at {@code
* index}, it will be replaced.
*
* @param index the index to set the value to
* @param value the value
* @return this array.
* @throws JSONException if processing of json failed
*/
public JSONArray put(int index, int value) throws JSONException {
return put(index, (Integer) value);
}
/**
* Sets the value at {@code index} to {@code value}, null padding this array to the required length if necessary. If a
* value already exists at {@code
* index}, it will be replaced.
*
* @param index the index to set the value to
* @param value the value
* @return this array.
* @throws JSONException if processing of json failed
*/
public JSONArray put(int index, long value) throws JSONException {
return put(index, (Long) value);
}
/**
* Sets the value at {@code index} to {@code value}, null padding this array to the required length if necessary. If a
* value already exists at {@code
* index}, it will be replaced.
*
* @param index the index to set the value to
* @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double,
* {@link JSONObject#NULL}, or {@code null}. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* @return this array.
* @throws JSONException if processing of json failed
*/
public JSONArray put(int index, Object value) throws JSONException {
if (value instanceof Number) {
// deviate from the original by checking all Numbers, not just floats &
// doubles
JSON.checkDouble(((Number) value).doubleValue());
}
while (this.values.size() <= index) {
this.values.add(null);
}
this.values.set(index, value);
return this;
}
/**
* Returns true if this array has no value at {@code index}, or if its value is the {@code null} reference or
* {@link JSONObject#NULL}.
*
* @param index the index to set the value to
* @return true if this array has no value at {@code index}
*/
public boolean isNull(int index) {
Object value = opt(index);
return value == null || value == JSONObject.NULL;
}
/**
* Returns the value at {@code index}.
*
* @param index the index to get the value from
* @return the value at {@code index}.
* @throws JSONException if this array has no value at {@code index}, or if that value is the {@code null} reference.
* This method returns normally if the value is {@code JSONObject#NULL}.
*/
public Object get(int index) throws JSONException {
try {
Object value = this.values.get(index);
if (value == null) {
throw new JSONException("Value at " + index + " is null.");
}
return value;
} catch (IndexOutOfBoundsException e) {
throw new JSONException("Index " + index + " out of range [0.." + this.values.size() + ")");
}
}
/**
* Returns the value at {@code index}, or null if the array has no value at {@code index}.
*
* @param index the index to get the value from
* @return the value at {@code index} or {@code null}
*/
public Object opt(int index) {
if (index < 0 || index >= this.values.size()) {
return null;
}
return this.values.get(index);
}
/**
* Removes and returns the value at {@code index}, or null if the array has no value at {@code index}.
*
* @param index the index of the value to remove
* @return the previous value at {@code index}
*/
public Object remove(int index) {
if (index < 0 || index >= this.values.size()) {
return null;
}
return this.values.remove(index);
}
/**
* Returns the value at {@code index} if it exists and is a boolean or can be coerced to a boolean.
*
* @param index the index to get the value from
* @return the value at {@code index}
* @throws JSONException if the value at {@code index} doesn't exist or cannot be coerced to a boolean.
*/
public boolean getBoolean(int index) throws JSONException {
Object object = get(index);
Boolean result = JSON.toBoolean(object);
if (result == null) {
throw JSON.typeMismatch(index, object, "boolean");
}
return result;
}
/**
* Returns the value at {@code index} if it exists and is a boolean or can be coerced to a boolean. Returns false
* otherwise.
*
* @param index the index to get the value from
* @return the {@code value} or {@code false}
*/
public boolean optBoolean(int index) {
return optBoolean(index, false);
}
/**
* Returns the value at {@code index} if it exists and is a boolean or can be coerced to a boolean. Returns
* {@code fallback} otherwise.
*
* @param index the index to get the value from
* @param fallback the fallback value
* @return the value at {@code index} of {@code fallback}
*/
public boolean optBoolean(int index, boolean fallback) {
Object object = opt(index);
Boolean result = JSON.toBoolean(object);
return result != null ? result : fallback;
}
/**
* Returns the value at {@code index} if it exists and is a double or can be coerced to a double.
*
* @param index the index to get the value from
* @return the {@code value}
* @throws JSONException if the value at {@code index} doesn't exist or cannot be coerced to a double.
*/
public double getDouble(int index) throws JSONException {
Object object = get(index);
Double result = JSON.toDouble(object);
if (result == null) {
throw JSON.typeMismatch(index, object, "double");
}
return result;
}
/**
* Returns the value at {@code index} if it exists and is a double or can be coerced to a double. Returns {@code NaN}
* otherwise.
*
* @param index the index to get the value from
* @return the {@code value} or {@code NaN}
*/
public double optDouble(int index) {
return optDouble(index, Double.NaN);
}
/**
* Returns the value at {@code index} if it exists and is a double or can be coerced to a double. Returns
* {@code fallback} otherwise.
*
* @param index the index to get the value from
* @param fallback the fallback value
* @return the value at {@code index} of {@code fallback}
*/
public double optDouble(int index, double fallback) {
Object object = opt(index);
Double result = JSON.toDouble(object);
return result != null ? result : fallback;
}
/**
* Returns the value at {@code index} if it exists and is an int or can be coerced to an int.
*
* @param index the index to get the value from
* @return the {@code value}
* @throws JSONException if the value at {@code index} doesn't exist or cannot be coerced to an int.
*/
public int getInt(int index) throws JSONException {
Object object = get(index);
Integer result = JSON.toInteger(object);
if (result == null) {
throw JSON.typeMismatch(index, object, "int");
}
return result;
}
/**
* Returns the value at {@code index} if it exists and is an int or can be coerced to an int. Returns 0 otherwise.
*
* @param index the index to get the value from
* @return the {@code value} or {@code 0}
*/
public int optInt(int index) {
return optInt(index, 0);
}
/**
* Returns the value at {@code index} if it exists and is an int or can be coerced to an int. Returns {@code fallback}
* otherwise.
*
* @param index the index to get the value from
* @param fallback the fallback value
* @return the value at {@code index} of {@code fallback}
*/
public int optInt(int index, int fallback) {
Object object = opt(index);
Integer result = JSON.toInteger(object);
return result != null ? result : fallback;
}
/**
* Returns the value at {@code index} if it exists and is a long or can be coerced to a long.
*
* @param index the index to get the value from
* @return the {@code value}
* @throws JSONException if the value at {@code index} doesn't exist or cannot be coerced to a long.
*/
public long getLong(int index) throws JSONException {
Object object = get(index);
Long result = JSON.toLong(object);
if (result == null) {
throw JSON.typeMismatch(index, object, "long");
}
return result;
}
/**
* Returns the value at {@code index} if it exists and is a long or can be coerced to a long. Returns 0 otherwise.
*
* @param index the index to get the value from
* @return the {@code value} or {@code 0}
*/
public long optLong(int index) {
return optLong(index, 0L);
}
/**
* Returns the value at {@code index} if it exists and is a long or can be coerced to a long. Returns {@code fallback}
* otherwise.
*
* @param index the index to get the value from
* @param fallback the fallback value
* @return the value at {@code index} of {@code fallback}
*/
public long optLong(int index, long fallback) {
Object object = opt(index);
Long result = JSON.toLong(object);
return result != null ? result : fallback;
}
/**
* Returns the value at {@code index} if it exists, coercing it if necessary.
*
* @param index the index to get the value from
* @return the {@code value}
* @throws JSONException if no such value exists.
*/
public String getString(int index) throws JSONException {
Object object = get(index);
String result = JSON.toString(object);
if (result == null) {
throw JSON.typeMismatch(index, object, "String");
}
return result;
}
/**
* Returns the value at {@code index} if it exists, coercing it if necessary. Returns the empty string if no such
* value exists.
*
* @param index the index to get the value from
* @return the {@code value} or an empty string
*/
public String optString(int index) {
return optString(index, "");
}
/**
* Returns the value at {@code index} if it exists, coercing it if necessary. Returns {@code fallback} if no such
* value exists.
*
* @param index the index to get the value from
* @param fallback the fallback value
* @return the value at {@code index} of {@code fallback}
*/
public String optString(int index, String fallback) {
Object object = opt(index);
String result = JSON.toString(object);
return result != null ? result : fallback;
}
/**
* Returns the value at {@code index} if it exists and is a {@code
* JSONArray}.
*
* @param index the index to get the value from
* @return the array at {@code index}
* @throws JSONException if the value doesn't exist or is not a {@code
* JSONArray}.
*/
public JSONArray getJSONArray(int index) throws JSONException {
Object object = get(index);
if (object instanceof JSONArray) {
return (JSONArray) object;
} else {
throw JSON.typeMismatch(index, object, "JSONArray");
}
}
/**
* Returns the value at {@code index} if it exists and is a {@code
* JSONArray}. Returns null otherwise.
*
* @param index the index to get the value from
* @return the array at {@code index} or {@code null}
*/
public JSONArray optJSONArray(int index) {
Object object = opt(index);
return object instanceof JSONArray ? (JSONArray) object : null;
}
/**
* Returns the value at {@code index} if it exists and is a {@code
* JSONObject}.
*
* @param index the index to get the value from
* @return the object at {@code index}
* @throws JSONException if the value doesn't exist or is not a {@code
* JSONObject}.
*/
public JSONObject getJSONObject(int index) throws JSONException {
Object object = get(index);
if (object instanceof JSONObject) {
return (JSONObject) object;
} else {
throw JSON.typeMismatch(index, object, "JSONObject");
}
}
/**
* Returns the value at {@code index} if it exists and is a {@code
* JSONObject}. Returns null otherwise.
*
* @param index the index to get the value from
* @return the object at {@code index} or {@code null}
*/
public JSONObject optJSONObject(int index) {
Object object = opt(index);
return object instanceof JSONObject ? (JSONObject) object : null;
}
/**
* Returns a new object whose values are the values in this array, and whose names are the values in {@code names}.
* Names and values are paired up by index from 0 through to the shorter array's length. Names that are not strings
* will be coerced to strings. This method returns null if either array is empty.
*
* @param names the property names
* @return a json object
* @throws JSONException if processing of json failed
*/
public JSONObject toJSONObject(JSONArray names) throws JSONException {
JSONObject result = new JSONObject();
int length = Math.min(names.length(), this.values.size());
if (length == 0) {
return null;
}
for (int i = 0; i < length; i++) {
String name = JSON.toString(names.opt(i));
result.put(name, opt(i));
}
return result;
}
/**
* Returns a new string by alternating this array's values with {@code
* separator}. This array's string values are quoted and have their special characters escaped. For example, the array
* containing the strings '12" pizza', 'taco' and 'soda' joined on '+' returns this:
*
* <pre>
* "12\" pizza" + "taco" + "soda"
* </pre>
*
* @param separator the separator to use
* @return the joined value
* @throws JSONException if processing of json failed
*/
public String join(String separator) throws JSONException {
JSONStringer stringer = new JSONStringer();
stringer.open(JSONStringer.Scope.NULL, "");
for (int i = 0, size = this.values.size(); i < size; i++) {
if (i > 0) {
stringer.out.append(separator);
}
stringer.value(this.values.get(i));
}
stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, "");
return stringer.out.toString();
}
/**
* Encodes this array as a compact JSON string, such as:
*
* <pre>
* [94043,90210]
* </pre>
*
* @return a compact JSON string representation of this array
*/
@Override
public String toString() {
try {
JSONStringer stringer = new JSONStringer();
writeTo(stringer);
return stringer.toString();
} catch (JSONException e) {
return null;
}
}
/**
* Encodes this array as a human-readable JSON string for debugging, such as:
*
* <pre>
* [
* 94043,
* 90210
* ]
* </pre>
*
* @param indentSpaces the number of spaces to indent for each level of nesting.
* @return a human-readable JSON string of this array
* @throws JSONException if processing of json failed
*/
public String toString(int indentSpaces) throws JSONException {
JSONStringer stringer = new JSONStringer(indentSpaces);
writeTo(stringer);
return stringer.toString();
}
void writeTo(JSONStringer stringer) throws JSONException {
stringer.array();
for (Object value : this.values) {
stringer.value(value);
}
stringer.endArray();
}
@Override
public boolean equals(Object o) {
return o instanceof JSONArray && ((JSONArray) o).values.equals(this.values);
}
@Override
public int hashCode() {
// diverge from the original, which doesn't implement hashCode
return this.values.hashCode();
}
}

52
src/main/java/org/springframework/data/repository/aot/generate/json/JSONException.java

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
/*
* 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.json;
// Note: this class was written without inspecting the non-free org.json source code.
/**
* Thrown to indicate a problem with the JSON API. Such problems include:
* <ul>
* <li>Attempts to parse or construct malformed documents
* <li>Use of null as a name
* <li>Use of numeric types not available to JSON, such as {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
* infinities}.
* <li>Lookups using an out of range index or nonexistent name
* <li>Type mismatches on lookups
* </ul>
* <p>
* Although this is a checked exception, it is rarely recoverable. Most callers should simply wrap this exception in an
* unchecked exception and rethrow:
*
* <pre class="code">
* public JSONArray toJSONObject() {
* try {
* JSONObject result = new JSONObject();
* ...
* } catch (JSONException e) {
* throw new RuntimeException(e);
* }
* }
* </pre>
*/
public class JSONException extends Exception {
public JSONException(String s) {
super(s);
}
}

836
src/main/java/org/springframework/data/repository/aot/generate/json/JSONObject.java

@ -0,0 +1,836 @@ @@ -0,0 +1,836 @@
/*
* 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.json;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
// Note: this class was written without inspecting the non-free org.json source code.
/**
* A modifiable set of name/value mappings. Names are unique, non-null strings. Values may be any mix of
* {@link JSONObject JSONObjects}, {@link JSONArray JSONArrays}, Strings, Booleans, Integers, Longs, Doubles or
* {@link #NULL}. Values may not be {@code null}, {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities},
* or of any type not listed here.
* <p>
* This class can coerce values to another type when requested.
* <ul>
* <li>When the requested type is a boolean, strings will be coerced using a case-insensitive comparison to "true" and
* "false".
* <li>When the requested type is a double, other {@link Number} types will be coerced using {@link Number#doubleValue()
* doubleValue}. Strings that can be coerced using {@link Double#valueOf(String)} will be.
* <li>When the requested type is an int, other {@link Number} types will be coerced using {@link Number#intValue()
* intValue}. Strings that can be coerced using {@link Double#valueOf(String)} will be, and then cast to int.
* <li><a id="lossy">When the requested type is a long, other {@link Number} types will be coerced using
* {@link Number#longValue() longValue}. Strings that can be coerced using {@link Double#valueOf(String)} will be, and
* then cast to long. This two-step conversion is lossy for very large values. For example, the string
* "9223372036854775806" yields the long 9223372036854775807.</a>
* <li>When the requested type is a String, other non-null values will be coerced using {@link String#valueOf(Object)}.
* Although null cannot be coerced, the sentinel value {@link JSONObject#NULL} is coerced to the string "null".
* </ul>
* <p>
* This class can look up both mandatory and optional values:
* <ul>
* <li>Use <code>get<i>Type</i>()</code> to retrieve a mandatory value. This fails with a {@code JSONException} if the
* requested name has no value or if the value cannot be coerced to the requested type.
* <li>Use <code>opt<i>Type</i>()</code> to retrieve an optional value. This returns a system- or user-supplied default
* if the requested name has no value or if the value cannot be coerced to the requested type.
* </ul>
* <p>
* <strong>Warning:</strong> this class represents null in two incompatible ways: the standard Java {@code null}
* reference, and the sentinel value {@link JSONObject#NULL}. In particular, calling {@code put(name, null)} removes the
* named entry from the object but {@code put(name, JSONObject.NULL)} stores an entry whose value is
* {@code JSONObject.NULL}.
* <p>
* Instances of this class are not thread safe. Although this class is nonfinal, it was not designed for inheritance and
* should not be subclassed. In particular, self-use by overrideable methods is not specified. See <i>Effective Java</i>
* Item 17, "Design and Document or inheritance or else prohibit it" for further information.
*/
public class JSONObject {
private static final Double NEGATIVE_ZERO = -0d;
/**
* A sentinel value used to explicitly define a name with no value. Unlike {@code null}, names with this value:
* <ul>
* <li>show up in the {@link #names} array
* <li>show up in the {@link #keys} iterator
* <li>return {@code true} for {@link #has(String)}
* <li>do not throw on {@link #get(String)}
* <li>are included in the encoded JSON string.
* </ul>
* <p>
* This value violates the general contract of {@link Object#equals} by returning true when compared to {@code null}.
* Its {@link #toString} method returns "null".
*/
public static final Object NULL = new Object() {
@Override
public boolean equals(Object o) {
return o == this || o == null; // API specifies this broken equals
// implementation
}
@Override
public String toString() {
return "null";
}
};
private final Map<String, Object> nameValuePairs;
/**
* Creates a {@code JSONObject} with no name/value mappings.
*/
public JSONObject() {
this.nameValuePairs = new LinkedHashMap<>();
}
/**
* Creates a new {@code JSONObject} by copying all name/value mappings from the given map.
*
* @param copyFrom a map whose keys are of type {@link String} and whose values are of supported types.
* @throws NullPointerException if any of the map's keys are null.
*/
/* (accept a raw type for API compatibility) */
@SuppressWarnings("rawtypes")
public JSONObject(Map copyFrom) {
this();
Map<?, ?> contentsTyped = copyFrom;
for (Map.Entry<?, ?> entry : contentsTyped.entrySet()) {
/*
* Deviate from the original by checking that keys are non-null and of the
* proper type. (We still defer validating the values).
*/
String key = (String) entry.getKey();
if (key == null) {
throw new NullPointerException("key == null");
}
this.nameValuePairs.put(key, wrap(entry.getValue()));
}
}
/**
* Creates a new {@code JSONObject} with name/value mappings from the next object in the tokener.
*
* @param readFrom a tokener whose nextValue() method will yield a {@code JSONObject}.
* @throws JSONException if the parse fails or doesn't yield a {@code JSONObject}.
*/
public JSONObject(JSONTokener readFrom) throws JSONException {
/*
* Getting the parser to populate this could get tricky. Instead, just parse to
* temporary JSONObject and then steal the data from that.
*/
Object object = readFrom.nextValue();
if (object instanceof JSONObject) {
this.nameValuePairs = ((JSONObject) object).nameValuePairs;
} else {
throw JSON.typeMismatch(object, "JSONObject");
}
}
/**
* Creates a new {@code JSONObject} with name/value mappings from the JSON string.
*
* @param json a JSON-encoded string containing an object.
* @throws JSONException if the parse fails or doesn't yield a {@code
* JSONObject}.
*/
public JSONObject(String json) throws JSONException {
this(new JSONTokener(json));
}
/**
* Creates a new {@code JSONObject} by copying mappings for the listed names from the given object. Names that aren't
* present in {@code copyFrom} will be skipped.
*
* @param copyFrom the source
* @param names the property names
* @throws JSONException if an error occurs
*/
public JSONObject(JSONObject copyFrom, String[] names) throws JSONException {
this();
for (String name : names) {
Object value = copyFrom.opt(name);
if (value != null) {
this.nameValuePairs.put(name, value);
}
}
}
/**
* Returns the number of name/value mappings in this object.
*
* @return the number of name/value mappings in this object
*/
public int length() {
return this.nameValuePairs.size();
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @param name the name of the property
* @param value the value of the property
* @return this object.
* @throws JSONException if an error occurs
*/
public JSONObject put(String name, boolean value) throws JSONException {
this.nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @param name the name of the property
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this object.
* @throws JSONException if an error occurs
*/
public JSONObject put(String name, double value) throws JSONException {
this.nameValuePairs.put(checkName(name), JSON.checkDouble(value));
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @param name the name of the property
* @param value the value of the property
* @return this object.
* @throws JSONException if an error occurs
*/
public JSONObject put(String name, int value) throws JSONException {
this.nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @param name the name of the property
* @param value the value of the property
* @return this object.
* @throws JSONException if an error occurs
*/
public JSONObject put(String name, long value) throws JSONException {
this.nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name. If the value is
* {@code null}, any existing mapping for {@code name} is removed.
*
* @param name the name of the property
* @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double, {@link #NULL}, or
* {@code null}. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this object.
* @throws JSONException if an error occurs
*/
public JSONObject put(String name, Object value) throws JSONException {
if (value == null) {
this.nameValuePairs.remove(name);
return this;
}
if (value instanceof Number) {
// deviate from the original by checking all Numbers, not just floats &
// doubles
JSON.checkDouble(((Number) value).doubleValue());
}
this.nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Equivalent to {@code put(name, value)} when both parameters are non-null; does nothing otherwise.
*
* @param name the name of the property
* @param value the value of the property
* @return this object.
* @throws JSONException if an error occurs
*/
public JSONObject putOpt(String name, Object value) throws JSONException {
if (name == null || value == null) {
return this;
}
return put(name, value);
}
/**
* Appends {@code value} to the array already mapped to {@code name}. If this object has no mapping for {@code name},
* this inserts a new mapping. If the mapping exists but its value is not an array, the existing and new values are
* inserted in order into a new array which is itself mapped to {@code name}. In aggregate, this allows values to be
* added to a mapping one at a time.
*
* @param name the name of the property
* @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double, {@link #NULL} or
* null. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this object.
* @throws JSONException if an error occurs
*/
public JSONObject accumulate(String name, Object value) throws JSONException {
Object current = this.nameValuePairs.get(checkName(name));
if (current == null) {
return put(name, value);
}
// check in accumulate, since array.put(Object) doesn't do any checking
if (value instanceof Number) {
JSON.checkDouble(((Number) value).doubleValue());
}
if (current instanceof JSONArray array) {
array.put(value);
} else {
JSONArray array = new JSONArray();
array.put(current);
array.put(value);
this.nameValuePairs.put(name, array);
}
return this;
}
String checkName(String name) throws JSONException {
if (name == null) {
throw new JSONException("Names must be non-null");
}
return name;
}
/**
* Removes the named mapping if it exists; does nothing otherwise.
*
* @param name the name of the property
* @return the value previously mapped by {@code name}, or null if there was no such mapping.
*/
public Object remove(String name) {
return this.nameValuePairs.remove(name);
}
/**
* Returns true if this object has no mapping for {@code name} or if it has a mapping whose value is {@link #NULL}.
*
* @param name the name of the property
* @return true if this object has no mapping for {@code name}
*/
public boolean isNull(String name) {
Object value = this.nameValuePairs.get(name);
return value == null || value == NULL;
}
/**
* Returns true if this object has a mapping for {@code name}. The mapping may be {@link #NULL}.
*
* @param name the name of the property
* @return true if this object has a mapping for {@code name}
*/
public boolean has(String name) {
return this.nameValuePairs.containsKey(name);
}
/**
* Returns the value mapped by {@code name}.
*
* @param name the name of the property
* @return the value
* @throws JSONException if no such mapping exists.
*/
public Object get(String name) throws JSONException {
Object result = this.nameValuePairs.get(name);
if (result == null) {
throw new JSONException("No value for " + name);
}
return result;
}
/**
* Returns the value mapped by {@code name}, or null if no such mapping exists.
*
* @param name the name of the property
* @return the value or {@code null}
*/
public Object opt(String name) {
return this.nameValuePairs.get(name);
}
/**
* Returns the value mapped by {@code name} if it exists and is a boolean or can be coerced to a boolean.
*
* @param name the name of the property
* @return the value
* @throws JSONException if the mapping doesn't exist or cannot be coerced to a boolean.
*/
public boolean getBoolean(String name) throws JSONException {
Object object = get(name);
Boolean result = JSON.toBoolean(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "boolean");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a boolean or can be coerced to a boolean. Returns
* false otherwise.
*
* @param name the name of the property
* @return the value or {@code null}
*/
public boolean optBoolean(String name) {
return optBoolean(name, false);
}
/**
* Returns the value mapped by {@code name} if it exists and is a boolean or can be coerced to a boolean. Returns
* {@code fallback} otherwise.
*
* @param name the name of the property
* @param fallback a fallback value
* @return the value or {@code fallback}
*/
public boolean optBoolean(String name, boolean fallback) {
Object object = opt(name);
Boolean result = JSON.toBoolean(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is a double or can be coerced to a double.
*
* @param name the name of the property
* @return the value
* @throws JSONException if the mapping doesn't exist or cannot be coerced to a double.
*/
public double getDouble(String name) throws JSONException {
Object object = get(name);
Double result = JSON.toDouble(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "double");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a double or can be coerced to a double. Returns
* {@code NaN} otherwise.
*
* @param name the name of the property
* @return the value or {@code NaN}
*/
public double optDouble(String name) {
return optDouble(name, Double.NaN);
}
/**
* Returns the value mapped by {@code name} if it exists and is a double or can be coerced to a double. Returns
* {@code fallback} otherwise.
*
* @param name the name of the property
* @param fallback a fallback value
* @return the value or {@code fallback}
*/
public double optDouble(String name, double fallback) {
Object object = opt(name);
Double result = JSON.toDouble(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is an int or can be coerced to an int.
*
* @param name the name of the property
* @return the value
* @throws JSONException if the mapping doesn't exist or cannot be coerced to an int.
*/
public int getInt(String name) throws JSONException {
Object object = get(name);
Integer result = JSON.toInteger(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "int");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is an int or can be coerced to an int. Returns 0
* otherwise.
*
* @param name the name of the property
* @return the value of {@code 0}
*/
public int optInt(String name) {
return optInt(name, 0);
}
/**
* Returns the value mapped by {@code name} if it exists and is an int or can be coerced to an int. Returns
* {@code fallback} otherwise.
*
* @param name the name of the property
* @param fallback a fallback value
* @return the value or {@code fallback}
*/
public int optInt(String name, int fallback) {
Object object = opt(name);
Integer result = JSON.toInteger(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is a long or can be coerced to a long. Note that JSON
* represents numbers as doubles, so this is <a href="#lossy">lossy</a>; use strings to transfer numbers over JSON.
*
* @param name the name of the property
* @return the value
* @throws JSONException if the mapping doesn't exist or cannot be coerced to a long.
*/
public long getLong(String name) throws JSONException {
Object object = get(name);
Long result = JSON.toLong(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "long");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a long or can be coerced to a long. Returns 0
* otherwise. Note that JSON represents numbers as doubles, so this is <a href="#lossy">lossy</a>; use strings to
* transfer numbers via JSON.
*
* @param name the name of the property
* @return the value or {@code 0L}
*/
public long optLong(String name) {
return optLong(name, 0L);
}
/**
* Returns the value mapped by {@code name} if it exists and is a long or can be coerced to a long. Returns
* {@code fallback} otherwise. Note that JSON represents numbers as doubles, so this is <a href="#lossy">lossy</a>;
* use strings to transfer numbers over JSON.
*
* @param name the name of the property
* @param fallback a fallback value
* @return the value or {@code fallback}
*/
public long optLong(String name, long fallback) {
Object object = opt(name);
Long result = JSON.toLong(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists, coercing it if necessary.
*
* @param name the name of the property
* @return the value
* @throws JSONException if no such mapping exists.
*/
public String getString(String name) throws JSONException {
Object object = get(name);
String result = JSON.toString(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "String");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists, coercing it if necessary. Returns the empty string if no
* such mapping exists.
*
* @param name the name of the property
* @return the value or an empty string
*/
public String optString(String name) {
return optString(name, "");
}
/**
* Returns the value mapped by {@code name} if it exists, coercing it if necessary. Returns {@code fallback} if no
* such mapping exists.
*
* @param name the name of the property
* @param fallback a fallback value
* @return the value or {@code fallback}
*/
public String optString(String name, String fallback) {
Object object = opt(name);
String result = JSON.toString(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code
* JSONArray}.
*
* @param name the name of the property
* @return the value
* @throws JSONException if the mapping doesn't exist or is not a {@code
* JSONArray}.
*/
public JSONArray getJSONArray(String name) throws JSONException {
Object object = get(name);
if (object instanceof JSONArray) {
return (JSONArray) object;
} else {
throw JSON.typeMismatch(name, object, "JSONArray");
}
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code
* JSONArray}. Returns null otherwise.
*
* @param name the name of the property
* @return the value or {@code null}
*/
public JSONArray optJSONArray(String name) {
Object object = opt(name);
return object instanceof JSONArray ? (JSONArray) object : null;
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code
* JSONObject}.
*
* @param name the name of the property
* @return the value
* @throws JSONException if the mapping doesn't exist or is not a {@code
* JSONObject}.
*/
public JSONObject getJSONObject(String name) throws JSONException {
Object object = get(name);
if (object instanceof JSONObject) {
return (JSONObject) object;
} else {
throw JSON.typeMismatch(name, object, "JSONObject");
}
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code
* JSONObject}. Returns null otherwise.
*
* @param name the name of the property
* @return the value or {@code null}
*/
public JSONObject optJSONObject(String name) {
Object object = opt(name);
return object instanceof JSONObject ? (JSONObject) object : null;
}
/**
* Returns an array with the values corresponding to {@code names}. The array contains null for names that aren't
* mapped. This method returns null if {@code names} is either null or empty.
*
* @param names the names of the properties
* @return the array
*/
public JSONArray toJSONArray(JSONArray names) {
JSONArray result = new JSONArray();
if (names == null) {
return null;
}
int length = names.length();
if (length == 0) {
return null;
}
for (int i = 0; i < length; i++) {
String name = JSON.toString(names.opt(i));
result.put(opt(name));
}
return result;
}
/**
* Returns an iterator of the {@code String} names in this object. The returned iterator supports
* {@link Iterator#remove() remove}, which will remove the corresponding mapping from this object. If this object is
* modified after the iterator is returned, the iterator's behavior is undefined. The order of the keys is undefined.
*
* @return the keys
*/
/* Return a raw type for API compatibility */
@SuppressWarnings("rawtypes")
public Iterator keys() {
return this.nameValuePairs.keySet().iterator();
}
/**
* Returns an array containing the string names in this object. This method returns null if this object contains no
* mappings.
*
* @return the array
*/
public JSONArray names() {
return this.nameValuePairs.isEmpty() ? null : new JSONArray(new ArrayList<>(this.nameValuePairs.keySet()));
}
/**
* Encodes this object as a compact JSON string, such as:
*
* <pre>
* {"query":"Pizza","locations":[94043,90210]}
* </pre>
*
* @return a string representation of the object.
*/
@Override
public String toString() {
try {
JSONStringer stringer = new JSONStringer();
writeTo(stringer);
return stringer.toString();
} catch (JSONException e) {
return null;
}
}
/**
* Encodes this object as a human-readable JSON string for debugging, such as:
*
* <pre>
* {
* "query": "Pizza",
* "locations": [
* 94043,
* 90210
* ]
* }
* </pre>
*
* @param indentSpaces the number of spaces to indent for each level of nesting.
* @return a string representation of the object.
* @throws JSONException if an error occurs
*/
public String toString(int indentSpaces) throws JSONException {
JSONStringer stringer = new JSONStringer(indentSpaces);
writeTo(stringer);
return stringer.toString();
}
void writeTo(JSONStringer stringer) throws JSONException {
stringer.object();
for (Map.Entry<String, Object> entry : this.nameValuePairs.entrySet()) {
stringer.key(entry.getKey()).value(entry.getValue());
}
stringer.endObject();
}
/**
* Encodes the number as a JSON string.
*
* @param number a finite value. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return the encoded value
* @throws JSONException if an error occurs
*/
public static String numberToString(Number number) throws JSONException {
if (number == null) {
throw new JSONException("Number must be non-null");
}
double doubleValue = number.doubleValue();
JSON.checkDouble(doubleValue);
// the original returns "-0" instead of "-0.0" for negative zero
if (number.equals(NEGATIVE_ZERO)) {
return "-0";
}
long longValue = number.longValue();
if (doubleValue == longValue) {
return Long.toString(longValue);
}
return number.toString();
}
/**
* Encodes {@code data} as a JSON string. This applies quotes and any necessary character escaping.
*
* @param data the string to encode. Null will be interpreted as an empty string.
* @return the quoted value
*/
public static String quote(String data) {
if (data == null) {
return "\"\"";
}
try {
JSONStringer stringer = new JSONStringer();
stringer.open(JSONStringer.Scope.NULL, "");
stringer.value(data);
stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, "");
return stringer.toString();
} catch (JSONException e) {
throw new AssertionError();
}
}
/**
* Wraps the given object if necessary.
* <p>
* If the object is null or, returns {@link #NULL}. If the object is a {@code JSONArray} or {@code JSONObject}, no
* wrapping is necessary. If the object is {@code NULL}, no wrapping is necessary. If the object is an array or
* {@code Collection}, returns an equivalent {@code JSONArray}. If the object is a {@code Map}, returns an equivalent
* {@code JSONObject}. If the object is a primitive wrapper type or {@code String}, returns the object. Otherwise if
* the object is from a {@code java} package, returns the result of {@code toString}. If wrapping fails, returns null.
*
* @param o the object to wrap
* @return the wrapped object
*/
@SuppressWarnings("rawtypes")
public static Object wrap(Object o) {
if (o == null) {
return NULL;
}
if (o instanceof JSONArray || o instanceof JSONObject) {
return o;
}
if (o.equals(NULL)) {
return o;
}
try {
if (o instanceof Collection) {
return new JSONArray((Collection) o);
} else if (o.getClass().isArray()) {
return new JSONArray(o);
}
if (o instanceof Map) {
return new JSONObject((Map) o);
}
if (o instanceof Boolean || o instanceof Byte || o instanceof Character || o instanceof Double
|| o instanceof Float || o instanceof Integer || o instanceof Long || o instanceof Short
|| o instanceof String) {
return o;
}
if (o.getClass().getPackage().getName().startsWith("java.")) {
return o.toString();
}
} catch (Exception ex) {
// Ignore
}
return null;
}
}

421
src/main/java/org/springframework/data/repository/aot/generate/json/JSONStringer.java

@ -0,0 +1,421 @@ @@ -0,0 +1,421 @@
/*
* 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.json;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// Note: this class was written without inspecting the non-free org.json source code.
/**
* Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most application developers should use those
* methods directly and disregard this API. For example:
*
* <pre>
* JSONObject object = ...
* String json = object.toString();
* </pre>
* <p>
* Stringers only encode well-formed JSON strings. In particular:
* <ul>
* <li>The stringer must have exactly one top-level array or object.
* <li>Lexical scopes must be balanced: every call to {@link #array} must have a matching call to {@link #endArray} and
* every call to {@link #object} must have a matching call to {@link #endObject}.
* <li>Arrays may not contain keys (property names).
* <li>Objects must alternate keys (property names) and values.
* <li>Values are inserted with either literal {@link #value(Object) value} calls, or by nesting arrays or objects.
* </ul>
* Calls that would result in a malformed JSON string will fail with a {@link JSONException}.
* <p>
* This class provides no facility for pretty-printing (ie. indenting) output. To encode indented output, use
* {@link JSONObject#toString(int)} or {@link JSONArray#toString(int)}.
* <p>
* Some implementations of the API support at most 20 levels of nesting. Attempts to create more than 20 levels of
* nesting may fail with a {@link JSONException}.
* <p>
* Each stringer may be used to encode a single top level value. Instances of this class are not thread safe. Although
* this class is nonfinal, it was not designed for inheritance and should not be subclassed. In particular, self-use by
* overrideable methods is not specified. See <i>Effective Java</i> Item 17, "Design and Document or inheritance or else
* prohibit it" for further information.
*/
public class JSONStringer {
/**
* The output data, containing at most one top-level array or object.
*/
final StringBuilder out = new StringBuilder();
/**
* Lexical scoping elements within this stringer, necessary to insert the appropriate separator characters (i.e.
* commas and colons) and to detect nesting errors.
*/
enum Scope {
/**
* An array with no elements requires no separators or newlines before it is closed.
*/
EMPTY_ARRAY,
/**
* An array with at least one value requires a comma and newline before the next element.
*/
NONEMPTY_ARRAY,
/**
* An object with no keys or values requires no separators or newlines before it is closed.
*/
EMPTY_OBJECT,
/**
* An object whose most recent element is a key. The next element must be a value.
*/
DANGLING_KEY,
/**
* An object with at least one name/value pair requires a comma and newline before the next element.
*/
NONEMPTY_OBJECT,
/**
* A special bracketless array needed by JSONStringer.join() and JSONObject.quote() only. Not used for JSON
* encoding.
*/
NULL
}
/**
* Unlike the original implementation, this stack isn't limited to 20 levels of nesting.
*/
private final List<Scope> stack = new ArrayList<>();
/**
* A string containing a full set of spaces for a single level of indentation, or null for no pretty printing.
*/
private final String indent;
public JSONStringer() {
this.indent = null;
}
JSONStringer(int indentSpaces) {
char[] indentChars = new char[indentSpaces];
Arrays.fill(indentChars, ' ');
this.indent = new String(indentChars);
}
/**
* Begins encoding a new array. Each call to this method must be paired with a call to {@link #endArray}.
*
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer array() throws JSONException {
return open(Scope.EMPTY_ARRAY, "[");
}
/**
* Ends encoding the current array.
*
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer endArray() throws JSONException {
return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]");
}
/**
* Begins encoding a new object. Each call to this method must be paired with a call to {@link #endObject}.
*
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer object() throws JSONException {
return open(Scope.EMPTY_OBJECT, "{");
}
/**
* Ends encoding the current object.
*
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer endObject() throws JSONException {
return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}");
}
/**
* Enters a new scope by appending any necessary whitespace and the given bracket.
*
* @param empty any necessary whitespace
* @param openBracket the open bracket
* @return this object
* @throws JSONException if processing of json failed
*/
JSONStringer open(Scope empty, String openBracket) throws JSONException {
if (this.stack.isEmpty() && !this.out.isEmpty()) {
throw new JSONException("Nesting problem: multiple top-level roots");
}
beforeValue();
this.stack.add(empty);
this.out.append(openBracket);
return this;
}
/**
* Closes the current scope by appending any necessary whitespace and the given bracket.
*
* @param empty any necessary whitespace
* @param nonempty the current scope
* @param closeBracket the close bracket
* @return the JSON stringer
* @throws JSONException if processing of json failed
*/
JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException {
Scope context = peek();
if (context != nonempty && context != empty) {
throw new JSONException("Nesting problem");
}
this.stack.remove(this.stack.size() - 1);
if (context == nonempty) {
newline();
}
this.out.append(closeBracket);
return this;
}
/**
* Returns the value on the top of the stack.
*
* @return the scope
* @throws JSONException if processing of json failed
*/
private Scope peek() throws JSONException {
if (this.stack.isEmpty()) {
throw new JSONException("Nesting problem");
}
return this.stack.get(this.stack.size() - 1);
}
/**
* Replace the value on the top of the stack with the given value.
*
* @param topOfStack the scope at the top of the stack
*/
private void replaceTop(Scope topOfStack) {
this.stack.set(this.stack.size() - 1, topOfStack);
}
/**
* Encodes {@code value}.
*
* @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double or null. May not be
* {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer value(Object value) throws JSONException {
if (this.stack.isEmpty()) {
throw new JSONException("Nesting problem");
}
if (value instanceof JSONArray) {
((JSONArray) value).writeTo(this);
return this;
} else if (value instanceof JSONObject) {
((JSONObject) value).writeTo(this);
return this;
}
beforeValue();
if (value == null || value instanceof Boolean || value == JSONObject.NULL) {
this.out.append(value);
} else if (value instanceof Number) {
this.out.append(JSONObject.numberToString((Number) value));
} else {
string(value.toString());
}
return this;
}
/**
* Encodes {@code value} to this stringer.
*
* @param value the value to encode
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer value(boolean value) throws JSONException {
if (this.stack.isEmpty()) {
throw new JSONException("Nesting problem");
}
beforeValue();
this.out.append(value);
return this;
}
/**
* Encodes {@code value} to this stringer.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer value(double value) throws JSONException {
if (this.stack.isEmpty()) {
throw new JSONException("Nesting problem");
}
beforeValue();
this.out.append(JSONObject.numberToString(value));
return this;
}
/**
* Encodes {@code value} to this stringer.
*
* @param value the value to encode
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer value(long value) throws JSONException {
if (this.stack.isEmpty()) {
throw new JSONException("Nesting problem");
}
beforeValue();
this.out.append(value);
return this;
}
private void string(String value) {
this.out.append("\"");
for (int i = 0, length = value.length(); i < length; i++) {
char c = value.charAt(i);
/*
* From RFC 4627, "All Unicode characters may be placed within the quotation
* marks except for the characters that must be escaped: quotation mark,
* reverse solidus, and the control characters (U+0000 through U+001F)."
*/
switch (c) {
case '"', '\\', '/' -> this.out.append('\\').append(c);
case '\t' -> this.out.append("\\t");
case '\b' -> this.out.append("\\b");
case '\n' -> this.out.append("\\n");
case '\r' -> this.out.append("\\r");
case '\f' -> this.out.append("\\f");
default -> {
if (c <= 0x1F) {
this.out.append(String.format("\\u%04x", (int) c));
} else {
this.out.append(c);
}
}
}
}
this.out.append("\"");
}
private void newline() {
if (this.indent == null) {
return;
}
this.out.append("\n");
this.out.append(this.indent.repeat(this.stack.size()));
}
/**
* Encodes the key (property name) to this stringer.
*
* @param name the name of the forthcoming value. May not be null.
* @return this stringer.
* @throws JSONException if processing of json failed
*/
public JSONStringer key(String name) throws JSONException {
if (name == null) {
throw new JSONException("Names must be non-null");
}
beforeKey();
string(name);
return this;
}
/**
* Inserts any necessary separators and whitespace before a name. Also adjusts the stack to expect the key's value.
*
* @throws JSONException if processing of json failed
*/
private void beforeKey() throws JSONException {
Scope context = peek();
if (context == Scope.NONEMPTY_OBJECT) { // first in object
this.out.append(',');
} else if (context != Scope.EMPTY_OBJECT) { // not in an object!
throw new JSONException("Nesting problem");
}
newline();
replaceTop(Scope.DANGLING_KEY);
}
/**
* Inserts any necessary separators and whitespace before a literal value, inline array, or inline object. Also
* adjusts the stack to expect either a closing bracket or another element.
*
* @throws JSONException if processing of json failed
*/
private void beforeValue() throws JSONException {
if (this.stack.isEmpty()) {
return;
}
Scope context = peek();
if (context == Scope.EMPTY_ARRAY) { // first in array
replaceTop(Scope.NONEMPTY_ARRAY);
newline();
} else if (context == Scope.NONEMPTY_ARRAY) { // another in array
this.out.append(',');
newline();
} else if (context == Scope.DANGLING_KEY) { // value for key
this.out.append(this.indent == null ? ":" : ": ");
replaceTop(Scope.NONEMPTY_OBJECT);
} else if (context != Scope.NULL) {
throw new JSONException("Nesting problem");
}
}
/**
* Returns the encoded JSON string.
* <p>
* If invoked with unterminated arrays or unclosed objects, this method's return value is undefined.
* <p>
* <strong>Warning:</strong> although it contradicts the general contract of {@link Object#toString}, this method
* returns null if the stringer contains no data.
*
* @return the encoded JSON string.
*/
@Override
public String toString() {
return this.out.isEmpty() ? null : this.out.toString();
}
}

539
src/main/java/org/springframework/data/repository/aot/generate/json/JSONTokener.java

@ -0,0 +1,539 @@ @@ -0,0 +1,539 @@
/*
* 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.json;
// Note: this class was written without inspecting the non-free org.json source code.
/**
* Parses a JSON (<a href="https://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) encoded string into the corresponding
* object. Most clients of this class will use only need the {@link #JSONTokener(String) constructor} and
* {@link #nextValue} method. Example usage:
*
* <pre>
* String json = "{" + " \"query\": \"Pizza\", " + " \"locations\": [ 94043, 90210 ] " + "}";
*
* JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
* String query = object.getString("query");
* JSONArray locations = object.getJSONArray("locations");
* </pre>
* <p>
* For best interoperability and performance use JSON that complies with RFC 4627, such as that generated by
* {@link JSONStringer}. For legacy reasons this parser is lenient, so a successful parse does not indicate that the
* input string was valid JSON. All the following syntax errors will be ignored:
* <ul>
* <li>End of line comments starting with {@code //} or {@code #} and ending with a newline character.
* <li>C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such comments may not be nested.
* <li>Strings that are unquoted or {@code 'single quoted'}.
* <li>Hexadecimal integers prefixed with {@code 0x} or {@code 0X}.
* <li>Octal integers prefixed with {@code 0}.
* <li>Array elements separated by {@code ;}.
* <li>Unnecessary array separators. These are interpreted as if null was the omitted value.
* <li>Key-value pairs separated by {@code =} or {@code =>}.
* <li>Key-value pairs separated by {@code ;}.
* </ul>
* <p>
* Each tokener may be used to parse a single JSON string. Instances of this class are not thread safe. Although this
* class is nonfinal, it was not designed for inheritance and should not be subclassed. In particular, self-use by
* overrideable methods is not specified. See <i>Effective Java</i> Item 17, "Design and Document or inheritance or else
* prohibit it" for further information.
*/
public class JSONTokener {
/**
* The input JSON.
*/
private final String in;
/**
* The index of the next character to be returned by {@link #next}. When the input is exhausted, this equals the
* input's length.
*/
private int pos;
/**
* @param in JSON encoded string. Null is not permitted and will yield a tokener that throws
* {@code NullPointerExceptions} when methods are called.
*/
public JSONTokener(String in) {
// consume an optional byte order mark (BOM) if it exists
if (in != null && in.startsWith("\ufeff")) {
in = in.substring(1);
}
this.in = in;
}
/**
* Returns the next value from the input.
*
* @return a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double or {@link JSONObject#NULL}.
* @throws JSONException if the input is malformed.
*/
public Object nextValue() throws JSONException {
int c = nextCleanInternal();
switch (c) {
case -1:
throw syntaxError("End of input");
case '{':
return readObject();
case '[':
return readArray();
case '\'', '"':
return nextString((char) c);
default:
this.pos--;
return readLiteral();
}
}
private int nextCleanInternal() throws JSONException {
while (this.pos < this.in.length()) {
int c = this.in.charAt(this.pos++);
switch (c) {
case '\t', ' ', '\n', '\r':
continue;
case '/':
if (this.pos == this.in.length()) {
return c;
}
char peek = this.in.charAt(this.pos);
switch (peek) {
case '*':
// skip a /* c-style comment */
this.pos++;
int commentEnd = this.in.indexOf("*/", this.pos);
if (commentEnd == -1) {
throw syntaxError("Unterminated comment");
}
this.pos = commentEnd + 2;
continue;
case '/':
// skip a // end-of-line comment
this.pos++;
skipToEndOfLine();
continue;
default:
return c;
}
case '#':
/*
* Skip a # hash end-of-line comment. The JSON RFC doesn't specify
* this behavior, but it's required to parse existing documents. See
* https://b/2571423.
*/
skipToEndOfLine();
continue;
default:
return c;
}
}
return -1;
}
/**
* Advances the position until after the next newline character. If the line is terminated by "\r\n", the '\n' must be
* consumed as whitespace by the caller.
*/
private void skipToEndOfLine() {
for (; this.pos < this.in.length(); this.pos++) {
char c = this.in.charAt(this.pos);
if (c == '\r' || c == '\n') {
this.pos++;
break;
}
}
}
/**
* Returns the string up to but not including {@code quote}, unescaping any character escape sequences encountered
* along the way. The opening quote should have already been read. This consumes the closing quote, but does not
* include it in the returned string.
*
* @param quote either ' or ".
* @return the string up to but not including {@code quote}
* @throws NumberFormatException if any unicode escape sequences are malformed.
* @throws JSONException if processing of json failed
*/
public String nextString(char quote) throws JSONException {
/*
* For strings that are free of escape sequences, we can just extract the result
* as a substring of the input. But if we encounter an escape sequence, we need to
* use a StringBuilder to compose the result.
*/
StringBuilder builder = null;
/* the index of the first character not yet appended to the builder. */
int start = this.pos;
while (this.pos < this.in.length()) {
int c = this.in.charAt(this.pos++);
if (c == quote) {
if (builder == null) {
// a new string avoids leaking memory
return new String(this.in.substring(start, this.pos - 1));
} else {
builder.append(this.in, start, this.pos - 1);
return builder.toString();
}
}
if (c == '\\') {
if (this.pos == this.in.length()) {
throw syntaxError("Unterminated escape sequence");
}
if (builder == null) {
builder = new StringBuilder();
}
builder.append(this.in, start, this.pos - 1);
builder.append(readEscapeCharacter());
start = this.pos;
}
}
throw syntaxError("Unterminated string");
}
/**
* Unescapes the character identified by the character or characters that immediately follow a backslash. The
* backslash '\' should have already been read. This supports both unicode escapes "u000A" and two-character escapes
* "\n".
*
* @return the unescaped char
* @throws NumberFormatException if any unicode escape sequences are malformed.
* @throws JSONException if processing of json failed
*/
private char readEscapeCharacter() throws JSONException {
char escaped = this.in.charAt(this.pos++);
switch (escaped) {
case 'u':
if (this.pos + 4 > this.in.length()) {
throw syntaxError("Unterminated escape sequence");
}
String hex = this.in.substring(this.pos, this.pos + 4);
this.pos += 4;
return (char) Integer.parseInt(hex, 16);
case 't':
return '\t';
case 'b':
return '\b';
case 'n':
return '\n';
case 'r':
return '\r';
case 'f':
return '\f';
case '\'', '"', '\\':
default:
return escaped;
}
}
/**
* Reads a null, boolean, numeric or unquoted string literal value. Numeric values will be returned as an Integer,
* Long, or Double, in that order of preference.
*
* @return a literal value
* @throws JSONException if processing of json failed
*/
private Object readLiteral() throws JSONException {
String literal = nextToInternal("{}[]/\\:,=;# \t\f");
if (literal.isEmpty()) {
throw syntaxError("Expected literal value");
} else if ("null".equalsIgnoreCase(literal)) {
return JSONObject.NULL;
} else if ("true".equalsIgnoreCase(literal)) {
return Boolean.TRUE;
} else if ("false".equalsIgnoreCase(literal)) {
return Boolean.FALSE;
}
/* try to parse as an integral type... */
if (literal.indexOf('.') == -1) {
int base = 10;
String number = literal;
if (number.startsWith("0x") || number.startsWith("0X")) {
number = number.substring(2);
base = 16;
} else if (number.startsWith("0") && number.length() > 1) {
number = number.substring(1);
base = 8;
}
try {
long longValue = Long.parseLong(number, base);
if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) {
return (int) longValue;
} else {
return longValue;
}
} catch (NumberFormatException e) {
/*
* This only happens for integral numbers greater than Long.MAX_VALUE,
* numbers in exponential form (5e-10) and unquoted strings. Fall through
* to try floating point.
*/
}
}
/* ...next try to parse as a floating point... */
try {
return Double.valueOf(literal);
} catch (NumberFormatException ex) {
// Ignore
}
/* ... finally give up. We have an unquoted string */
return new String(literal); // a new string avoids leaking memory
}
/**
* Returns the string up to but not including any of the given characters or a newline character. This does not
* consume the excluded character.
*
* @return the string up to but not including any of the given characters or a newline character
*/
private String nextToInternal(String excluded) {
int start = this.pos;
for (; this.pos < this.in.length(); this.pos++) {
char c = this.in.charAt(this.pos);
if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) {
return this.in.substring(start, this.pos);
}
}
return this.in.substring(start);
}
/**
* Reads a sequence of key/value pairs and the trailing closing brace '}' of an object. The opening brace '{' should
* have already been read.
*
* @return an object
* @throws JSONException if processing of json failed
*/
private JSONObject readObject() throws JSONException {
JSONObject result = new JSONObject();
/* Peek to see if this is the empty object. */
int first = nextCleanInternal();
if (first == '}') {
return result;
} else if (first != -1) {
this.pos--;
}
while (true) {
Object name = nextValue();
if (!(name instanceof String)) {
if (name == null) {
throw syntaxError("Names cannot be null");
} else {
throw syntaxError("Names must be strings, but " + name + " is of type " + name.getClass().getName());
}
}
/*
* Expect the name/value separator to be either a colon ':', an equals sign
* '=', or an arrow "=>". The last two are bogus but we include them because
* that's what the original implementation did.
*/
int separator = nextCleanInternal();
if (separator != ':' && separator != '=') {
throw syntaxError("Expected ':' after " + name);
}
if (this.pos < this.in.length() && this.in.charAt(this.pos) == '>') {
this.pos++;
}
result.put((String) name, nextValue());
switch (nextCleanInternal()) {
case '}':
return result;
case ';', ',':
continue;
default:
throw syntaxError("Unterminated object");
}
}
}
/**
* Reads a sequence of values and the trailing closing brace ']' of an array. The opening brace '[' should have
* already been read. Note that "[]" yields an empty array, but "[,]" returns a two-element array equivalent to
* "[null,null]".
*
* @return an array
* @throws JSONException if processing of json failed
*/
private JSONArray readArray() throws JSONException {
JSONArray result = new JSONArray();
/* to cover input that ends with ",]". */
boolean hasTrailingSeparator = false;
while (true) {
switch (nextCleanInternal()) {
case -1:
throw syntaxError("Unterminated array");
case ']':
if (hasTrailingSeparator) {
result.put(null);
}
return result;
case ',', ';':
/* A separator without a value first means "null". */
result.put(null);
hasTrailingSeparator = true;
continue;
default:
this.pos--;
}
result.put(nextValue());
switch (nextCleanInternal()) {
case ']':
return result;
case ',', ';':
hasTrailingSeparator = true;
continue;
default:
throw syntaxError("Unterminated array");
}
}
}
/**
* Returns an exception containing the given message plus the current position and the entire input string.
*
* @param message the message
* @return an exception
*/
public JSONException syntaxError(String message) {
return new JSONException(message + this);
}
/**
* Returns the current position and the entire input string.
*
* @return the current position and the entire input string.
*/
@Override
public String toString() {
// consistent with the original implementation
return " at character " + this.pos + " of " + this.in;
}
/*
* Legacy APIs.
*
* None of the methods below are on the critical path of parsing JSON documents. They
* exist only because they were exposed by the original implementation and may be used
* by some clients.
*/
public boolean more() {
return this.pos < this.in.length();
}
public char next() {
return this.pos < this.in.length() ? this.in.charAt(this.pos++) : '\0';
}
public char next(char c) throws JSONException {
char result = next();
if (result != c) {
throw syntaxError("Expected " + c + " but was " + result);
}
return result;
}
public char nextClean() throws JSONException {
int nextCleanInt = nextCleanInternal();
return nextCleanInt == -1 ? '\0' : (char) nextCleanInt;
}
public String next(int length) throws JSONException {
if (this.pos + length > this.in.length()) {
throw syntaxError(length + " is out of bounds");
}
String result = this.in.substring(this.pos, this.pos + length);
this.pos += length;
return result;
}
public String nextTo(String excluded) {
if (excluded == null) {
throw new NullPointerException("excluded == null");
}
return nextToInternal(excluded).trim();
}
public String nextTo(char excluded) {
return nextToInternal(String.valueOf(excluded)).trim();
}
public void skipPast(String thru) {
int thruStart = this.in.indexOf(thru, this.pos);
this.pos = thruStart == -1 ? this.in.length() : (thruStart + thru.length());
}
public char skipTo(char to) {
int index = this.in.indexOf(to, this.pos);
if (index != -1) {
this.pos = index;
return to;
} else {
return '\0';
}
}
public void back() {
if (--this.pos == -1) {
this.pos = 0;
}
}
public static int dehexchar(char hex) {
if (hex >= '0' && hex <= '9') {
return hex - '0';
} else if (hex >= 'A' && hex <= 'F') {
return hex - 'A' + 10;
} else if (hex >= 'a' && hex <= 'f') {
return hex - 'a' + 10;
} else {
return -1;
}
}
}

5
src/main/java/org/springframework/data/repository/aot/generate/json/package-info.java

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
/**
* Shaded JSON. This source was originally taken from com.vaadin.external.google:android-json which provides a clean
* room re-implementation of the org.json APIs and does not include the "Do not use for evil" clause.
*/
package org.springframework.data.repository.aot.generate.json;

5
src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java

@ -73,4 +73,9 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R @@ -73,4 +73,9 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R
return baseComposition.get().findMethod(method).orElse(method);
}
@Override
public RepositoryComposition getRepositoryComposition() {
return baseComposition.get().append(RepositoryComposition.RepositoryFragments.from(fragments.get()));
}
}

12
src/main/java/org/springframework/data/repository/core/RepositoryInformation.java

@ -18,6 +18,8 @@ package org.springframework.data.repository.core; @@ -18,6 +18,8 @@ package org.springframework.data.repository.core;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.data.repository.core.support.RepositoryComposition;
/**
* Additional repository specific information
*
@ -93,4 +95,14 @@ public interface RepositoryInformation extends RepositoryMetadata { @@ -93,4 +95,14 @@ public interface RepositoryInformation extends RepositoryMetadata {
default boolean hasQueryMethods() {
return getQueryMethods().iterator().hasNext();
}
/**
* Returns the {@link RepositoryComposition} for this repository. This is used to determine the composition of the
* repository and its fragments.
*
* @return never {@literal null}.
* @since 4.0
*/
RepositoryComposition getRepositoryComposition();
}

5
src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java

@ -131,4 +131,9 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen @@ -131,4 +131,9 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
return composition.getFragments().toSet();
}
@Override
public RepositoryComposition getRepositoryComposition() {
return composition.append(baseComposition.getFragments());
}
}

29
src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java

@ -18,6 +18,7 @@ package org.springframework.data.repository.core.support; @@ -18,6 +18,7 @@ package org.springframework.data.repository.core.support;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@ -290,6 +291,25 @@ public class RepositoryComposition { @@ -290,6 +291,25 @@ public class RepositoryComposition {
method, methodToCall, argumentConverter.apply(methodToCall, args));
}
/**
* Find the {@link RepositoryFragment} for the given {@link Method} invoked on the composite interface.
*
* @param method must not be {@literal null}.
* @return the fragment implementing that method or {@literal null} if not found.
*/
public @Nullable RepositoryFragment<?> findFragment(Method method) {
Method methodToCall = getMethod(method);
if (methodToCall != null) {
return fragments.stream().filter(it -> it.hasMethod(methodToCall)) //
.findFirst().orElse(null);
}
return null;
}
/**
* Find the implementation method for the given {@link Method} invoked on the composite interface.
*
@ -414,12 +434,12 @@ public class RepositoryComposition { @@ -414,12 +434,12 @@ public class RepositoryComposition {
}
/**
* Create {@link RepositoryFragments} from a {@link List} of {@link RepositoryFragment fragments}.
* Create {@link RepositoryFragments} from a {@link Collection} of {@link RepositoryFragment fragments}.
*
* @param fragments must not be {@literal null}.
* @return the {@link RepositoryFragments} for {@code implementations}.
*/
public static RepositoryFragments from(List<RepositoryFragment<?>> fragments) {
public static RepositoryFragments from(Collection<RepositoryFragment<?>> fragments) {
Assert.notNull(fragments, "RepositoryFragments must not be null");
@ -476,6 +496,11 @@ public class RepositoryComposition { @@ -476,6 +496,11 @@ public class RepositoryComposition {
return fragments.iterator();
}
@Nullable
RepositoryFragment<?> findFragment(Method methodToCall) {
return fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment);
}
/**
* @return {@link Stream} of {@link Method methods}.
*/

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

@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*;
import example.UserRepository;
import java.lang.reflect.Method;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@ -48,6 +49,13 @@ class RepositoryContributorUnitTests { @@ -48,6 +49,13 @@ class RepositoryContributorUnitTests {
RepositoryInformation repositoryInformation) {
return MethodContributor.forQueryMethod(new QueryMethod(method, repositoryInformation, getProjectionFactory()))
.withMetadata(new QueryMetadata() {
@Override
public Map<String, Object> serialize() {
return Map.of();
}
})
.contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();

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

@ -124,4 +124,10 @@ class StubRepositoryInformation implements RepositoryInformation { @@ -124,4 +124,10 @@ class StubRepositoryInformation implements RepositoryInformation {
public Method getTargetClassMethod(Method method) {
return null;
}
@Override
public RepositoryComposition getRepositoryComposition() {
return baseComposition;
}
}

6
src/test/java/org/springframework/data/repository/core/support/DummyRepositoryInformation.java

@ -121,4 +121,10 @@ public final class DummyRepositoryInformation implements RepositoryInformation { @@ -121,4 +121,10 @@ public final class DummyRepositoryInformation implements RepositoryInformation {
public Set<RepositoryFragment<?>> getFragments() {
return Collections.emptySet();
}
@Override
public RepositoryComposition getRepositoryComposition() {
return RepositoryComposition.empty();
}
}

Loading…
Cancel
Save