diff --git a/pom.xml b/pom.xml index ba7a7a9..5aaef21 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ jef jef - 1.0 + 0.1 UTF-8 @@ -133,5 +133,10 @@ ${lombok.version} compile + + javax.persistence + javax.persistence-api + 2.2 + \ No newline at end of file diff --git a/src/main/java/jef/model/DbContext.java b/src/main/java/jef/model/DbContext.java new file mode 100644 index 0000000..530f6b9 --- /dev/null +++ b/src/main/java/jef/model/DbContext.java @@ -0,0 +1,4 @@ +package jef.model; + +public abstract class DbContext { +} diff --git a/src/main/java/jef/model/DbEntity.java b/src/main/java/jef/model/DbEntity.java new file mode 100644 index 0000000..56000ec --- /dev/null +++ b/src/main/java/jef/model/DbEntity.java @@ -0,0 +1,94 @@ +package jef.model; + +import jef.asm.AsmParseException; +import jef.asm.AsmParser; +import jef.expressions.Expression; +import jef.expressions.IntermediateFieldExpression; +import jef.serializable.SerializableFunction; +import jef.serializable.SerializableObject; +import lombok.Getter; +import lombok.Setter; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Getter +@Setter +public class DbEntity { + private final Class entityClazz; + private final List> fields; + private String name; + + public DbEntity(Class entityClazz) { + this(entityClazz, new ArrayList<>()); + } + + public DbEntity(Class entityClazz, List> fields) { + this.entityClazz = entityClazz; + this.fields = fields; + this.name = entityClazz.getSimpleName(); + } + + public List> getFields() { + return Collections.unmodifiableList(fields); + } + + /** + * Returns the database model for the requested property or null if not present. + * + * @param getter the expression selecting the property + * @param the type of property class + * @return the database model for the requested property or null if not present. + */ + public DbField getField(SerializableFunction getter) { + var name = extractFieldName(getter); + var prop = (DbField) this.fields.stream().filter(p -> p.getField().getName().equals(name)).findFirst().orElse(null); + return prop; + } + + public DbField getOrCreateField(SerializableFunction getter) { + try { + var prop = getField(getter); + if (prop == null) { + var name = extractFieldName(getter); + var field = ReflectionUtil.getFieldsRecursive(entityClazz).stream().filter(f -> f.getName().equals(name)).findFirst().orElse(null); + if (field == null) { + throw new RuntimeException("Field not found: " + name); + } + prop = new DbField<>((Class) field.getType(), field); + fields.add(prop); + } + return prop; + } catch (Exception e) { + throw new RuntimeException("Invalid expression", e); + } + } + + public DbField getOrCreateField(Field f) { + try { + var prop = (DbField) fields.stream().filter(e -> e.getField() == f).findFirst().orElse(null); + if (prop == null) { + prop = new DbField<>((Class) f.getType(), f); + fields.add(prop); + } + return prop; + } catch (Exception e) { + throw new RuntimeException("Invalid expression", e); + } + } + + private String extractFieldName(SerializableFunction getter) { + try { + var expr = new AsmParser(getter).parse(); + if (expr.getType() != Expression.Type.INTERMEDIATE_FIELD) { + throw new RuntimeException(expr.getClass().getSimpleName() + " is not a field expression"); + } + var name = ((IntermediateFieldExpression) expr).getName(); + return name; + } catch (AsmParseException e) { + throw new RuntimeException("Invalid expression", e); + } + } +} diff --git a/src/main/java/jef/model/DbField.java b/src/main/java/jef/model/DbField.java new file mode 100644 index 0000000..45802e4 --- /dev/null +++ b/src/main/java/jef/model/DbField.java @@ -0,0 +1,21 @@ +package jef.model; + +import lombok.Getter; +import lombok.Setter; + +import java.lang.reflect.Field; + +@Getter +@Setter +public class DbField { + private final Class propertyClazz; + private final Field field; + private String name; + private boolean notNull = false; + + public DbField(Class propertyClazz, Field field) { + this.propertyClazz = propertyClazz; + this.field = field; + this.name = field.getName(); + } +} diff --git a/src/main/java/jef/model/ModelBuilder.java b/src/main/java/jef/model/ModelBuilder.java new file mode 100644 index 0000000..4ead7c8 --- /dev/null +++ b/src/main/java/jef/model/ModelBuilder.java @@ -0,0 +1,116 @@ +package jef.model; + +import jef.DbSet; +import jef.model.annotations.Clazz; +import jef.model.annotations.processors.AnnotationProcessor; +import jef.model.annotations.processors.NotNullProcessor; +import jef.serializable.SerializableObject; + +import javax.persistence.Transient; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class ModelBuilder { + private static final List annotationProcessors = new ArrayList<>(); + + static { + annotationProcessors.add(NotNullProcessor.INSTANCE); + } + + + public static ModelBuilder from(DbContext context) { + try { + return from0(context); + } catch (Exception e) { + throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e); + } + } + + private static ModelBuilder from0(DbContext context) throws Exception { + var mb = new ModelBuilder(new ArrayList<>()); + for (Field ctxfield : context.getClass().getDeclaredFields()) { + System.out.println(ctxfield); + if (!DbSet.class.isAssignableFrom(ctxfield.getType())) { + continue; + } + Clazz clazzAnnontation = ctxfield.getAnnotation(Clazz.class); + if (clazzAnnontation == null) { + throw new ModelException("DbSet " + ctxfield.getName() + " is missing the Clazz annotation"); + } + var dbsetClazz = (Class)clazzAnnontation.clazz(); + var entity = mb.getOrCreateEntity(dbsetClazz); + + var fields = ReflectionUtil.getFieldsRecursive(dbsetClazz); + for (var f : fields) { + if (f.getAnnotationsByType(Transient.class).length > 0) { + continue; + } + if (Collection.class.isAssignableFrom(f.getType())) { + throw new UnsupportedOperationException(); + } else { + var dbField = entity.getOrCreateField(f); + if (f.getType().isPrimitive()) { + dbField.setNotNull(true); + } + } + } + } + for (AnnotationProcessor processor : annotationProcessors) { + processor.apply(mb); + } + return mb; +// var entities = new HashMap, DbEntity>(); +// for (Field ctxfield : context.getClass().getDeclaredFields()) { +// if (!DbSet.class.isAssignableFrom(ctxfield.getClass())) { +// continue; +// } +// ctxfield.setAccessible(true); +// var dbset = (DbSet) ctxfield.get(context); +// var dbsetClazz = dbset.getClazz(); +// var fields = getFieldsRecursive(dbsetClazz); +// entities.put(dbsetClazz, createEntity(dbsetClazz, fields)); +// } +// +// return new ModelBuilder(new ArrayList<>(entities.values())); + } + + private final List> entities; + + public ModelBuilder(List> entities) { + this.entities = entities; + } + + public List> getEntities() { + return Collections.unmodifiableList(entities); + } + + /** + * Returns the database model for the requested class or null if not present. + * + * @param clazz the class of the model class + * @param the type of model class + * @return the database model for the requested class or null if not present. + */ + public DbEntity getEntity(Class clazz) { + return (DbEntity) entities.stream().filter(e -> e.getEntityClazz() == clazz).findFirst().orElse(null); + } + + /** + * Returns the database model for the requested class or creates a new one if none exists. + * + * @param clazz the class of the model class + * @param the type of model class + * @return the database model for the requested class or the newly created one if none existed. + */ + public DbEntity getOrCreateEntity(Class clazz) { + var entity = getEntity(clazz); + if (entity == null) { + entity = new DbEntity<>(clazz); + entities.add(entity); + } + return entity; + } +} diff --git a/src/main/java/jef/model/ModelException.java b/src/main/java/jef/model/ModelException.java new file mode 100644 index 0000000..9e01c8c --- /dev/null +++ b/src/main/java/jef/model/ModelException.java @@ -0,0 +1,15 @@ +package jef.model; + +public class ModelException extends RuntimeException { + public ModelException(String message) { + super(message); + } + + public ModelException(String message, Throwable cause) { + super(message, cause); + } + + public ModelException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/jef/model/ReflectionUtil.java b/src/main/java/jef/model/ReflectionUtil.java new file mode 100644 index 0000000..d27bc1f --- /dev/null +++ b/src/main/java/jef/model/ReflectionUtil.java @@ -0,0 +1,22 @@ +package jef.model; + +import jef.serializable.SerializableObject; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; + +class ReflectionUtil { + static Collection getFieldsRecursive(Class clazz) { + var fields = new HashMap(); + do { + for (Field f : clazz.getDeclaredFields()) { + if (!fields.containsKey(f.getName())) { + fields.put(f.getName(), f); + } + } + clazz = (Class) clazz.getSuperclass(); + } while (clazz != SerializableObject.class); + return fields.values(); + } +} diff --git a/src/main/java/jef/model/annotations/Clazz.java b/src/main/java/jef/model/annotations/Clazz.java new file mode 100644 index 0000000..1590ec7 --- /dev/null +++ b/src/main/java/jef/model/annotations/Clazz.java @@ -0,0 +1,12 @@ +package jef.model.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Clazz { + Class clazz(); +} diff --git a/src/main/java/jef/model/annotations/NotNull.java b/src/main/java/jef/model/annotations/NotNull.java new file mode 100644 index 0000000..0d444f6 --- /dev/null +++ b/src/main/java/jef/model/annotations/NotNull.java @@ -0,0 +1,11 @@ +package jef.model.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface NotNull { +} diff --git a/src/main/java/jef/model/annotations/processors/AnnotationProcessor.java b/src/main/java/jef/model/annotations/processors/AnnotationProcessor.java new file mode 100644 index 0000000..b2f1c41 --- /dev/null +++ b/src/main/java/jef/model/annotations/processors/AnnotationProcessor.java @@ -0,0 +1,8 @@ +package jef.model.annotations.processors; + +import jef.model.ModelBuilder; + +public interface AnnotationProcessor { + + void apply(ModelBuilder mb); +} diff --git a/src/main/java/jef/model/annotations/processors/NotNullProcessor.java b/src/main/java/jef/model/annotations/processors/NotNullProcessor.java new file mode 100644 index 0000000..3cc9994 --- /dev/null +++ b/src/main/java/jef/model/annotations/processors/NotNullProcessor.java @@ -0,0 +1,25 @@ +package jef.model.annotations.processors; + +import jef.model.DbEntity; +import jef.model.DbField; +import jef.model.ModelBuilder; +import jef.model.annotations.NotNull; +import jef.serializable.SerializableObject; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NotNullProcessor implements AnnotationProcessor { + public static final NotNullProcessor INSTANCE = new NotNullProcessor(); + + @Override + public void apply(ModelBuilder mb) { + for (DbEntity entity : mb.getEntities()) { + for (DbField field : entity.getFields()) { + if (field.getField().getAnnotationsByType(NotNull.class).length > 0) { + field.setNotNull(true); + } + } + } + } +} diff --git a/src/test/java/jef/model/DbContextTest.java b/src/test/java/jef/model/DbContextTest.java new file mode 100644 index 0000000..98a519f --- /dev/null +++ b/src/test/java/jef/model/DbContextTest.java @@ -0,0 +1,62 @@ +package jef.model; + +import jef.DbSet; +import jef.model.annotations.Clazz; +import jef.serializable.SerializableObject; +import lombok.Getter; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DbContextTest { + + @Test + public void test() { + var mb = ModelBuilder.from(new Ctx()); + for (DbEntity entity : mb.getEntities()) { + assertEquals("TestClass", entity.getName()); + assertEquals(5, entity.getFields().size()); + assertEquals(1, entity.getFields().stream().filter(e -> e.getName().equals("i")).count()); + assertEquals(1, entity.getFields().stream().filter(e -> e.getName().equals("d")).count()); + assertEquals(1, entity.getFields().stream().filter(e -> e.getName().equals("f")).count()); + assertEquals(1, entity.getFields().stream().filter(e -> e.getName().equals("l")).count()); + assertEquals(1, entity.getFields().stream().filter(e -> e.getName().equals("o")).count()); + + //intrinsic non null + assertEquals(5, entity.getFields().size()); + assertTrue(entity.getFields().stream().filter(e -> e.getName().equals("i")).findFirst().get().isNotNull()); + assertTrue(entity.getFields().stream().filter(e -> e.getName().equals("d")).findFirst().get().isNotNull()); + assertTrue(entity.getFields().stream().filter(e -> e.getName().equals("f")).findFirst().get().isNotNull()); + assertTrue(entity.getFields().stream().filter(e -> e.getName().equals("l")).findFirst().get().isNotNull()); + assertFalse(entity.getFields().stream().filter(e -> e.getName().equals("o")).findFirst().get().isNotNull()); + } + } + + @Test + public void testMissingAnnotation() { + assertThrows(ModelException.class, () -> ModelBuilder.from(new CtxMissingAnnotation())); + } + + public static class Ctx extends DbContext { + + @Clazz(clazz = TestClass.class) + private DbSet objects1; + } + + public static class CtxMissingAnnotation extends DbContext { + + private DbSet objects1; + } + + @Getter + public static class TestClass extends SerializableObject { + public int i = 1; + public Object o = new Object(); + public double d; + public float f; + public long l; + } +} \ No newline at end of file