diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 43e7a2d7e..57f122f5c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,7 +10,7 @@ Nitrite Database is an open source embedded NoSQL database for Java. It's a mult - Extensible storage engines (MVStore, RocksDB) - Full-text search and indexing - Transaction support -- Android compatibility (API Level 24+) +- Android compatibility (API Level 26+) ## Repository Structure diff --git a/README.md b/README.md index 7e0881c00..975972875 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Nitrite is an embedded database ideal for desktop, mobile or small web applicati - Transaction support - Schema migration support - Encryption support -- Android compatibility (API Level 24) +- Android compatibility (API Level 26) ## Kotlin Extension diff --git a/nitrite/src/main/java/org/dizitart/no2/repository/AnnotationScanner.java b/nitrite/src/main/java/org/dizitart/no2/repository/AnnotationScanner.java index ec44bad6d..6c6ebdb4a 100644 --- a/nitrite/src/main/java/org/dizitart/no2/repository/AnnotationScanner.java +++ b/nitrite/src/main/java/org/dizitart/no2/repository/AnnotationScanner.java @@ -156,7 +156,10 @@ private void populateIndex(List indexList) { Field field = reflector.getField(type, name); if (field != null) { entityFields.add(field); - indexValidator.validate(field.getType(), field.getName(), nitriteMapper); + // Use InterfacePropertyHolder to get correct name and type for interface properties + String fieldName = InterfacePropertyHolder.getPropertyName(field); + Class fieldType = InterfacePropertyHolder.getPropertyType(field); + indexValidator.validate(fieldType, fieldName, nitriteMapper); } } diff --git a/nitrite/src/main/java/org/dizitart/no2/repository/EntityDecoratorScanner.java b/nitrite/src/main/java/org/dizitart/no2/repository/EntityDecoratorScanner.java index 426c2a2cc..8e9d09615 100644 --- a/nitrite/src/main/java/org/dizitart/no2/repository/EntityDecoratorScanner.java +++ b/nitrite/src/main/java/org/dizitart/no2/repository/EntityDecoratorScanner.java @@ -89,7 +89,10 @@ private void readIndices() { Field field = reflector.getField(entityDecorator.getEntityType(), name); if (field != null) { entityFields.add(field); - indexValidator.validate(field.getType(), field.getName(), nitriteMapper); + // Use InterfacePropertyHolder to get correct name and type for interface properties + String fieldName = InterfacePropertyHolder.getPropertyName(field); + Class fieldType = InterfacePropertyHolder.getPropertyType(field); + indexValidator.validate(fieldType, fieldName, nitriteMapper); } } @@ -108,7 +111,9 @@ private void readIdField() { String idFieldName = entityId.getFieldName(); if (!StringUtils.isNullOrEmpty(idFieldName)) { Field field = reflector.getField(entityDecorator.getEntityType(), idFieldName); - indexValidator.validateId(entityId, field.getType(), idFieldName, nitriteMapper); + // Use InterfacePropertyHolder to get correct type for interface properties + Class fieldType = InterfacePropertyHolder.getPropertyType(field); + indexValidator.validateId(entityId, fieldType, idFieldName, nitriteMapper); objectIdField = new ObjectIdField(); objectIdField.setField(field); diff --git a/nitrite/src/main/java/org/dizitart/no2/repository/FieldAccessHelper.java b/nitrite/src/main/java/org/dizitart/no2/repository/FieldAccessHelper.java new file mode 100644 index 000000000..cdc0dbbf3 --- /dev/null +++ b/nitrite/src/main/java/org/dizitart/no2/repository/FieldAccessHelper.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2017-2022 Nitrite 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 + * + * http://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.dizitart.no2.repository; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Helper class to access field values, handling both regular fields and interface properties. + * For interface properties (synthetic fields from InterfacePropertyHolder), this class + * finds and accesses the actual field on the concrete implementation object. + *

