Skip to content

Commit 3c66bd2

Browse files
Copilotanidotnet
andcommitted
Add support for interface entity types in repositories
- Modified Reflector to detect and handle interface properties via getter methods - Created InterfacePropertyHolder to store metadata for synthetic fields - Updated EntityDecoratorScanner and AnnotationScanner to use property metadata - Fixed Android compatibility by using getParameterTypes().length instead of getParameterCount() - Added test case for interface entity support Co-authored-by: anidotnet <[email protected]>
1 parent 0e27c8f commit 3c66bd2

File tree

5 files changed

+383
-4
lines changed

5 files changed

+383
-4
lines changed

nitrite/src/main/java/org/dizitart/no2/repository/AnnotationScanner.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,10 @@ private void populateIndex(List<Index> indexList) {
156156
Field field = reflector.getField(type, name);
157157
if (field != null) {
158158
entityFields.add(field);
159-
indexValidator.validate(field.getType(), field.getName(), nitriteMapper);
159+
// Use InterfacePropertyHolder to get correct name and type for interface properties
160+
String fieldName = InterfacePropertyHolder.getPropertyName(field);
161+
Class<?> fieldType = InterfacePropertyHolder.getPropertyType(field);
162+
indexValidator.validate(fieldType, fieldName, nitriteMapper);
160163
}
161164
}
162165

nitrite/src/main/java/org/dizitart/no2/repository/EntityDecoratorScanner.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ private void readIndices() {
8989
Field field = reflector.getField(entityDecorator.getEntityType(), name);
9090
if (field != null) {
9191
entityFields.add(field);
92-
indexValidator.validate(field.getType(), field.getName(), nitriteMapper);
92+
// Use InterfacePropertyHolder to get correct name and type for interface properties
93+
String fieldName = InterfacePropertyHolder.getPropertyName(field);
94+
Class<?> fieldType = InterfacePropertyHolder.getPropertyType(field);
95+
indexValidator.validate(fieldType, fieldName, nitriteMapper);
9396
}
9497
}
9598

