2323
2424import java .lang .annotation .Annotation ;
2525import java .lang .reflect .Field ;
26+ import java .lang .reflect .Method ;
2627import java .text .MessageFormat ;
2728import java .util .ArrayList ;
2829import 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