Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,11 @@ private List<String> fetchGroupChainCalls(Set<String> excludedFieldNames) {
Set<String> nestedAssocPaths = new LinkedHashSet<>();
for (DtoPropertyMeta property : activeProperties) {
if ((property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY)
&& !property.isUnfetchable()
&& !property.hasComputedSegment()) {
nestedAssocPaths.add(property.sourcePropertyPath().get(0));
} else if (property.kind() == DtoPropertyMeta.Kind.SCALAR && property.isListTarget()
&& !property.isScalarCollection()
&& !property.hasComputedSegment() && property.sourcePropertyPath().size() == 1) {
// a single-segment SCALAR property whose DTO field is a List with no registered nested
// DTO mapping of its own (e.g. @DtoConvert reducing a ToMany association) - still fully
Expand All @@ -253,7 +255,7 @@ private List<String> fetchGroupChainCalls(Set<String> excludedFieldNames) {
switch (property.kind()) {
case NESTED_ONE:
case NESTED_MANY:
if (property.hasComputedSegment()) {
if (property.hasComputedSegment() || property.isUnfetchable()) {
// a single-hop @DtoPath rename traversing a computed/derived getter (no backing
// field) that happens to target a nested DTO type - just as unfetchable via
// fetch(path, mapper.fetchGroup()) as the analogous SCALAR case, since "path" here
Expand All @@ -267,7 +269,7 @@ private List<String> fetchGroupChainCalls(Set<String> excludedFieldNames) {
property.sourcePropertyPath().get(0), mapperFieldName(property)));
break;
case SCALAR:
if (property.hasComputedSegment()) {
if (property.hasComputedSegment() || property.isUnfetchable()) {
// the path traverses a computed/derived getter (no backing field) - its own segments
// past that point aren't real Ebean fetch paths, so don't add them to pathSelect/
// rootSelect at all; @DtoPath#requires() (plus the real prefix, if any) already names
Expand All @@ -277,7 +279,7 @@ private List<String> fetchGroupChainCalls(Set<String> excludedFieldNames) {
}
List<String> path = property.sourcePropertyPath();
if (path.size() == 1) {
if (property.isListTarget()) {
if (property.isListTarget() && !property.isScalarCollection()) {
// a single-segment path whose DTO field type is a List, but with no registered
// nested DTO mapping of its own (e.g. a @DtoConvert-backed property reducing a
// ToMany association to a simpler element type) - the source side is still a real
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -819,32 +819,37 @@ private DtoPropertyMeta resolveProperty(VariableElement field, DtoBeanMeta meta)
// straight into a NullPointerException. Default to the primitive's zero-equivalent value,
// or fail fast with a clear message instead when @DtoPath(failOnNull = true).
boolean isListTarget = listElementType(field.asType()) != null;
boolean scalarCollection = isListTarget && isScalarCollectionProperty(lastOwnerType, properties.get(properties.size() - 1));
DtoConverterMeta pathConverter = isListTarget ? converter
: autoTypeConverter(converter, lastOwnerType != null ? getterReturnTypeMirror(lastOwnerType, lastGetter) : null, field.asType());
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, pathConverter,
field.asType().getKind().isPrimitive(), pathPrism.failOnNull(), computedFrom >= 0, requiredFetchPaths,
isListTarget, false);
isListTarget, scalarCollection, false);
}
TypeMirror fieldType = field.asType();
TypeMirror listElementType = listElementType(fieldType);
boolean unfetchable = isUnfetchableProperty(meta.source(), name);
if (listElementType != null) {
DtoBeanMeta nested = lookupByTarget(listElementType);
if (nested != null) {
rejectConverterOnNested(field, converter, name, meta);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, List.of(getterName(meta.source(), name)), List.of(name), nested);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, List.of(getterName(meta.source(), name)), List.of(name), nested,
unfetchable, List.of());
}
} else {
DtoBeanMeta nested = lookupByTarget(fieldType);
if (nested != null) {
rejectConverterOnNested(field, converter, name, meta);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, List.of(getterName(meta.source(), name)), List.of(name), nested);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, List.of(getterName(meta.source(), name)), List.of(name), nested,
unfetchable, List.of());
}
}
String getter = getterName(meta.source(), name);
boolean scalarCollection = listElementType != null && isScalarCollectionProperty(meta.source(), name);
DtoConverterMeta scalarConverter = listElementType != null ? converter
: autoTypeConverter(converter, getterReturnTypeMirror(meta.source(), getter), fieldType);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getter), List.of(name), null, scalarConverter,
fieldType.getKind().isPrimitive(), listElementType != null);
fieldType.getKind().isPrimitive(), false, false, List.of(), listElementType != null, scalarCollection, unfetchable, false);
}

