Skip to content

Commit 04ad364

Browse files
authored
feat: Add support for optional embeddable classes (#1409)
2 parents 5892431 + 812b80e commit 04ad364

File tree

37 files changed

+1498
-150
lines changed

37 files changed

+1498
-150
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright Doma Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.seasar.doma.internal.jdbc.entity;
17+
18+
import org.seasar.doma.internal.WrapException;
19+
20+
/**
21+
* A wrapper interface for accessing and manipulating object fields.
22+
*
23+
* <p>This interface provides a unified way to get and set field values on objects, abstracting the
24+
* underlying reflection operations. It handles different types of fields including regular fields
25+
* and optional fields, providing type-safe access patterns.
26+
*
27+
* <p>Implementations of this interface are typically created by {@link PropertyPathSegment}
28+
* implementations to handle field access for specific property path segments.
29+
*
30+
* <p>This interface is used internally by the Doma framework for property mapping and should not be
31+
* used directly by application code.
32+
*
33+
* @see PropertyPathSegment
34+
* @see PropertyPath
35+
*/
36+
public interface FieldWrapper {
37+
/**
38+
* Retrieves the value of this field from the specified object.
39+
*
40+
* @param obj the object from which to retrieve the field value
41+
* @return the field value, which may be null
42+
* @throws WrapException if an error occurs during field access
43+
*/
44+
Object get(Object obj) throws WrapException;
45+
46+
/**
47+
* Sets the value of this field on the specified object.
48+
*
49+
* @param obj the object on which to set the field value
50+
* @param value the value to set, which may be null
51+
* @throws WrapException if an error occurs during field access
52+
*/
53+
void set(Object obj, Object value) throws WrapException;
54+
55+
/**
56+
* Returns the type of this field.
57+
*
58+
* <p>For optional fields, this returns the element type rather than the Optional type itself.
59+
*
60+
* @return the field type
61+
*/
62+
Class<?> getType();
63+
}

doma-core/src/main/java/org/seasar/doma/internal/jdbc/entity/PropertyField.java

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.util.LinkedList;
2020
import org.seasar.doma.internal.WrapException;
2121
import org.seasar.doma.internal.util.AssertionUtil;
22-
import org.seasar.doma.internal.util.FieldUtil;
2322
import org.seasar.doma.jdbc.entity.EntityPropertyAccessException;
2423
import org.seasar.doma.jdbc.entity.EntityPropertyNotFoundException;
2524

@@ -29,35 +28,36 @@ public class PropertyField<ENTITY> {
2928

3029
protected final Class<ENTITY> entityClass;
3130

32-
protected final LinkedList<Field> fields = new LinkedList<>();
31+
protected final LinkedList<FieldWrapper> fields = new LinkedList<>();
3332

3433
public PropertyField(String path, Class<ENTITY> entityClass) {
34+
this(PropertyPath.of(path), entityClass);
35+
}
36+
37+
public PropertyField(PropertyPath path, Class<ENTITY> entityClass) {
3538
AssertionUtil.assertNotNull(path, entityClass);
36-
this.path = path;
39+
this.path = path.name();
3740
this.entityClass = entityClass;
38-
String[] segments = path.split("\\.");
3941
Class<?> clazz = entityClass;
40-
for (String segment : segments) {
41-
Field field = getField(clazz, segment);
42+
for (PropertyPathSegment segment : path.segments()) {
43+
FieldWrapper field = getField(clazz, segment);
4244
fields.add(field);
4345
clazz = field.getType();
4446
}
4547
AssertionUtil.assertTrue(fields.size() > 0);
4648
}
4749

48-
private Field getField(Class<?> clazz, String name) {
49-
Field field = findField(clazz, name);
50+
private FieldWrapper getField(Class<?> clazz, PropertyPathSegment segment) {
51+
Field field = findField(clazz, segment.name());
5052
if (field == null) {
51-
throw new EntityPropertyNotFoundException(clazz.getName(), name);
53+
throw new EntityPropertyNotFoundException(clazz.getName(), segment.name());
5254
}
53-
if (!FieldUtil.isPublic(field)) {
54-
try {
55-
FieldUtil.setAccessible(field, true);
56-
} catch (WrapException wrapException) {
57-
throw new EntityPropertyAccessException(wrapException.getCause(), clazz.getName(), name);
58-
}
55+
try {
56+
return segment.wrapField(field);
57+
} catch (WrapException wrapException) {
58+
throw new EntityPropertyAccessException(
59+
wrapException.getCause(), clazz.getName(), segment.name());
5960
}
60-
return field;
6161
}
6262

6363
private Field findField(Class<?> clazz, String name) {
@@ -73,7 +73,7 @@ private Field findField(Class<?> clazz, String name) {
7373
public Object getValue(ENTITY entity) {
7474
AssertionUtil.assertNotNull(entity);
7575
Object value = entity;
76-
for (Field field : fields) {
76+
for (FieldWrapper field : fields) {
7777
if (value == null) {
7878
break;
7979
}
@@ -82,9 +82,9 @@ public Object getValue(ENTITY entity) {
8282
return value;
8383
}
8484

85-
private Object getFieldValue(Field field, Object target) {
85+
private Object getFieldValue(FieldWrapper field, Object target) {
8686
try {
87-
return FieldUtil.get(field, target);
87+
return field.get(target);
8888
} catch (WrapException wrapException) {
8989
throw new EntityPropertyAccessException(
9090
wrapException.getCause(), entityClass.getName(), path);
@@ -99,17 +99,17 @@ public void setValue(ENTITY entity, Object value) {
9999
setFieldValue(fields.getFirst(), entity, value);
100100
}
101101

102-
private void setFieldValue(Field field, ENTITY entity, Object value) {
102+
private void setFieldValue(FieldWrapper field, ENTITY entity, Object value) {
103103
try {
104-
FieldUtil.set(field, entity, value);
104+
field.set(entity, value);
105105
} catch (WrapException wrapException) {
106106
throw new EntityPropertyAccessException(
107107
wrapException.getCause(), entityClass.getName(), path);
108108
}
109109
}
110110

111111
public boolean isPrimitive() {
112-
Field field = fields.getLast();
112+
FieldWrapper field = fields.getLast();
113113
return field.getType().isPrimitive();
114114
}
115115
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright Doma Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.seasar.doma.internal.jdbc.entity;
17+
18+
import java.util.Arrays;
19+
import java.util.List;
20+
import java.util.Objects;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.Stream;
23+
24+
/**
25+
* Represents a path to a property within an object graph, composed of one or more segments.
26+
*
27+
* <p>A property path is used to navigate through object hierarchies, particularly when dealing with
28+
* embeddable objects that may be nested within entities. Each segment of the path represents a step
29+
* in the navigation from one object to another.
30+
*
31+
* <p>For example, a property path like "address.street" would consist of two segments: one for
32+
* "address" and another for "street", allowing navigation from an entity to its embedded address
33+
* object and then to the street field within that address.
34+
*
35+
* <p>This record is immutable and provides factory methods for creating and combining paths.
36+
*
37+
* <p>This class is used internally by the Doma framework for property mapping and should not be
38+
* used directly by application code.
39+
*
40+
* @param segments the list of path segments that make up this property path, must not be empty
41+
* @see PropertyPathSegment
42+
* @see PropertyField
43+
*/
44+
public record PropertyPath(List<? extends PropertyPathSegment> segments) {
45+
public PropertyPath {
46+
if (segments.isEmpty()) {
47+
throw new IllegalArgumentException("segments");
48+
}
49+
}
50+
51+
/**
52+
* Returns the full name of this property path as a dot-separated string.
53+
*
54+
* <p>For example, if this path has segments "address" and "street", this method would return
55+
* "address.street".
56+
*
57+
* @return the full property path name with segments joined by dots
58+
*/
59+
public String name() {
60+
return segments.stream().map(PropertyPathSegment::name).collect(Collectors.joining("."));
61+
}
62+
63+
/**
64+
* Creates a new property path by combining an existing path with an additional segment.
65+
*
66+
* <p>This method allows for building longer property paths by appending segments to existing
67+
* paths. The original path is not modified; a new immutable path is returned.
68+
*
69+
* @param path the existing property path to extend
70+
* @param segment the segment to append to the path
71+
* @return a new property path containing all segments from the original path plus the new segment
72+
* @throws NullPointerException if either path or segment is null
73+
*/
74+
public static PropertyPath combine(PropertyPath path, PropertyPathSegment segment) {
75+
Objects.requireNonNull(path);
76+
Objects.requireNonNull(segment);
77+
var segments = Stream.concat(path.segments.stream(), Stream.of(segment)).toList();
78+
return new PropertyPath(segments);
79+
}
80+
81+
/**
82+
* Creates a property path from a dot-separated string representation.
83+
*
84+
* <p>This factory method parses a string like "address.street" into a property path with
85+
* appropriate segments. Each segment is created as a default property path segment.
86+
*
87+
* @param path the dot-separated string representation of the property path
88+
* @return a new property path with segments parsed from the string
89+
* @throws NullPointerException if path is null
90+
*/
91+
public static PropertyPath of(String path) {
92+
Objects.requireNonNull(path);
93+
var segments = Arrays.stream(path.split("\\.")).map(PropertyPathSegment.Default::new).toList();
94+
return new PropertyPath(segments);
95+
}
96+
97+
/**
98+
* Returns the string representation of this property path.
99+
*
100+
* <p>This is equivalent to calling {@link #name()}.
101+
*
102+
* @return the full property path name with segments joined by dots
103+
*/
104+
@Override
105+
public String toString() {
106+
return name();
107+
}
108+
}

0 commit comments

Comments
 (0)