diff --git a/inset-core/src/main/java/slatepowered/inset/codec/DataCodec.java b/inset-core/src/main/java/slatepowered/inset/codec/DataCodec.java index 4aec58d..ee0c360 100644 --- a/inset-core/src/main/java/slatepowered/inset/codec/DataCodec.java +++ b/inset-core/src/main/java/slatepowered/inset/codec/DataCodec.java @@ -1,6 +1,7 @@ package slatepowered.inset.codec; import slatepowered.inset.datastore.DataItem; +import slatepowered.inset.internal.ProjectionType; import slatepowered.inset.operation.Projection; import slatepowered.inset.query.Query; @@ -13,7 +14,7 @@ * @param The key type. * @param The data value type. */ -public interface DataCodec extends ValueCodec { +public interface DataCodec extends ValueCodec, ProjectionType { /** * Retrieve the primary key from the given value if present. @@ -48,13 +49,4 @@ public interface DataCodec extends ValueCodec { */ Predicate getFilterPredicate(Query query); - /** - * Create a new projection which only includes the fields - * applicable to this data. - * - * @param primaryKeyName The primary key field name override. - * @return The projection. - */ - Projection createExclusiveProjection(String primaryKeyName); - } diff --git a/inset-core/src/main/java/slatepowered/inset/datastore/DataItem.java b/inset-core/src/main/java/slatepowered/inset/datastore/DataItem.java index 0d5a0c0..ccdcfc7 100644 --- a/inset-core/src/main/java/slatepowered/inset/datastore/DataItem.java +++ b/inset-core/src/main/java/slatepowered/inset/datastore/DataItem.java @@ -1,7 +1,9 @@ package slatepowered.inset.datastore; import slatepowered.inset.codec.*; +import slatepowered.inset.internal.ProjectionInterface; import slatepowered.inset.operation.Sorting; +import slatepowered.inset.query.FindAllOperation; import slatepowered.inset.query.FoundItem; import slatepowered.inset.query.Query; import slatepowered.inset.source.DataSourceFindResult; @@ -240,6 +242,11 @@ public DataItem fetchSync() { return decode(queryResult.input()).fetchedNow(); } + @Override + protected Datastore assertQualified() { + return this.datastore; + } + @Override public boolean isPartial() { return false; @@ -266,13 +273,19 @@ public V getField(String fieldName, Type expectedType) { } @Override - public V project(Class vClass) { - return null; // TODO + public DataItem fetch() { + return this; } @Override - public DataItem fetch() { - return this; + @SuppressWarnings("unchecked") + protected V projectInterface(ProjectionInterface projectionInterface) { + if (projectionInterface.getKlass().isInstance(value)) { + return (V) value; + } + + final DataCodec codec = datastore.getDataCodec(); + return (V) projectionInterface.createProxy(() -> key, (name, type) -> codec.getField(value, name)); } /** diff --git a/inset-core/src/main/java/slatepowered/inset/internal/ProjectionInterface.java b/inset-core/src/main/java/slatepowered/inset/internal/ProjectionInterface.java new file mode 100644 index 0000000..6b04051 --- /dev/null +++ b/inset-core/src/main/java/slatepowered/inset/internal/ProjectionInterface.java @@ -0,0 +1,82 @@ +package slatepowered.inset.internal; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import slatepowered.inset.operation.Projection; +import slatepowered.inset.util.Reflections; +import slatepowered.veru.reflect.ReflectUtil; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +@RequiredArgsConstructor +@Getter +public class ProjectionInterface implements ProjectionType { + + /** + * The interface class. + */ + protected final Class klass; + + /** + * The method representing the key field. + */ + protected final Method keyMethod; + + /** + * The other data field methods. + */ + protected final List fieldMethods; + + /** + * Create a new proxy for this interface with the given parameters. + * + * @return The proxy instance. + */ + public Object createProxy(Supplier keySupplier, + BiFunction fieldGetter) { + return Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[] { klass }, (proxy, method, args) -> { + if (method.isDefault()) { + return ReflectUtil.invokeDefault(proxy, method, args); + } + + if (method.equals(keyMethod)) { + return keySupplier.get(); + } + + if (method.equals(Reflections.METHOD_OBJECT_EQUALS)) { + return false; // todo + } else if (method.equals(Reflections.METHOD_OBJECT_TOSTRING)) { + return "partial projection of key " + keySupplier.get().toString(); + } else if (method.equals(Reflections.METHOD_OBJECT_HASHCODE)) { + return keySupplier.get().hashCode(); + } + + return fieldGetter.apply(method.getName(), method.getGenericReturnType()); + }); + } + + @Override + public Projection createExclusiveProjection(String primaryKeyNameOverride) { + List fields = new ArrayList<>(); + + // add applicable data fields + for (Method method : fieldMethods) { + fields.add(method.getName()); + } + + // add primary key field + if (primaryKeyNameOverride == null) + primaryKeyNameOverride = keyMethod.getName(); + fields.add(primaryKeyNameOverride); + + return Projection.include(fields); + } + +} diff --git a/inset-core/src/main/java/slatepowered/inset/internal/ProjectionType.java b/inset-core/src/main/java/slatepowered/inset/internal/ProjectionType.java new file mode 100644 index 0000000..096bc35 --- /dev/null +++ b/inset-core/src/main/java/slatepowered/inset/internal/ProjectionType.java @@ -0,0 +1,19 @@ +package slatepowered.inset.internal; + +import slatepowered.inset.operation.Projection; + +/** + * Represents a type which can be used as a projection. + */ +public interface ProjectionType { + + /** + * Create a new projection which only includes the fields + * applicable to this data. + * + * @param primaryKeyNameOverride The primary key field name override. + * @return The projection. + */ + Projection createExclusiveProjection(String primaryKeyNameOverride); + +} diff --git a/inset-core/src/main/java/slatepowered/inset/internal/ProjectionTypes.java b/inset-core/src/main/java/slatepowered/inset/internal/ProjectionTypes.java new file mode 100644 index 0000000..0f7d3d1 --- /dev/null +++ b/inset-core/src/main/java/slatepowered/inset/internal/ProjectionTypes.java @@ -0,0 +1,85 @@ +package slatepowered.inset.internal; + +import slatepowered.inset.codec.DataCodec; +import slatepowered.inset.datastore.Datastore; +import slatepowered.inset.operation.Projection; +import slatepowered.inset.reflective.Key; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Support with projection-defining interfaces. + */ +public final class ProjectionTypes { + + /** + * All compiled projection interfaces. + */ + private static final Map, ProjectionInterface> compiledInterfaces = new ConcurrentHashMap<>(); + + /** + * Get or compile the projection data for the given interface. + * + * @param klass The interface class. + * @return The interface. + */ + public static ProjectionInterface compileProjectionInterface(Class klass) { + ProjectionInterface projectionInterface = compiledInterfaces.get(klass); + if (projectionInterface != null) { + return projectionInterface; + } + + if (!klass.isInterface()) { + return null; + } + + Method[] methods = klass.getMethods(); + List fieldMethods = new ArrayList<>(); + Method keyMethod = null; + for (Method method : methods) { + // check for primary key method + if (method.isAnnotationPresent(Key.class)) { + keyMethod = method; + continue; + } + + fieldMethods.add(method); + } + + compiledInterfaces.put(klass, projectionInterface = new ProjectionInterface(klass, keyMethod, fieldMethods)); + return projectionInterface; + } + + /** + * Get or compile a {@link ProjectionType} for the given class/type in the context + * of the given datastore. + * + * @param klass The class. + * @param datastore The datastore. + * @return The {@link ProjectionType} instance. + */ + @SuppressWarnings("unchecked") + public static ProjectionType getProjectionType(Class klass, Datastore datastore) { + ProjectionType projectionType = null; + + // check for projection interface + if (klass.isInterface()) { + projectionType = compileProjectionInterface(klass); + } + + // check for class + else { + projectionType = datastore.getCodecRegistry().getCodec(klass).expect(DataCodec.class); + } + + if (projectionType != null) + return projectionType; + throw new UnsupportedOperationException("Unsupported type for projection of potentially partial data: " + klass); + } + +} diff --git a/inset-core/src/main/java/slatepowered/inset/query/FindAllOperation.java b/inset-core/src/main/java/slatepowered/inset/query/FindAllOperation.java index f081932..357bdc7 100644 --- a/inset-core/src/main/java/slatepowered/inset/query/FindAllOperation.java +++ b/inset-core/src/main/java/slatepowered/inset/query/FindAllOperation.java @@ -8,9 +8,12 @@ import slatepowered.inset.datastore.Datastore; import slatepowered.inset.datastore.OperationStatus; import slatepowered.inset.internal.CachedStreams; +import slatepowered.inset.internal.ProjectionType; +import slatepowered.inset.internal.ProjectionTypes; import slatepowered.inset.operation.Projection; import slatepowered.inset.operation.Sorting; import slatepowered.inset.source.DataSourceBulkIterable; +import slatepowered.inset.source.SourceFoundItem; import sun.security.util.Cache; import java.util.Iterator; @@ -224,8 +227,8 @@ public FindAllOperation projection(Projection projection) { * @return This. */ public FindAllOperation projection(Class vClass) { - DataCodec dataCodec = datastore.getCodecRegistry().getCodec(vClass).expect(DataCodec.class); - Projection projection = dataCodec.createExclusiveProjection(iterable.getPrimaryKeyFieldOverride()); + ProjectionType projectionType = ProjectionTypes.getProjectionType(vClass, datastore); + Projection projection = projectionType.createExclusiveProjection(iterable.getPrimaryKeyFieldOverride()); return projection(projection); } @@ -262,7 +265,7 @@ private CompletableFuture async(Supplier supplier) { } // qualify the given item for this query - private FoundItem qualify(FoundItem item) { + private SourceFoundItem qualify(SourceFoundItem item) { return item.qualify(this); } @@ -342,8 +345,8 @@ public List> list() { return stream.collect(Collectors.toList()); } - List> list = (List>) (Object) iterable.list(); - for (FoundItem item : list) { + List> list = (List>) (Object) iterable.list(); + for (SourceFoundItem item : list) { this.qualify(item); } diff --git a/inset-core/src/main/java/slatepowered/inset/query/FoundItem.java b/inset-core/src/main/java/slatepowered/inset/query/FoundItem.java index def4479..02d0a12 100644 --- a/inset-core/src/main/java/slatepowered/inset/query/FoundItem.java +++ b/inset-core/src/main/java/slatepowered/inset/query/FoundItem.java @@ -5,10 +5,12 @@ import slatepowered.inset.codec.DecodeInput; import slatepowered.inset.datastore.DataItem; import slatepowered.inset.datastore.Datastore; +import slatepowered.inset.internal.ProjectionInterface; +import slatepowered.inset.internal.ProjectionType; +import slatepowered.inset.internal.ProjectionTypes; import slatepowered.inset.operation.Sorting; import java.lang.reflect.Type; -import java.util.Arrays; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -20,30 +22,18 @@ */ public abstract class FoundItem { - /** - * The operation status this found item is a part of. - * - * This is only available after qualified. - */ - protected FindAllOperation source; - protected DecodeInput cachedInput; // The cached input, used by this class to read partial data private double[] cachedOrderCoefficients; // The cached order coefficient array private Sorting cachedSort; // The ID the cached order coefficient is for - @SuppressWarnings("unchecked") - protected FoundItem qualify(FindAllOperation source) { - this.source = source; - return (FoundItem) this; - } - - // assert this item has been qualified - @SuppressWarnings("unchecked") - protected final FindAllOperation assertQualified() { - if (source == null) - throw new IllegalStateException("Item has not been qualified yet"); - return (FindAllOperation) source; - } + /** + * Assert this item has been qualified and return the source of the + * item if present. + * + * @return The source. + * @throws IllegalStateException If the item has not been qualified. + */ + protected abstract Datastore assertQualified(); /** * Whether the data of this item was projected and should be @@ -91,7 +81,7 @@ public DecodeInput getOrCreateInput() { * @return The key. */ public K getKey() { - Datastore datastore = assertQualified().getDatastore(); + Datastore datastore = assertQualified(); return getOrReadKey(datastore.getDataCodec().getPrimaryKeyFieldName(), datastore.getKeyClass()); } @@ -105,16 +95,6 @@ public K getKey() { */ public abstract V getField(String fieldName, Type expectedType); - /** - * Project the potentially partial data onto a new instance of the given - * data type. - * - * @param vClass The data class. - * @param The data type. - * @return The data instance with the projected data. - */ - public abstract V project(Class vClass); - /** * Fetch a data item from the database if this result was partial, * otherwise ensure it is cached. @@ -130,7 +110,7 @@ public K getKey() { * @return The data item. */ public CompletableFuture> fetchAsync() { - return CompletableFuture.supplyAsync(this::fetch, assertQualified().getDatastore().getDataManager().getExecutorService()); + return CompletableFuture.supplyAsync(this::fetch, assertQualified().getDataManager().getExecutorService()); } /** @@ -157,6 +137,44 @@ public double[] getFastOrderCoefficients(String[] fields, Sorting sorting) { return cachedOrderCoefficients; } + /** + * Project the potentially partial data onto a new instance of the given + * data type. + * + * @param vClass The data class. + * @param The data type. + * @return The data instance with the projected data. + */ + @SuppressWarnings("unchecked") + public V project(Class vClass) { + ProjectionType projectionType = ProjectionTypes.getProjectionType(vClass, assertQualified()); + + if (projectionType instanceof ProjectionInterface) { + return projectInterface((ProjectionInterface) projectionType); + } else if (projectionType instanceof DataCodec) { + return projectDataClass((DataCodec) projectionType); + } + + throw new UnsupportedOperationException("Unsupported projection type compiled from " + vClass + ": " + projectionType); + } + + /** + * Project the potentially partial data from this item through the + * given projection interface. + * + * @param projectionInterface The projection interface. + * @param The interface type. + * @return The interface instance. + */ + protected abstract V projectInterface(ProjectionInterface projectionInterface); + + // project the partial data into the given class + protected V projectDataClass(DataCodec codec) { + Datastore datastore = assertQualified(); + CodecContext context = datastore.newCodecContext(); + return codec.constructAndDecode(context, input()); + } + @Override public boolean equals(Object other) { if (other == this) return true; diff --git a/inset-core/src/main/java/slatepowered/inset/reflective/Key.java b/inset-core/src/main/java/slatepowered/inset/reflective/Key.java index 91772a7..ce11691 100644 --- a/inset-core/src/main/java/slatepowered/inset/reflective/Key.java +++ b/inset-core/src/main/java/slatepowered/inset/reflective/Key.java @@ -10,7 +10,7 @@ * key for the data object. */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ ElementType.FIELD, ElementType.METHOD }) public @interface Key { } diff --git a/inset-core/src/main/java/slatepowered/inset/source/DataSourceBulkIterable.java b/inset-core/src/main/java/slatepowered/inset/source/DataSourceBulkIterable.java index a3633dd..8144232 100644 --- a/inset-core/src/main/java/slatepowered/inset/source/DataSourceBulkIterable.java +++ b/inset-core/src/main/java/slatepowered/inset/source/DataSourceBulkIterable.java @@ -71,7 +71,7 @@ public interface DataSourceBulkIterable extends DataSourceOperation { * * @return The first item or empty if absent. */ - Optional> first(); + Optional> first(); /** * Get the next item in this result set. @@ -80,7 +80,7 @@ public interface DataSourceBulkIterable extends DataSourceOperation { * * @return The next item. */ - Optional> next(); + Optional> next(); /** * Check whether there is another item. @@ -94,13 +94,13 @@ public interface DataSourceBulkIterable extends DataSourceOperation { * * @return The list of items. */ - List> list(); + List> list(); /** * Stream all items in this result set. * * @return The stream. */ - Stream> stream(); + Stream> stream(); } diff --git a/inset-core/src/main/java/slatepowered/inset/query/SourceFoundItem.java b/inset-core/src/main/java/slatepowered/inset/source/SourceFoundItem.java similarity index 67% rename from inset-core/src/main/java/slatepowered/inset/query/SourceFoundItem.java rename to inset-core/src/main/java/slatepowered/inset/source/SourceFoundItem.java index fc65a85..0633c59 100644 --- a/inset-core/src/main/java/slatepowered/inset/query/SourceFoundItem.java +++ b/inset-core/src/main/java/slatepowered/inset/source/SourceFoundItem.java @@ -1,12 +1,17 @@ -package slatepowered.inset.query; +package slatepowered.inset.source; import slatepowered.inset.codec.CodecContext; import slatepowered.inset.codec.DataCodec; import slatepowered.inset.codec.DecodeInput; import slatepowered.inset.datastore.DataItem; import slatepowered.inset.datastore.Datastore; +import slatepowered.inset.internal.ProjectionInterface; import slatepowered.inset.operation.Sorting; +import slatepowered.inset.query.FindAllOperation; +import slatepowered.inset.query.FindOperation; +import slatepowered.inset.query.FoundItem; +import java.lang.reflect.Proxy; import java.lang.reflect.Type; /** @@ -17,19 +22,40 @@ */ public abstract class SourceFoundItem extends FoundItem { + /** + * The operation status this found item is a part of. + * + * This is only available after qualified. + */ + protected Datastore source; + protected CodecContext partialCodecContext; // The context used to read from the partial data protected K cachedKey; // The cached key object + @SuppressWarnings("unchecked") + public SourceFoundItem qualify(FindAllOperation source) { + this.source = source.getDatastore(); + return (SourceFoundItem) this; + } + // ensure a codec context for the reading // of partial data exists and return it private CodecContext ensurePartialCodecContext() { if (partialCodecContext == null) { - partialCodecContext = assertQualified().getDatastore().newCodecContext(); + partialCodecContext = assertQualified().newCodecContext(); } return partialCodecContext; } + @Override + @SuppressWarnings("unchecked") + protected final Datastore assertQualified() { + if (source == null) + throw new IllegalStateException("Sourced item is not qualified"); + return (Datastore) source; + } + @Override @SuppressWarnings("unchecked") public V getField(String fieldName, Type expectedType) { @@ -45,29 +71,17 @@ public K getOrReadKey(String fieldName, Type expectedType) { @Override public K getKey() { if (cachedKey == null) { - Datastore datastore = assertQualified().getDatastore(); + Datastore datastore = assertQualified(); cachedKey = getOrReadKey(datastore.getDataCodec().getPrimaryKeyFieldName(), datastore.getKeyClass()); } return cachedKey; } - @Override - public V project(Class vClass) { - FindAllOperation status = assertQualified(); - Datastore datastore = status.getDatastore(); - - DataCodec dataCodec = datastore.getCodecRegistry().getCodec(vClass).expect(DataCodec.class); - CodecContext context = datastore.newCodecContext(); - return dataCodec.constructAndDecode(context, input()); - } - @Override @SuppressWarnings("unchecked") public DataItem fetch() { - FindAllOperation status = assertQualified(); - Datastore datastore = (Datastore) status.getDatastore(); - + Datastore datastore = assertQualified(); DecodeInput input = input(); // if complete there is no need to fetch the @@ -89,6 +103,12 @@ public DataItem fetch() { return findStatus.item(); } + @Override + @SuppressWarnings("unchecked") + protected V projectInterface(ProjectionInterface projectionInterface) { + return (V) projectionInterface.createProxy(this::getKey, this::getField); + } + @Override public double[] createFastOrderCoefficients(String[] fields, Sorting sorting) { final int len = fields.length; diff --git a/inset-core/src/main/java/slatepowered/inset/util/Reflections.java b/inset-core/src/main/java/slatepowered/inset/util/Reflections.java new file mode 100644 index 0000000..25910cb --- /dev/null +++ b/inset-core/src/main/java/slatepowered/inset/util/Reflections.java @@ -0,0 +1,32 @@ +package slatepowered.inset.util; + +import slatepowered.veru.misc.Throwables; + +import java.lang.reflect.Method; + +/** + * Support for reflections. + */ +public final class Reflections { + + // Common methods + public static final Method METHOD_OBJECT_EQUALS = getMethod(Object.class, "equals"); + public static final Method METHOD_OBJECT_TOSTRING = getMethod(Object.class, "toString"); + public static final Method METHOD_OBJECT_HASHCODE = getMethod(Object.class, "hashCode"); + + public static Method getMethod(Class klass, String name) { + try { + for (Method method : klass.getMethods()) { + if (method.getName().equals(name)) { + return method; + } + } + + return null; + } catch (Exception e) { + Throwables.sneakyThrow(e); + throw new AssertionError(); + } + } + +} diff --git a/inset-mongodb/src/main/java/slatepowered/inset/mongodb/MongoQueries.java b/inset-mongodb/src/main/java/slatepowered/inset/mongodb/MongoQueries.java index f67122d..58eae99 100644 --- a/inset-mongodb/src/main/java/slatepowered/inset/mongodb/MongoQueries.java +++ b/inset-mongodb/src/main/java/slatepowered/inset/mongodb/MongoQueries.java @@ -13,7 +13,7 @@ import slatepowered.inset.operation.*; import slatepowered.inset.query.FoundItem; import slatepowered.inset.query.Query; -import slatepowered.inset.query.SourceFoundItem; +import slatepowered.inset.source.SourceFoundItem; import slatepowered.inset.query.constraint.CommonFieldConstraint; import slatepowered.inset.query.constraint.FieldConstraint; import slatepowered.inset.source.DataSourceBulkIterable; @@ -240,12 +240,12 @@ public DataSourceBulkIterable limit(int limit) { } // convert the given document to a bulk item result - private FoundItem convert(Document document) { + private SourceFoundItem convert(Document document) { return toBulkItem(document, keyFieldNameOverride, partial); } // convert the given optional document to a bulk item result - private Optional> convertNullable(Document document) { + private Optional> convertNullable(Document document) { return document == null ? Optional.empty() : Optional.of(convert(document)); } @@ -263,12 +263,12 @@ public DataSourceBulkIterable sort(Sorting sorting) { } @Override - public Optional> first() { + public Optional> first() { return convertNullable(iterable.first()); } @Override - public Optional> next() { + public Optional> next() { return convertNullable(cursor.tryNext()); } @@ -278,8 +278,8 @@ public boolean hasNext() { } @Override - public List> list() { - List> list = new ArrayList<>(); + public List> list() { + List> list = new ArrayList<>(); while (cursor.hasNext()) { Document doc = cursor.tryNext(); if (doc == null) @@ -292,7 +292,7 @@ public boolean hasNext() { } @Override - public Stream> stream() { + public Stream> stream() { return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterable.iterator(), Spliterator.ORDERED), false) .filter(Objects::nonNull) .map(this::convert); @@ -303,9 +303,9 @@ public boolean hasNext() { /** * Convert the given document with the given metadata to a found bulk item. */ - public static FoundItem toBulkItem(Document document, - String keyFieldOverride, - boolean partial) { + public static SourceFoundItem toBulkItem(Document document, + String keyFieldOverride, + boolean partial) { return new SourceFoundItem() { @Override public boolean isPartial() { diff --git a/inset-mongodb/src/test/java/example/slatepowered/inset/MongoDatastoreExample.java b/inset-mongodb/src/test/java/example/slatepowered/inset/MongoDatastoreExample.java index c931247..452bdd6 100644 --- a/inset-mongodb/src/test/java/example/slatepowered/inset/MongoDatastoreExample.java +++ b/inset-mongodb/src/test/java/example/slatepowered/inset/MongoDatastoreExample.java @@ -31,12 +31,11 @@ public static class Stats { protected Integer deaths = 0; } - @ToString - public static class PartialStats { + public interface PartialStats { @Key - UUID uuid; + UUID uuid(); - protected Integer deaths; + Integer deaths(); } public static void main(String[] args) throws InterruptedException { @@ -122,7 +121,8 @@ public static void main(String[] args) throws InterruptedException { .projection(PartialStats.class) .sort(Sorting.builder().descend("deaths").build()) .stream() - .forEachOrdered(foundItem -> System.out.println(foundItem.fetch())); + .map(item -> item.project(PartialStats.class)) + .forEachOrdered(stats -> System.out.println("UUID " + stats.uuid() + " has " + stats.deaths() + " deaths")); dataManager.await(); }