/**
Expand Down Expand Up @@ -1055,7 +1060,35 @@ private boolean hasField(TypeElement type, String propertyName) {
for (TypeElement current = type; current != null; current = superclassOf(current)) {
for (VariableElement f : ElementFilter.fieldsIn(current.getEnclosedElements())) {
if (f.getSimpleName().contentEquals(propertyName)) {
return true;
return !ctx.isTransientField(f);
}
}
}
return false;
}

private boolean isScalarCollectionProperty(TypeElement type, String propertyName) {
if (type == null) {
return false;
}
for (TypeElement current = type; current != null; current = superclassOf(current)) {
for (VariableElement field : ElementFilter.fieldsIn(current.getEnclosedElements())) {
if (field.getSimpleName().contentEquals(propertyName)) {
return ctx.isScalarCollectionField(field);
}
}
}
return false;
}

private boolean isUnfetchableProperty(TypeElement type, String propertyName) {
if (type == null) {
return false;
}
for (TypeElement current = type; current != null; current = superclassOf(current)) {
for (VariableElement field : ElementFilter.fieldsIn(current.getEnclosedElements())) {
if (field.getSimpleName().contentEquals(propertyName)) {
return ctx.isTransientField(field);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ enum Kind {
private final boolean computedSegment;
private final List<String> requiredFetchPaths;
private final boolean listTarget;
private final boolean scalarCollection;
private final boolean unfetchable;
private final boolean ignored;

DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath, DtoBeanMeta nested) {
Expand All @@ -44,7 +46,7 @@ enum Kind {
* before ever consulting them.
*/
static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) {
return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, true);
return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, false, false, true);
}

/**
Expand Down Expand Up @@ -90,6 +92,22 @@ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) {
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull,
boolean computedSegment, List<String> requiredFetchPaths, boolean listTarget, boolean ignored) {
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, primitiveTarget, failOnNull,
computedSegment, requiredFetchPaths, listTarget, false, ignored);
}

DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull,
boolean computedSegment, List<String> requiredFetchPaths, boolean listTarget,
boolean scalarCollection, boolean ignored) {
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, primitiveTarget, failOnNull,
computedSegment, requiredFetchPaths, listTarget, scalarCollection, false, ignored);
}

DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull,
boolean computedSegment, List<String> requiredFetchPaths, boolean listTarget,
boolean scalarCollection, boolean unfetchable, boolean ignored) {
this.dtoFieldName = dtoFieldName;
this.kind = kind;
this.sourceGetterPath = sourceGetterPath;
Expand All @@ -101,6 +119,8 @@ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) {
this.computedSegment = computedSegment;
this.requiredFetchPaths = requiredFetchPaths;
this.listTarget = listTarget;
this.scalarCollection = scalarCollection;
this.unfetchable = unfetchable;
this.ignored = ignored;
}

Expand Down Expand Up @@ -177,6 +197,18 @@ boolean isListTarget() {
return listTarget || kind == Kind.NESTED_MANY;
}

/**
* Return true when the source property is a scalar collection such as an Ebean {@code @DbArray},
* rather than a to-many association.
*/
boolean isScalarCollection() {
return scalarCollection;
}

boolean isUnfetchable() {
return unfetchable;
}

