diff --git a/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerQueryMethod.java b/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerQueryMethod.java index 444a33d..58365e1 100644 --- a/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerQueryMethod.java +++ b/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerQueryMethod.java @@ -44,6 +44,10 @@ public final class ReindexerQueryMethod extends QueryMethod { private final Lazy queryAnnotationExtractor; + private final Class returnType; + + private final ProjectionFactory factory; + /** * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following * invocations of the method given. @@ -54,11 +58,13 @@ public final class ReindexerQueryMethod extends QueryMethod { */ public ReindexerQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { super(method, metadata, factory); - this.isIteratorQuery = Lazy.of(() -> Iterator.class.isAssignableFrom(getReturnedObjectType())); + this.isIteratorQuery = Lazy.of(() -> Iterator.class.isAssignableFrom(method.getReturnType())); this.isOptionalQuery = Lazy.of(() -> Optional.class.isAssignableFrom(method.getReturnType())); this.isListQuery = Lazy.of(() -> List.class.isAssignableFrom(method.getReturnType())); this.isSetQuery = Lazy.of(() -> Set.class.isAssignableFrom(method.getReturnType())); this.queryAnnotationExtractor = Lazy.of(() -> method.getAnnotation(Query.class)); + this.returnType = method.getReturnType(); + this.factory = factory; } /** @@ -129,4 +135,30 @@ public boolean isUpdateQuery() { return query.update(); } + /** + * Returns method's return type + * + * @return the method's return type + */ + Class getReturnType() { + return this.returnType; + } + + /** + * Returns a {@link ProjectionFactory} to be used. + * + * @return the {@link ProjectionFactory} to use + */ + ProjectionFactory getFactory() { + return this.factory; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getDomainClass() { + return super.getDomainClass(); + } + } diff --git a/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerRepositoryQuery.java b/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerRepositoryQuery.java index 98936bb..73138a7 100644 --- a/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerRepositoryQuery.java +++ b/src/main/java/org/springframework/data/reindexer/repository/query/ReindexerRepositoryQuery.java @@ -15,14 +15,22 @@ */ package org.springframework.data.reindexer.repository.query; +import java.beans.PropertyDescriptor; import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import ru.rt.restream.reindexer.AggregationResult; import org.springframework.data.reindexer.repository.support.TransactionalNamespace; import ru.rt.restream.reindexer.FieldType; import ru.rt.restream.reindexer.Namespace; @@ -30,7 +38,10 @@ import ru.rt.restream.reindexer.Reindexer; import ru.rt.restream.reindexer.ReindexerIndex; import ru.rt.restream.reindexer.ReindexerNamespace; +import ru.rt.restream.reindexer.ResultIterator; +import ru.rt.restream.reindexer.util.BeanPropertyUtils; +import org.springframework.core.CollectionFactory; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.repository.query.ParametersParameterAccessor; @@ -55,6 +66,8 @@ public class ReindexerRepositoryQuery implements RepositoryQuery { private final Map indexes; + private final String[] selectFields; + /** * Creates an instance. * @@ -72,22 +85,33 @@ public ReindexerRepositoryQuery(ReindexerQueryMethod queryMethod, ReindexerEntit for (ReindexerIndex index : namespace.getIndexes()) { this.indexes.put(index.getName(), index); } + this.selectFields = getSelectFields(); + } + + private String[] getSelectFields() { + if (this.queryMethod.getDomainClass() == this.queryMethod.getReturnedObjectType() + || this.queryMethod.getParameters().hasDynamicProjection()) { + return new String[0]; + } + return getSelectFields(this.queryMethod.getReturnedObjectType()); } @Override public Object execute(Object[] parameters) { Query query = createQuery(parameters); if (this.queryMethod.isCollectionQuery()) { - return query.toList(); + return toCollection(query, parameters); } if (this.queryMethod.isStreamQuery()) { - return query.stream(); + return toStream(query, parameters); } if (this.queryMethod.isIteratorQuery()) { - return query.execute(); + return new ProjectingResultIterator(query, parameters); } if (this.queryMethod.isQueryForEntity()) { - return query.getOne(); + Object entity = toEntity(query, parameters); + Assert.state(entity != null, "Exactly one item expected, but there is zero"); + return entity; } if (this.tree.isExistsProjection()) { return query.exists(); @@ -99,13 +123,13 @@ public Object execute(Object[] parameters) { query.delete(); return null; } - return query.findOne(); + return Optional.ofNullable(toEntity(query, parameters)); } private Query createQuery(Object[] parameters) { ParametersParameterAccessor accessor = new ParametersParameterAccessor(this.queryMethod.getParameters(), parameters); - Query base = this.namespace.query(); + Query base = this.namespace.query().select(getSelectFields(parameters)); Iterator iterator = accessor.iterator(); for (OrPart node : this.tree) { Iterator parts = node.iterator(); @@ -125,6 +149,34 @@ private Query createQuery(Object[] parameters) { return base; } + private String[] getSelectFields(Object[] parameters) { + if (this.queryMethod.getParameters().hasDynamicProjection()) { + Class type = (Class) parameters[this.queryMethod.getParameters().getDynamicProjectionIndex()]; + return getSelectFields(type); + } + return this.selectFields; + } + + private String[] getSelectFields(Class type) { + if (type.isInterface()) { + List inputProperties = this.queryMethod.getFactory() + .getProjectionInformation(type).getInputProperties(); + String[] result = new String[inputProperties.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = inputProperties.get(i).getName(); + } + return result; + } + else { + List inheritedFields = BeanPropertyUtils.getInheritedFields(type); + String[] result = new String[inheritedFields.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = inheritedFields.get(i).getName(); + } + return result; + } + } + private Query where(Part part, Query criteria, Iterator parameters) { String indexName = part.getProperty().toDotPath(); switch (part.getType()) { @@ -188,9 +240,110 @@ private Object getParameterValue(String indexName, Object value) { return value; } + private Collection toCollection(Query query, Object[] parameters) { + try (ResultIterator iterator = new ProjectingResultIterator(query, parameters)) { + Collection result = CollectionFactory.createCollection(this.queryMethod.getReturnType(), + this.queryMethod.getReturnedObjectType(), (int) iterator.size()); + while (iterator.hasNext()) { + result.add(iterator.next()); + } + return result; + } + } + + private Stream toStream(Query query, Object[] parameters) { + ResultIterator iterator = new ProjectingResultIterator(query, parameters); + Spliterator spliterator = Spliterators.spliterator(iterator, iterator.size(), Spliterator.NONNULL); + return StreamSupport.stream(spliterator, false).onClose(iterator::close); + } + + private Object toEntity(Query query, Object[] parameters) { + try (ResultIterator iterator = new ProjectingResultIterator(query, parameters)) { + Object item = null; + if (iterator.hasNext()) { + item = iterator.next(); + } + if (iterator.hasNext()) { + throw new IllegalStateException("Exactly one item expected, but there are more"); + } + return item; + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + @Override public ReindexerQueryMethod getQueryMethod() { return this.queryMethod; } + private final class ProjectingResultIterator implements ResultIterator { + + private final Object[] parameters; + + private final ResultIterator delegate; + + private ProjectingResultIterator(Query query, Object[] parameters) { + this.parameters = parameters; + this.delegate = query.execute(determineReturnType()); + } + + private Class determineReturnType() { + if (ReindexerRepositoryQuery.this.queryMethod.getParameters().hasDynamicProjection()) { + Class type = (Class) this.parameters[ReindexerRepositoryQuery.this.queryMethod.getParameters().getDynamicProjectionIndex()]; + if (type.isInterface()) { + return ReindexerRepositoryQuery.this.queryMethod.getDomainClass(); + } + return type; + } + if (ReindexerRepositoryQuery.this.queryMethod.getDomainClass() != ReindexerRepositoryQuery.this.queryMethod.getReturnedObjectType() + && ReindexerRepositoryQuery.this.queryMethod.getReturnedObjectType().isInterface()) { + return ReindexerRepositoryQuery.this.queryMethod.getDomainClass(); + } + return ReindexerRepositoryQuery.this.queryMethod.getReturnedObjectType(); + } + + @Override + public long getTotalCount() { + return this.delegate.getTotalCount(); + } + + @Override + public long size() { + return this.delegate.size(); + } + + @Override + public List aggResults() { + return this.delegate.aggResults(); + } + + @Override + public void close() { + this.delegate.close(); + } + + @Override + public boolean hasNext() { + return this.delegate.hasNext(); + } + + @Override + public Object next() { + Object item = this.delegate.next(); + if (ReindexerRepositoryQuery.this.queryMethod.getParameters().hasDynamicProjection()) { + Class type = (Class) this.parameters[ReindexerRepositoryQuery.this.queryMethod.getParameters().getDynamicProjectionIndex()]; + if (type.isInterface()) { + return ReindexerRepositoryQuery.this.queryMethod.getFactory().createProjection(type, item); + } + } + else if (ReindexerRepositoryQuery.this.queryMethod.getReturnedObjectType().isInterface()) { + return ReindexerRepositoryQuery.this.queryMethod.getFactory() + .createProjection(ReindexerRepositoryQuery.this.queryMethod.getReturnedObjectType(), item); + } + return item; + } + + } + } diff --git a/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerAnnotationRepositoryMetadata.java b/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerAnnotationRepositoryMetadata.java new file mode 100644 index 0000000..4fcc936 --- /dev/null +++ b/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerAnnotationRepositoryMetadata.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 evgeniycheban + * + * 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.springframework.data.reindexer.repository.support; + +import java.lang.reflect.Method; + +import org.springframework.data.reindexer.repository.util.ReindexerQueryExecutionConverters; +import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.util.TypeInformation; + +/** + * {@link org.springframework.data.repository.core.RepositoryMetadata} implementation inspecting the given + * repository interface for a {@link org.springframework.data.repository.RepositoryDefinition} annotation. + * + * @author Evgeniy Cheban + */ +public class ReindexerAnnotationRepositoryMetadata extends AnnotationRepositoryMetadata { + + /** + * Creates a new {@link ReindexerAnnotationRepositoryMetadata} instance looking up repository types from a + * {@link org.springframework.data.repository.RepositoryDefinition} annotation. + * + * @param repositoryInterface must not be {@literal null}. + */ + public ReindexerAnnotationRepositoryMetadata(Class repositoryInterface) { + super(repositoryInterface); + } + + @Override + public Class getReturnedDomainClass(Method method) { + TypeInformation returnType = getReturnType(method); + returnType = ReactiveWrapperConverters.unwrapWrapperTypes(returnType); + return ReindexerQueryExecutionConverters.unwrapWrapperTypes(returnType, getDomainTypeInformation()).getType(); + } + +} diff --git a/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerDefaultRepositoryMetadata.java b/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerDefaultRepositoryMetadata.java new file mode 100644 index 0000000..34c5bee --- /dev/null +++ b/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerDefaultRepositoryMetadata.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 evgeniycheban + * + * 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.springframework.data.reindexer.repository.support; + +import java.lang.reflect.Method; + +import org.springframework.data.reindexer.repository.util.ReindexerQueryExecutionConverters; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.util.TypeInformation; + +/** + * Default implementation of {@link org.springframework.data.repository.core.RepositoryMetadata}. + * Will inspect generic types of {@link org.springframework.data.repository.Repository} to find out about domain and id class. + * + * @author Evgeniy Cheban + */ +public class ReindexerDefaultRepositoryMetadata extends DefaultRepositoryMetadata { + + /** + * Creates a new {@link ReindexerDefaultRepositoryMetadata} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + public ReindexerDefaultRepositoryMetadata(Class repositoryInterface) { + super(repositoryInterface); + } + + @Override + public Class getReturnedDomainClass(Method method) { + TypeInformation returnType = getReturnType(method); + returnType = ReactiveWrapperConverters.unwrapWrapperTypes(returnType); + return ReindexerQueryExecutionConverters.unwrapWrapperTypes(returnType, getDomainTypeInformation()).getType(); + + } + +} diff --git a/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerRepositoryFactory.java b/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerRepositoryFactory.java index 8b48667..72aa976 100644 --- a/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerRepositoryFactory.java +++ b/src/main/java/org/springframework/data/reindexer/repository/support/ReindexerRepositoryFactory.java @@ -27,6 +27,7 @@ import org.springframework.data.reindexer.repository.query.ReindexerQueryMethod; import org.springframework.data.reindexer.repository.query.ReindexerRepositoryQuery; import org.springframework.data.reindexer.repository.query.StringBasedReindexerRepositoryQuery; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; @@ -35,6 +36,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.Assert; /** * Factory to create {@link ReindexerRepository} instances. @@ -59,6 +61,13 @@ public ReindexerEntityInformation getEntityInformation(Class d return MappingReindexerEntityInformation.getInstance(domainClass); } + @Override + protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { + Assert.notNull(repositoryInterface, "Repository interface must not be null"); + return Repository.class.isAssignableFrom(repositoryInterface) ? new ReindexerDefaultRepositoryMetadata(repositoryInterface) + : new ReindexerAnnotationRepositoryMetadata(repositoryInterface); + } + @Override protected Object getTargetRepository(RepositoryInformation metadata) { EntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); diff --git a/src/main/java/org/springframework/data/reindexer/repository/util/ReindexerQueryExecutionConverters.java b/src/main/java/org/springframework/data/reindexer/repository/util/ReindexerQueryExecutionConverters.java new file mode 100644 index 0000000..0664d7f --- /dev/null +++ b/src/main/java/org/springframework/data/reindexer/repository/util/ReindexerQueryExecutionConverters.java @@ -0,0 +1,66 @@ +/* + * Copyright 2022 evgeniycheban + * + * 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.springframework.data.reindexer.repository.util; + +import java.util.stream.Stream; + +import ru.rt.restream.reindexer.ResultIterator; + +import org.springframework.data.domain.Slice; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +/** + * Converters to potentially wrap the execution of a repository method into a variety of wrapper types + * potentially being available on the classpath. + * @see QueryExecutionConverters + * + * @author Evgeniy Cheban + */ +public final class ReindexerQueryExecutionConverters { + + private ReindexerQueryExecutionConverters() { + } + + /** + * Recursively unwraps well known wrapper types from the given {@link TypeInformation} but aborts at the given + * reference type. + * This method is a copy of {@link QueryExecutionConverters#unwrapWrapperTypes(TypeInformation, TypeInformation)} + * with extension of adding {@link ResultIterator} type check. + * + * @param type must not be {@literal null}. + * @param reference must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static TypeInformation unwrapWrapperTypes(TypeInformation type, TypeInformation reference) { + Assert.notNull(type, "type must not be null"); + if (reference.isAssignableFrom(type)) { + return type; + } + Class rawType = type.getType(); + boolean needToUnwrap = type.isCollectionLike() + || Slice.class.isAssignableFrom(rawType) + || GeoResults.class.isAssignableFrom(rawType) + || rawType.isArray() + || QueryExecutionConverters.supports(rawType) + || Stream.class.isAssignableFrom(rawType) + || ResultIterator.class.isAssignableFrom(rawType); + return needToUnwrap ? unwrapWrapperTypes(type.getRequiredComponentType(), reference) : type; + } + +} diff --git a/src/test/java/org/springframework/data/reindexer/repository/ReindexerRepositoryTests.java b/src/test/java/org/springframework/data/reindexer/repository/ReindexerRepositoryTests.java index ad615b5..a9ee5f8 100644 --- a/src/test/java/org/springframework/data/reindexer/repository/ReindexerRepositoryTests.java +++ b/src/test/java/org/springframework/data/reindexer/repository/ReindexerRepositoryTests.java @@ -596,6 +596,70 @@ public void findByIdIn() { assertEquals(expectedItems.size(), foundItems.size()); } + @Test + public void findItemProjectionByIdIn() { + List expectedItems = new ArrayList<>(); + for (long i = 0; i < 100; i++) { + expectedItems.add(this.repository.save(new TestItem(i, "TestName" + i, "TestValue" + i))); + } + List foundItems = this.repository.findItemProjectionByIdIn(expectedItems.stream() + .map(TestItem::getId) + .collect(Collectors.toList())); + assertEquals(expectedItems.size(), foundItems.size()); + for (int i = 0; i < foundItems.size(); i++) { + assertEquals(expectedItems.get(i).getId(), foundItems.get(i).getId()); + assertEquals(expectedItems.get(i).getName(), foundItems.get(i).getName()); + } + } + + @Test + public void findItemDtoByIdIn() { + List expectedItems = new ArrayList<>(); + for (long i = 0; i < 100; i++) { + expectedItems.add(this.repository.save(new TestItem(i, "TestName" + i, "TestValue" + i))); + } + List foundItems = this.repository.findItemDtoByIdIn(expectedItems.stream() + .map(TestItem::getId) + .collect(Collectors.toList())); + assertEquals(expectedItems.size(), foundItems.size()); + for (int i = 0; i < foundItems.size(); i++) { + assertEquals(expectedItems.get(i).getId(), foundItems.get(i).getId()); + assertEquals(expectedItems.get(i).getName(), foundItems.get(i).getName()); + } + } + + @Test + public void findDynamicItemProjectionByIdIn() { + List expectedItems = new ArrayList<>(); + for (long i = 0; i < 100; i++) { + expectedItems.add(this.repository.save(new TestItem(i, "TestName" + i, "TestValue" + i))); + } + List foundItems = this.repository.findByIdIn(expectedItems.stream() + .map(TestItem::getId) + .collect(Collectors.toList()), TestItemProjection.class); + assertEquals(expectedItems.size(), foundItems.size()); + for (int i = 0; i < foundItems.size(); i++) { + assertEquals(expectedItems.get(i).getId(), foundItems.get(i).getId()); + assertEquals(expectedItems.get(i).getName(), foundItems.get(i).getName()); + } + } + + @Test + public void findDynamicItemDtoByIdIn() { + List expectedItems = new ArrayList<>(); + for (long i = 0; i < 100; i++) { + expectedItems.add(this.repository.save(new TestItem(i, "TestName" + i, "TestValue" + i))); + } + List foundItems = this.repository.findByIdIn(expectedItems.stream() + .map(TestItem::getId) + .collect(Collectors.toList()), TestItemDto.class); + assertEquals(expectedItems.size(), foundItems.size()); + for (int i = 0; i < foundItems.size(); i++) { + assertEquals(expectedItems.get(i).getId(), foundItems.get(i).getId()); + assertEquals(expectedItems.get(i).getName(), foundItems.get(i).getName()); + } + } + @Test public void findByIdInArray() { List expectedItems = new ArrayList<>(); @@ -899,6 +963,12 @@ Optional findOneSqlByNameAndValueManyParams(String name1, String name2 void deleteByName(String name); List findAllByIdIn(List ids, Sort sort); + + List findItemProjectionByIdIn(List ids); + + List findItemDtoByIdIn(List ids); + + List findByIdIn(List ids, Class type); } @Namespace(name = NAMESPACE_NAME) @@ -1006,6 +1076,41 @@ public String toString() { } + interface TestItemProjection { + + Long getId(); + + String getName(); + + } + + public static class TestItemDto { + + private Long id; + + private String name; + + public TestItemDto() { + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + public enum TestEnum { TEST_CONSTANT_1, TEST_CONSTANT_2,