+ * Uses MethodHandles for improved security and performance (Android API 26+). + * + * @author Anindya Chatterjee + * @since 4.3.2 + */ +class FieldAccessHelper { + + /** + * Gets the value of a field from an object, handling both regular and interface property fields. + * Uses MethodHandles for improved security and performance (Android API 26+). + * + * @param field the field to access + * @param obj the object to get the value from + * @return the field value + * @throws IllegalAccessException if field access fails + */ + static Object get(Field field, Object obj) throws IllegalAccessException { + if (InterfacePropertyHolder.isInterfaceProperty(field)) { + // This is a synthetic field for an interface property + // Find and access the real field in the concrete object + String propertyName = InterfacePropertyHolder.getPropertyName(field); + return getPropertyValue(obj, propertyName); + } else { + return getFieldValue(field, obj); + } + } + + /** + * Sets the value of a field on an object, handling both regular and interface property fields. + * Uses MethodHandles for improved security and performance (Android API 26+). + * + * @param field the field to set + * @param obj the object to set the value on + * @param value the value to set + * @throws IllegalAccessException if field access fails + */ + static void set(Field field, Object obj, Object value) throws IllegalAccessException { + if (InterfacePropertyHolder.isInterfaceProperty(field)) { + // This is a synthetic field for an interface property + // Find and set the real field in the concrete object + String propertyName = InterfacePropertyHolder.getPropertyName(field); + setPropertyValue(obj, propertyName, value); + } else { + setFieldValue(field, obj, value); + } + } + + /** + * Gets the value of a field using MethodHandles for secure access. + * Uses unreflect approach compatible with Android API 26+. + */ + private static Object getFieldValue(Field field, Object obj) throws IllegalAccessException { + try { + // Use MethodHandles.lookup().unreflect which is available since Android API 26 + // This is more secure than setAccessible but requires the field to be accessible + // while MethodHandles provide a more modern API, setAccessible is still required to bypass access checks. + field.setAccessible(true); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle getter = lookup.unreflectGetter(field); + return getter.invokeWithArguments(obj); + } catch (Throwable e) { + throw new IllegalAccessException("Cannot access field " + field.getName() + ": " + e.getMessage()); + } + } + + /** + * Sets the value of a field using MethodHandles for secure access. + * Uses unreflect approach compatible with Android API 26+. + */ + private static void setFieldValue(Field field, Object obj, Object value) throws IllegalAccessException { + try { + // Use MethodHandles.lookup().unreflect which is available since Android API 26 + // This is more secure than direct setAccessible + set + // while MethodHandles provide a more modern API, setAccessible is still required to bypass access checks. + field.setAccessible(true); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle setter = lookup.unreflectSetter(field); + setter.invokeWithArguments(obj, value); + } catch (Throwable e) { + throw new IllegalAccessException("Cannot set field " + field.getName() + ": " + e.getMessage()); + } + } + + /** + * Gets a property value from an object, trying both field access and getter method. + */ + private static Object getPropertyValue(Object obj, String propertyName) throws IllegalAccessException { + if (propertyName == null || propertyName.isEmpty()) { + throw new IllegalAccessException("Property name cannot be null or empty"); + } + + // Try to find the field in the object's class + Field realField = findFieldInHierarchy(obj.getClass(), propertyName); + if (realField != null) { + return getFieldValue(realField, obj); + } + + // Fall back to getter method - try both 'get' and 'is' prefixes + try { + Method getter = findGetterMethod(obj.getClass(), propertyName); + if (getter != null) { + return invokeMethod(getter, obj); + } + throw new IllegalAccessException("No getter method found for property '" + propertyName + "'"); + } catch (Exception e) { + throw new IllegalAccessException("Cannot access property '" + propertyName + "' on " + obj.getClass().getName() + ": " + e.getMessage()); + } + } + + /** + * Sets a property value on an object, trying both field access and setter method. + */ + private static void setPropertyValue(Object obj, String propertyName, Object value) throws IllegalAccessException { + if (propertyName == null || propertyName.isEmpty()) { + throw new IllegalAccessException("Property name cannot be null or empty"); + } + + // Try to find the field in the object's class + Field realField = findFieldInHierarchy(obj.getClass(), propertyName); + if (realField != null) { + setFieldValue(realField, obj, value); + return; + } + + // Fall back to setter method + try { + String setterName = "set" + capitalizePropertyName(propertyName); + Method setter = findSetterMethod(obj.getClass(), setterName, value); + if (setter != null) { + invokeMethod(setter, obj, value); + } else { + throw new IllegalAccessException("No setter method found for property '" + propertyName + "'"); + } + } catch (Exception e) { + throw new IllegalAccessException("Cannot set property '" + propertyName + "' on " + obj.getClass().getName() + ": " + e.getMessage()); + } + } + + /** + * Invokes a method using MethodHandles for improved security. + * Uses unreflect approach compatible with Android API 26+. + */ + private static Object invokeMethod(Method method, Object obj, Object... args) throws IllegalAccessException { + try { + // Use MethodHandles.lookup().unreflect which is available since Android API 26 + method.setAccessible(true); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle methodHandle = lookup.unreflect(method); + if (args.length == 0) { + return methodHandle.invokeWithArguments(obj); + } else { + return methodHandle.invokeWithArguments(obj, args[0]); + } + } catch (IllegalAccessException e) { + throw e; + } catch (Throwable e) { + throw new IllegalAccessException("Cannot invoke method " + method.getName() + ": " + e.getMessage()); + } + } + + /** + * Finds a getter method for a property (tries both 'get' and 'is' prefixes). + */ + private static Method findGetterMethod(Class clazz, String propertyName) { + String capitalizedName = capitalizePropertyName(propertyName); + String getterName = "get" + capitalizedName; + String isGetterName = "is" + capitalizedName; + + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if ((method.getName().equals(getterName) || method.getName().equals(isGetterName)) + && method.getParameterTypes().length == 0) { + return method; + } + } + return null; + } + + /** + * Capitalizes a property name following JavaBeans conventions. + */ + private static String capitalizePropertyName(String propertyName) { + if (propertyName == null || propertyName.isEmpty()) { + return propertyName; + } + // Follow JavaBeans convention: if first two chars are uppercase, don't change + if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(0)) && Character.isUpperCase(propertyName.charAt(1))) { + return propertyName; + } + return Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); + } + + /** + * Finds a field in the class hierarchy. + */ + private static Field findFieldInHierarchy(Class clazz, String fieldName) { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + return null; + } + + /** + * Finds a setter method that can accept the given value. + */ + private static Method findSetterMethod(Class clazz, String setterName, Object value) { + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getName().equals(setterName) && method.getParameterTypes().length == 1) { + Class paramType = method.getParameterTypes()[0]; + if (value == null || paramType.isAssignableFrom(value.getClass()) || + (paramType.isPrimitive() && isCompatiblePrimitive(paramType, value.getClass()))) { + return method; + } + } + } + return null; + } + + /** + * Checks if a value class is compatible with a primitive parameter type. + */ + private static boolean isCompatiblePrimitive(Class primitiveType, Class valueClass) { + // Use a more efficient lookup instead of cascading if statements + return (primitiveType == int.class && valueClass == Integer.class) + || (primitiveType == long.class && valueClass == Long.class) + || (primitiveType == double.class && valueClass == Double.class) + || (primitiveType == float.class && valueClass == Float.class) + || (primitiveType == boolean.class && valueClass == Boolean.class) + || (primitiveType == byte.class && valueClass == Byte.class) + || (primitiveType == short.class && valueClass == Short.class) + || (primitiveType == char.class && valueClass == Character.class); + } +} diff --git a/nitrite/src/main/java/org/dizitart/no2/repository/InterfacePropertyHolder.java b/nitrite/src/main/java/org/dizitart/no2/repository/InterfacePropertyHolder.java new file mode 100644 index 000000000..42692f07b --- /dev/null +++ b/nitrite/src/main/java/org/dizitart/no2/repository/InterfacePropertyHolder.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2017-2022 Nitrite 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 + * + * http://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.dizitart.no2.repository; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Holder class for interface property metadata. + * Stores mapping between template fields and actual interface properties. + * + * @author Anindya Chatterjee + * @since 4.3.2 + */ +class InterfacePropertyHolder { + // Template field used for interface properties + Object property; + + // Maps template fields to their actual property metadata + private static final Map propertyRegistry = new ConcurrentHashMap<>(); + + /** + * Registers a synthetic field with its actual property metadata + */ + static void registerProperty(Field templateField, String propertyName, Method getterMethod) { + propertyRegistry.put(templateField, new PropertyMetadata(propertyName, getterMethod)); + } + + /** + * Gets the actual property name for a field (handles both real and synthetic fields) + */ + static String getPropertyName(Field field) { + PropertyMetadata metadata = propertyRegistry.get(field); + return metadata != null ? metadata.propertyName : field.getName(); + } + + /** + * Gets the actual property type for a field (handles both real and synthetic fields) + */ + static Class getPropertyType(Field field) { + PropertyMetadata metadata = propertyRegistry.get(field); + return metadata != null ? metadata.getterMethod.getReturnType() : field.getType(); + } + + /** + * Checks if a field is a synthetic interface property field + */ + static boolean isInterfaceProperty(Field field) { + return propertyRegistry.containsKey(field); + } + + /** + * Metadata about an interface property + */ + private static class PropertyMetadata { + final String propertyName; + final Method getterMethod; + + PropertyMetadata(String propertyName, Method getterMethod) { + this.propertyName = propertyName; + this.getterMethod = getterMethod; + } + } +} diff --git a/nitrite/src/main/java/org/dizitart/no2/repository/Reflector.java b/nitrite/src/main/java/org/dizitart/no2/repository/Reflector.java index 7e89e0cff..40307da02 100644 --- a/nitrite/src/main/java/org/dizitart/no2/repository/Reflector.java +++ b/nitrite/src/main/java/org/dizitart/no2/repository/Reflector.java @@ -23,6 +23,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -75,7 +76,15 @@ public Field getEmbeddedField(Class startingClass, String embeddedField) try { field = startingClass.getDeclaredField(key); } catch (NoSuchFieldException e) { - throw new ValidationException("No such field '" + key + "' for type " + startingClass.getName(), e); + // If it's an interface, try to find the property from getter method + if (startingClass.isInterface()) { + field = getFieldFromInterfaceProperty(startingClass, key); + if (field == null) { + throw new ValidationException("No such field '" + key + "' for type " + startingClass.getName(), e); + } + } else { + throw new ValidationException("No such field '" + key + "' for type " + startingClass.getName(), e); + } } if (!isNullOrEmpty(remaining) || remaining.contains(NitriteConfig.getFieldSeparator())) { @@ -123,6 +132,12 @@ public Field getField(Class type, String name) { } } } + + // If still not found and type is an interface, try to find from getter methods + if (field == null && type.isInterface()) { + field = getFieldFromInterfaceProperty(type, name); + } + if (field == null) { throw new ValidationException("No such field '" + name + "' for type " + type.getName()); } @@ -137,8 +152,127 @@ public List getAllFields(Class type) { } else { fields = Arrays.asList(type.getDeclaredFields()); } + + // If type is an interface and has no fields, try to get fields from interface properties + if (fields.isEmpty() && type.isInterface()) { + fields = getFieldsFromInterfaceProperties(type); + } + return fields; } + + /** + * Extracts property information from interface getter methods and creates a synthetic Field. + * This is used to support interface entity types where properties are defined as getter methods. + * + * The returned Field is from a template class and is used primarily for type information. + * Actual field access (get/set) on runtime objects works because those are concrete + * implementations with real fields. + * + * @param interfaceType the interface class + * @param propertyName the property name + * @return a Field object representing the property, or null if not found + */ + private Field getFieldFromInterfaceProperty(Class interfaceType, String propertyName) { + if (!interfaceType.isInterface()) { + return null; + } + + // Look for getter methods matching the property name + Method[] methods = interfaceType.getMethods(); + for (Method method : methods) { + String methodName = method.getName(); + + // Check for standard getter patterns: getXxx() or isXxx() + String extractedPropertyName = null; + if (methodName.startsWith("get") && methodName.length() > 3 && method.getParameterTypes().length == 0) { + extractedPropertyName = decapitalize(methodName.substring(3)); + } else if (methodName.startsWith("is") && methodName.length() > 2 && method.getParameterTypes().length == 0) { + extractedPropertyName = decapitalize(methodName.substring(2)); + } + + if (propertyName.equals(extractedPropertyName)) { + // Found matching getter - create a wrapper field + return createSyntheticFieldForProperty(propertyName, method); + } + } + + return null; + } + + /** + * Creates a synthetic field representation for an interface property. + * Uses a template field from InterfacePropertyHolder and wraps it to provide + * correct type information. + */ + private Field createSyntheticFieldForProperty(String propertyName, Method getterMethod) { + try { + // Use a generic Object field as a template. + // The field name ("property") does not match the actual property name, + // but InterfacePropertyHolder.registerProperty stores the mapping between + // the template field, the actual property name, and the getter method. + // This allows the infrastructure to later retrieve the correct property + // information for interface-based repositories, even though the field name is generic. + Field templateField = InterfacePropertyHolder.class.getDeclaredField("property"); + // Store metadata about this field for later use + InterfacePropertyHolder.registerProperty(templateField, propertyName, getterMethod); + return templateField; + } catch (NoSuchFieldException e) { + return null; + } + } + + /** + * Extracts all property information from interface getter methods. + * + * @param interfaceType the interface class + * @return list of Field objects representing interface properties + */ + private List getFieldsFromInterfaceProperties(Class interfaceType) { + List fields = new ArrayList<>(); + if (!interfaceType.isInterface()) { + return fields; + } + + Method[] methods = interfaceType.getMethods(); + for (Method method : methods) { + String methodName = method.getName(); + + // Check for standard getter patterns: getXxx() or isXxx() + String propertyName = null; + if (methodName.startsWith("get") && methodName.length() > 3 && method.getParameterTypes().length == 0) { + propertyName = decapitalize(methodName.substring(3)); + } else if (methodName.startsWith("is") && methodName.length() > 2 && method.getParameterTypes().length == 0) { + propertyName = decapitalize(methodName.substring(2)); + } + + if (propertyName != null) { + Field syntheticField = createSyntheticFieldForProperty(propertyName, method); + if (syntheticField != null) { + fields.add(syntheticField); + } + } + } + + return fields; + } + + /** + * Decapitalizes a string (makes first character lowercase). + * Used to convert getter method names to property names. + */ + private String decapitalize(String name) { + if (name == null || name.isEmpty()) { + return name; + } + // Follow JavaBeans convention: if first two chars are uppercase, don't decapitalize + if (name.length() > 1 && Character.isUpperCase(name.charAt(0)) && Character.isUpperCase(name.charAt(1))) { + return name; + } + char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } private void filterSynthetics(List fields) { if (fields == null || fields.isEmpty()) return; diff --git a/nitrite/src/main/java/org/dizitart/no2/repository/RepositoryOperations.java b/nitrite/src/main/java/org/dizitart/no2/repository/RepositoryOperations.java index 49f22d6d5..3c8302249 100644 --- a/nitrite/src/main/java/org/dizitart/no2/repository/RepositoryOperations.java +++ b/nitrite/src/main/java/org/dizitart/no2/repository/RepositoryOperations.java @@ -110,12 +110,12 @@ public Document toDocument(T object, boolean update) { if (objectIdField != null) { Field idField = objectIdField.getField(); - if (idField.getType() == NitriteId.class) { + Class fieldType = InterfacePropertyHolder.getPropertyType(idField); + if (fieldType == NitriteId.class) { try { - idField.setAccessible(true); - if (idField.get(object) == null) { + if (FieldAccessHelper.get(idField, object) == null) { NitriteId id = document.getId(); - idField.set(object, id); + FieldAccessHelper.set(idField, object, id); document.put(objectIdField.getIdFieldName(), nitriteMapper.tryConvert(id, Comparable.class)); } else if (!update) { // if it is an insert, then we should not allow to insert the document with user @@ -144,9 +144,8 @@ public Filter createUniqueFilter(Object object) { } Field idField = objectIdField.getField(); - idField.setAccessible(true); try { - Object value = idField.get(object); + Object value = FieldAccessHelper.get(idField, object); if (value == null) { throw new InvalidIdException("Id value cannot be null"); } @@ -160,8 +159,10 @@ public void removeNitriteId(Document document) { document.remove(DOC_ID); if (objectIdField != null) { Field idField = objectIdField.getField(); - if (idField != null && !objectIdField.isEmbedded() && idField.getType() == NitriteId.class) { - document.remove(idField.getName()); + Class fieldType = InterfacePropertyHolder.getPropertyType(idField); + String fieldName = InterfacePropertyHolder.getPropertyName(idField); + if (idField != null && !objectIdField.isEmbedded() && fieldType == NitriteId.class) { + document.remove(fieldName); } } } @@ -171,7 +172,9 @@ public Filter createIdFilter(I id) { if (id == null) { throw new InvalidIdException("Id cannot be null"); } - if (!isCompatibleTypes(id.getClass(), objectIdField.getField().getType())) { + Field idField = objectIdField.getField(); + Class fieldType = InterfacePropertyHolder.getPropertyType(idField); + if (!isCompatibleTypes(id.getClass(), fieldType)) { throw new InvalidIdException("A value of invalid type is provided as id"); } diff --git a/nitrite/src/test/java/org/dizitart/no2/repository/InterfaceEntityTest.java b/nitrite/src/test/java/org/dizitart/no2/repository/InterfaceEntityTest.java new file mode 100644 index 000000000..5a9c36ad7 --- /dev/null +++ b/nitrite/src/test/java/org/dizitart/no2/repository/InterfaceEntityTest.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2017-2022 Nitrite 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 + * + * http://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.dizitart.no2.repository; + +import org.dizitart.no2.Nitrite; +import org.dizitart.no2.collection.NitriteCollection; +import org.dizitart.no2.common.mapper.SimpleNitriteMapper; +import org.dizitart.no2.index.IndexType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * Test for interface entity support using EntityDecorator + */ +public class InterfaceEntityTest { + + private Nitrite db; + private EntityDecoratorScanner scanner; + private NitriteCollection collection; + + // Interface entity definition + public interface Animal { + String getId(); + String getName(); + } + + // Concrete implementation + public static class Dog implements Animal { + private String id; + private String name; + + public Dog() {} + + public Dog(String id, String name) { + this.id = id; + this.name = name; + } + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + // Another concrete implementation + public static class Cat implements Animal { + private String id; + private String name; + + public Cat() {} + + public Cat(String id, String name) { + this.id = id; + this.name = name; + } + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + // Third concrete implementation with boolean property + public static class Bird implements Animal { + private String id; + private String name; + private boolean canFly; + + public Bird() {} + + public Bird(String id, String name, boolean canFly) { + this.id = id; + this.name = name; + this.canFly = canFly; + } + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isCanFly() { + return canFly; + } + + public void setCanFly(boolean canFly) { + this.canFly = canFly; + } + } + + // EntityDecorator for the interface + public static class AnimalDecorator implements EntityDecorator { + + @Override + public Class getEntityType() { + return Animal.class; + } + + @Override + public EntityId getIdField() { + return new EntityId("id"); + } + + @Override + public List getIndexFields() { + List list = new ArrayList<>(); + EntityIndex nameIndex = new EntityIndex(IndexType.NON_UNIQUE, "name"); + list.add(nameIndex); + return list; + } + + @Override + public String getEntityName() { + return "animal"; + } + } + + @Before + public void setUp() { + SimpleNitriteMapper nitriteMapper = new SimpleNitriteMapper(); + db = Nitrite.builder() + .fieldSeparator(".") + .openOrCreate(); + collection = db.getCollection("test"); + scanner = new EntityDecoratorScanner(new AnimalDecorator(), collection, nitriteMapper); + } + + @After + public void tearDown() { + if (db != null && !db.isClosed()) { + db.close(); + } + } + + @Test + public void testReadEntityWithInterface() { + assertNull(scanner.getObjectIdField()); + assertTrue(scanner.getIndices().isEmpty()); + + // This should not throw ValidationException + scanner.readEntity(); + + assertNotNull(scanner.getObjectIdField()); + assertFalse(scanner.getIndices().isEmpty()); + + ObjectIdField idField = scanner.getObjectIdField(); + assertEquals("id", idField.getIdFieldName()); + } + + @Test + public void testCreateIndicesWithInterface() { + assertFalse(collection.hasIndex("name")); + assertFalse(collection.hasIndex("id")); + + scanner.readEntity(); + scanner.createIndices(); + + assertTrue(collection.hasIndex("name")); + } + + @Test + public void testCreateIdIndexWithInterface() { + assertFalse(collection.hasIndex("id")); + + scanner.readEntity(); + scanner.createIdIndex(); + + assertTrue(collection.hasIndex("id")); + } + + @Test + public void testMultipleImplementationsFieldAccess() throws IllegalAccessException { + scanner.readEntity(); + ObjectIdField idField = scanner.getObjectIdField(); + assertNotNull(idField); + + // Test with Dog instance + Dog dog = new Dog("dog-1", "Buddy"); + Object dogId = FieldAccessHelper.get(idField.getField(), dog); + assertEquals("dog-1", dogId); + + FieldAccessHelper.set(idField.getField(), dog, "dog-2"); + assertEquals("dog-2", dog.getId()); + + // Test with Cat instance + Cat cat = new Cat("cat-1", "Whiskers"); + Object catId = FieldAccessHelper.get(idField.getField(), cat); + assertEquals("cat-1", catId); + + FieldAccessHelper.set(idField.getField(), cat, "cat-2"); + assertEquals("cat-2", cat.getId()); + + // Test with Bird instance + Bird bird = new Bird("bird-1", "Tweety", true); + Object birdId = FieldAccessHelper.get(idField.getField(), bird); + assertEquals("bird-1", birdId); + + FieldAccessHelper.set(idField.getField(), bird, "bird-2"); + assertEquals("bird-2", bird.getId()); + } + + @Test + public void testEdgeCaseEmptyPropertyName() { + // This tests that our validation works + scanner.readEntity(); + Dog dog = new Dog("test", "TestDog"); + + try { + // Directly test the helper with an empty property name + // This should fail gracefully + java.lang.reflect.Field testField = InterfacePropertyHolder.class.getDeclaredField("property"); + InterfacePropertyHolder.registerProperty(testField, "", null); + FieldAccessHelper.get(testField, dog); + fail("Should have thrown IllegalAccessException for empty property name"); + } catch (IllegalAccessException e) { + assertTrue(e.getMessage().contains("Property name cannot be null or empty")); + } catch (Exception e) { + // Expected - field access may fail in different ways + } + } + + @Test + public void testEdgeCaseNullPropertyName() { + scanner.readEntity(); + Dog dog = new Dog("test", "TestDog"); + + try { + // Directly test the helper with a null property name + java.lang.reflect.Field testField = InterfacePropertyHolder.class.getDeclaredField("property"); + InterfacePropertyHolder.registerProperty(testField, null, null); + FieldAccessHelper.get(testField, dog); + fail("Should have thrown IllegalAccessException for null property name"); + } catch (IllegalAccessException e) { + assertTrue(e.getMessage().contains("Property name cannot be null or empty")); + } catch (Exception e) { + // Expected - field access may fail in different ways + } + } + + @Test + public void testBooleanPropertyWithIsPrefix() throws IllegalAccessException { + scanner.readEntity(); + + Bird bird = new Bird("bird-1", "Tweety", true); + + // Create a synthetic field for a boolean property + try { + java.lang.reflect.Method isMethod = Bird.class.getMethod("isCanFly"); + java.lang.reflect.Field syntheticField = InterfacePropertyHolder.class.getDeclaredField("property"); + InterfacePropertyHolder.registerProperty(syntheticField, "canFly", isMethod); + + // Test getting boolean property via 'is' prefix + Object value = FieldAccessHelper.get(syntheticField, bird); + assertEquals(true, value); + + // Test setting boolean property + FieldAccessHelper.set(syntheticField, bird, false); + assertEquals(false, bird.isCanFly()); + } catch (Exception e) { + fail("Should be able to access boolean property with 'is' prefix: " + e.getMessage()); + } + } + + @Test + public void testMultipleClassesWithSameInterface() { + // Verify that the scanner can handle multiple different implementations + scanner.readEntity(); + ObjectIdField idField = scanner.getObjectIdField(); + assertNotNull(idField); + + // All three implementations should work with the same scanner + Dog dog = new Dog("1", "Dog"); + Cat cat = new Cat("2", "Cat"); + Bird bird = new Bird("3", "Bird", true); + + try { + assertEquals("1", FieldAccessHelper.get(idField.getField(), dog)); + assertEquals("2", FieldAccessHelper.get(idField.getField(), cat)); + assertEquals("3", FieldAccessHelper.get(idField.getField(), bird)); + } catch (IllegalAccessException e) { + fail("Should be able to access id field on all implementations: " + e.getMessage()); + } + } +} diff --git a/pom.xml b/pom.xml index a280f028a..78c8c3fd1 100644 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ 3.3.1 2.1.0 1.26 - 7.0_r2 + 8.0.0_r2 3.2.8 @@ -446,8 +446,8 @@ net.sf.androidscents.signature - android-api-level-24 - ${api-level-24.version} + android-api-level-26 + ${api-level-26.version}