/**
* {@code true} when this property is marked {@code @DtoIgnore} - permanently excluded from
* every mapping (base and every named variant alike), always given its empty default rather
Expand Down Expand Up @@ -245,4 +277,3 @@ private void appendGuardedChain(StringBuilder sb, String prefix, int index) {
sb.append(')');
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ private boolean isStaticOrTransient(VariableElement field) {
);
}

boolean isTransientField(Element field) {
if (field.getKind() != ElementKind.FIELD) {
return false;
}
VariableElement variable = (VariableElement) field;
return variable.getModifiers().contains(Modifier.TRANSIENT)
|| hasAnnotations(variable, "jakarta.persistence.Transient");
}

private static boolean hasAnnotations(Element element, String... annotations) {
return getAnnotation(element, annotations) != null;
}
Expand Down Expand Up @@ -268,17 +277,21 @@ String findDbName(TypeElement element) {
/**
* Return true if it is a DbJson field.
*/
private static boolean dbJsonField(Element field) {
boolean isDbJsonField(Element field) {
return hasAnnotations(field, DBJSON, DBJSONB);
}

/**
* Return true if it is a DbArray field.
*/
private static boolean dbArrayField(Element field) {
boolean isDbArrayField(Element field) {
return hasAnnotations(field, DBARRAY);
}

boolean isScalarCollectionField(Element field) {
return isDbArrayField(field) || isDbJsonField(field);
}

private static boolean dbToMany(Element field) {
return hasAnnotations(field, ONE_TO_MANY, MANY_TO_MANY);
}
Expand Down Expand Up @@ -417,10 +430,10 @@ PropertyType getPropertyType(VariableElement field) {
}

boolean toMany = dbToMany(field);
if (dbJsonField(field)) {
if (isDbJsonField(field)) {
return propertyTypeMap.getDbJsonType();
}
if (dbArrayField(field)) {
if (isDbArrayField(field)) {
// get generic parameter type
DeclaredType declaredType = (DeclaredType) field.asType();
TypeMirror arrayElementType = declaredType.getTypeArguments().get(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package io.ebean.querybean.generator;

import org.junit.jupiter.api.Test;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertTrue;

class DtoMapperDbArrayTest {

@Test
void dbArrayList_isSelectedAsScalarColumn() throws IOException {
Path sourceDir = Files.createTempDirectory("dto-db-array-src");
Path outDir = Files.createTempDirectory("dto-db-array-out");
Path genSourceDir = Files.createTempDirectory("dto-db-array-gensrc");

writeSource(sourceDir, "org.tests.dbarray.ProcessLog",
"package org.tests.dbarray;\n"
+ "import io.ebean.annotation.DbArray;\n"
+ "import io.ebean.annotation.DbJson;\n"
+ "import io.ebean.annotation.DbJsonB;\n"
+ "import jakarta.persistence.Transient;\n"
+ "public class ProcessLog {\n"
+ " @DbArray private java.util.List<Long> sourceIds;\n"
+ " @DbJson private java.util.List<Long> jsonIds;\n"
+ " @DbJsonB private java.util.List<Long> jsonbIds;\n"
+ " @Transient private String computed;\n"
+ " public java.util.List<Long> sourceIds() { return sourceIds; }\n"
+ " public java.util.List<Long> jsonIds() { return jsonIds; }\n"
+ " public java.util.List<Long> jsonbIds() { return jsonbIds; }\n"
+ " public String computed() { return computed; }\n"
+ "}\n");
writeSource(sourceDir, "org.tests.dbarray.ProcessLogDto",
"package org.tests.dbarray;\n"
+ "public class ProcessLogDto {\n"
+ " private final java.util.List<Long> sourceIds;\n"
+ " private final java.util.List<Long> jsonIds;\n"
+ " private final java.util.List<Long> jsonbIds;\n"
+ " private final String computed;\n"
+ " public ProcessLogDto(java.util.List<Long> sourceIds, java.util.List<Long> jsonIds,\n"
+ " java.util.List<Long> jsonbIds, String computed) {\n"
+ " this.sourceIds = sourceIds; this.jsonIds = jsonIds; this.jsonbIds = jsonbIds; this.computed = computed;\n"
+ " }\n"
+ " public java.util.List<Long> getSourceIds() { return sourceIds; }\n"
+ " public java.util.List<Long> getJsonIds() { return jsonIds; }\n"
+ " public java.util.List<Long> getJsonbIds() { return jsonbIds; }\n"
+ " public String getComputed() { return computed; }\n"
+ "}\n");
writeSource(sourceDir, "org.tests.dbarray.package-info",
"@io.ebean.annotation.DtoMapping(source = ProcessLog.class, target = ProcessLogDto.class)\n"
+ "package org.tests.dbarray;\n");
writeSource(sourceDir, "io.ebean.typequery.Generated",
"package io.ebean.typequery;\n"
+ "public @interface Generated { String value(); }\n");

DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
List<Path> sourceFiles;
try (var walk = Files.walk(sourceDir)) {
sourceFiles = walk.filter(path -> path.toString().endsWith(".java")).collect(Collectors.toList());
}
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromPaths(sourceFiles);
List<String> options = List.of(
"-d", outDir.toString(),
"-s", genSourceDir.toString(),
"-classpath", System.getProperty("java.class.path"),
"-processor", Processor.class.getName());

JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, diagnostics, options, null, compilationUnits);
assertTrue(task.call(), "compilation failed: " + errors(diagnostics));

Path mapperFile;
try (var walk = Files.walk(genSourceDir)) {
mapperFile = walk.filter(path -> path.getFileName().toString().equals("ProcessLogDtoMapper.java"))
.findFirst()
.orElseThrow(() -> new AssertionError("generated mapper source not found"));
}
String generated = Files.readString(mapperFile);
assertTrue(generated.contains("select(\"sourceIds,jsonIds,jsonbIds\")"), generated);
assertTrue(generated.contains("source.sourceIds()"), generated);
assertTrue(generated.contains("source.jsonIds()"), generated);
assertTrue(generated.contains("source.jsonbIds()"), generated);
assertTrue(generated.contains("source.computed()"), generated);
assertTrue(!generated.contains("fetch(\"sourceIds\")"), generated);
assertTrue(!generated.contains("fetch(\"jsonIds\")"), generated);
assertTrue(!generated.contains("fetch(\"jsonbIds\")"), generated);
}
}

private List<String> errors(DiagnosticCollector<JavaFileObject> diagnostics) {
return diagnostics.getDiagnostics().stream()
.filter(diagnostic -> diagnostic.getKind() == Diagnostic.Kind.ERROR)
.map(diagnostic -> diagnostic.getMessage(Locale.getDefault()))
.collect(Collectors.toList());
}

private void writeSource(Path sourceDir, String fqn, String content) {
try {
Path packageDir = sourceDir.resolve(fqn.substring(0, fqn.lastIndexOf('.')).replace('.', '/'));
Files.createDirectories(packageDir);
String simpleName = fqn.substring(fqn.lastIndexOf('.') + 1);
try (Writer writer = Files.newBufferedWriter(packageDir.resolve(simpleName + ".java"))) {
writer.write(content);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Loading