@@ -108,7 +111,9 @@ private void readIdField() {
108111
String idFieldName = entityId.getFieldName();
109112
if (!StringUtils.isNullOrEmpty(idFieldName)) {
110113
Field field = reflector.getField(entityDecorator.getEntityType(), idFieldName);
111-
indexValidator.validateId(entityId, field.getType(), idFieldName, nitriteMapper);
114+
// Use InterfacePropertyHolder to get correct type for interface properties
115+
Class<?> fieldType = InterfacePropertyHolder.getPropertyType(field);
116+
indexValidator.validateId(entityId, fieldType, idFieldName, nitriteMapper);
112117

113118
objectIdField = new ObjectIdField();
114119
objectIdField.setField(field);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) 2017-2022 Nitrite author or 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+
* http://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+
*/
17+
18+
package org.dizitart.no2.repository;
19+
20+
import java.lang.reflect.Field;
21+
import java.lang.reflect.Method;
22+
import java.util.Map;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
25+
/**
26+
* Holder class for interface property metadata.
27+
* Stores mapping between template fields and actual interface properties.
28+
*
29+
* @author Anindya Chatterjee
30+
* @since 4.3.2
31+
*/
32+
class InterfacePropertyHolder {
33+
// Template field used for interface properties
34+
Object property;
35+
36+
// Maps template fields to their actual property metadata
37+
private static final Map<Field, PropertyMetadata> propertyRegistry = new ConcurrentHashMap<>();
38+
39+
/**
40+
* Registers a synthetic field with its actual property metadata
41+
*/
42+
static void registerProperty(Field templateField, String propertyName, Method getterMethod) {
43+
propertyRegistry.put(templateField, new PropertyMetadata(propertyName, getterMethod));
44+
}
45+
46+
/**
47+
* Gets the actual property name for a field (handles both real and synthetic fields)
48+
*/
49+
static String getPropertyName(Field field) {
50+
PropertyMetadata metadata = propertyRegistry.get(field);
51+
return metadata != null ? metadata.propertyName : field.getName();
52+
}
53+
54+
/**
55+
* Gets the actual property type for a field (handles both real and synthetic fields)
56+
*/
57+
static Class<?> getPropertyType(Field field) {
58+
PropertyMetadata metadata = propertyRegistry.get(field);
59+
return metadata != null ? metadata.getterMethod.getReturnType() : field.getType();
60+
}
61+
62+
/**
63+
* Checks if a field is a synthetic interface property field
64+
*/
65+
static boolean isInterfaceProperty(Field field) {
66+
return propertyRegistry.containsKey(field);
67+
}
68+
69+
/**
70+
* Metadata about an interface property
71+
*/
72+
private static class PropertyMetadata {
73+
final String propertyName;
74+
final Method getterMethod;
75+
76+
PropertyMetadata(String propertyName, Method getterMethod) {
77+
this.propertyName = propertyName;
78+
this.getterMethod = getterMethod;
79+
}
80+
}
81+
}

nitrite/src/main/java/org/dizitart/no2/repository/Reflector.java

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import java.lang.annotation.Annotation;
2525
import java.lang.reflect.Field;
26+
import java.lang.reflect.Method;
2627
import java.text.MessageFormat;
2728
import java.util.ArrayList;
2829
import java.util.Arrays;
@@ -75,7 +76,15 @@ public <T> Field getEmbeddedField(Class<T> startingClass, String embeddedField)
7576
try {
7677
field = startingClass.getDeclaredField(key);
7778
} catch (NoSuchFieldException e) {
78-
throw new ValidationException("No such field '" + key + "' for type " + startingClass.getName(), e);
79+
// If it's an interface, try to find the property from getter method
80+
if (startingClass.isInterface()) {
81+
field = getFieldFromInterfaceProperty(startingClass, key);
82+
if (field == null) {
83+
throw new ValidationException("No such field '" + key + "' for type " + startingClass.getName(), e);
84+
}
85+
} else {
86+
throw new ValidationException("No such field '" + key + "' for type " + startingClass.getName(), e);
87+
}
7988
}
8089

8190
if (!isNullOrEmpty(remaining) || remaining.contains(NitriteConfig.getFieldSeparator())) {
@@ -123,6 +132,12 @@ public <T> Field getField(Class<T> type, String name) {
123132
}
124133
}
125134
}
135+
136+
// If still not found and type is an interface, try to find from getter methods
137+
if (field == null && type.isInterface()) {
138+
field = getFieldFromInterfaceProperty(type, name);
139+
}
140+
126141
if (field == null) {
127142
throw new ValidationException("No such field '" + name + "' for type " + type.getName());
128143
}
@@ -137,8 +152,125 @@ public <T> List<Field> getAllFields(Class<T> type) {
137152
} else {
138153
fields = Arrays.asList(type.getDeclaredFields());
139154
}
155+
156+
// If type is an interface and has no fields, try to get fields from interface properties
157+
if (fields.isEmpty() && type.isInterface()) {
158+
fields = getFieldsFromInterfaceProperties(type);
159+
}
160+
140161
return fields;
141162
}
163+
164+
/**
165+
* Extracts property information from interface getter methods and creates a synthetic Field.
166+
* This is used to support interface entity types where properties are defined as getter methods.
167+
*
168+
* The returned Field is from a template class and is used primarily for type information.
169+
* Actual field access (get/set) on runtime objects works because those are concrete
170+
* implementations with real fields.
171+
*
172+
* @param interfaceType the interface class
173+
* @param propertyName the property name
174+
* @return a Field object representing the property, or null if not found
175+
*/
176+
private <T> Field getFieldFromInterfaceProperty(Class<T> interfaceType, String propertyName) {
177+
if (!interfaceType.isInterface()) {
178+
return null;
179+
}
180+
181+
// Look for getter methods matching the property name
182+
Method[] methods = interfaceType.getMethods();
183+
for (Method method : methods) {
184+
String methodName = method.getName();
185+
186+
// Check for standard getter patterns: getXxx() or isXxx()
187+
String extractedPropertyName = null;
188+
if (methodName.startsWith("get") && methodName.length() > 3 && method.getParameterTypes().length == 0) {
189+
extractedPropertyName = decapitalize(methodName.substring(3));
190+
} else if (methodName.startsWith("is") && methodName.length() > 2 && method.getParameterTypes().length == 0) {
191+
extractedPropertyName = decapitalize(methodName.substring(2));
192+
}
193+
194+
if (propertyName.equals(extractedPropertyName)) {
195+
// Found matching getter - create a wrapper field
196+
// For now, we return null and will handle this case specially
197+
// Actually, let's throw a more helpful error with a workaround suggestion
198+
return createSyntheticFieldForProperty(propertyName, method);
199+
}
200+
}
201+
202+
return null;
203+
}
204+
205+
/**
206+
* Creates a synthetic field representation for an interface property.
207+
* Uses a template field from InterfacePropertyHolder and wraps it to provide
208+
* correct type information.
209+
*/
210+
private Field createSyntheticFieldForProperty(String propertyName, Method getterMethod) {
211+
try {
212+
// Use a generic Object field as a template
213+
// The name won't match but the infrastructure can work around it
214+
Field templateField = InterfacePropertyHolder.class.getDeclaredField("property");
215+
// Store metadata about this field for later use
216+
InterfacePropertyHolder.registerProperty(templateField, propertyName, getterMethod);
217+
return templateField;
218+
} catch (NoSuchFieldException e) {
219+
return null;
220+
}
221+
}
222+
223+
/**
224+
* Extracts all property information from interface getter methods.
225+
*
226+
* @param interfaceType the interface class
227+
* @return list of Field objects representing interface properties
228+
*/
229+
private <T> List<Field> getFieldsFromInterfaceProperties(Class<T> interfaceType) {
230+
List<Field> fields = new ArrayList<>();
231+
if (!interfaceType.isInterface()) {
232+
return fields;
233+
}
234+
235+
Method[] methods = interfaceType.getMethods();
236+
for (Method method : methods) {
237+
String methodName = method.getName();
238+
239+
// Check for standard getter patterns: getXxx() or isXxx()
240+
String propertyName = null;
241+
if (methodName.startsWith("get") && methodName.length() > 3 && method.getParameterTypes().length == 0) {
242+
propertyName = decapitalize(methodName.substring(3));
243+
} else if (methodName.startsWith("is") && methodName.length() > 2 && method.getParameterTypes().length == 0) {
244+
propertyName = decapitalize(methodName.substring(2));
245+
}
246+
247+
if (propertyName != null) {
248+
Field syntheticField = createSyntheticFieldForProperty(propertyName, method);
249+
if (syntheticField != null) {
250+
fields.add(syntheticField);
251+
}
252+
}
253+
}
254+
255+
return fields;
256+
}
257+
258+
/**
259+
* Decapitalizes a string (makes first character lowercase).
260+
* Used to convert getter method names to property names.
261+
*/
262+
private String decapitalize(String name) {
263+
if (name == null || name.isEmpty()) {
264+
return name;
265+
}
266+
// Follow JavaBeans convention: if first two chars are uppercase, don't decapitalize
267+
if (name.length() > 1 && Character.isUpperCase(name.charAt(0)) && Character.isUpperCase(name.charAt(1))) {
268+
return name;
269+
}
270+
char[] chars = name.toCharArray();
271+
chars[0] = Character.toLowerCase(chars[0]);
272+
return new String(chars);
273+
}
142274

143275
private void filterSynthetics(List<Field> fields) {
144276
if (fields == null || fields.isEmpty()) return;

0 commit comments

Comments
 (0)