/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.ignite.springdata.repository.query; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import javax.cache.Cache; import org.apache.ignite.IgniteCache; import org.apache.ignite.cache.query.Query; import org.apache.ignite.cache.query.QueryCursor; import org.apache.ignite.cache.query.SqlFieldsQuery; import org.apache.ignite.cache.query.SqlQuery; import org.apache.ignite.internal.processors.cache.CacheEntryImpl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.Sort; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; /** * Ignite SQL query implementation. */ @SuppressWarnings("unchecked") public class IgniteRepositoryQuery implements RepositoryQuery { /** Defines the way how to process query result */ private enum ReturnStrategy { /** Need to return only one value. */ ONE_VALUE, /** Need to return one cache entry */ CACHE_ENTRY, /** Need to return list of cache entries */ LIST_OF_CACHE_ENTRIES, /** Need to return list of values */ LIST_OF_VALUES, /** Need to return list of lists */ LIST_OF_LISTS, /** Need to return slice */ SLICE_OF_VALUES, /** Slice of cache entries. */ SLICE_OF_CACHE_ENTRIES, /** Slice of lists. */ SLICE_OF_LISTS } /** Type. */ private final Class<?> type; /** Sql. */ private final IgniteQuery qry; /** Cache. */ private final IgniteCache cache; /** Method. */ private final Method mtd; /** Metadata. */ private final RepositoryMetadata metadata; /** Factory. */ private final ProjectionFactory factory; /** Return strategy. */ private final ReturnStrategy returnStgy; /** * @param metadata Metadata. * @param qry Query. * @param mtd Method. * @param factory Factory. * @param cache Cache. */ public IgniteRepositoryQuery(RepositoryMetadata metadata, IgniteQuery qry, Method mtd, ProjectionFactory factory, IgniteCache cache) { type = metadata.getDomainType(); this.qry = qry; this.cache = cache; this.metadata = metadata; this.mtd = mtd; this.factory = factory; returnStgy = calcReturnType(mtd, qry.isFieldQuery()); } /** {@inheritDoc} */ @Override public Object execute(Object[] prmtrs) { Query qry = prepareQuery(prmtrs); QueryCursor qryCursor = cache.query(qry); return transformQueryCursor(prmtrs, qryCursor); } /** {@inheritDoc} */ @Override public QueryMethod getQueryMethod() { return new QueryMethod(mtd, metadata, factory); } /** * @param mtd Method. * @param isFieldQry Is field query. */ private ReturnStrategy calcReturnType(Method mtd, boolean isFieldQry) { Class<?> returnType = mtd.getReturnType(); if (returnType.isAssignableFrom(ArrayList.class)) { if (isFieldQry) { if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd)) return ReturnStrategy.LIST_OF_LISTS; } else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd)) return ReturnStrategy.LIST_OF_CACHE_ENTRIES; return ReturnStrategy.LIST_OF_VALUES; } else if (returnType == Slice.class) { if (isFieldQry) { if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd)) return ReturnStrategy.SLICE_OF_LISTS; } else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd)) return ReturnStrategy.SLICE_OF_CACHE_ENTRIES; return ReturnStrategy.SLICE_OF_VALUES; } else if (Cache.Entry.class.isAssignableFrom(returnType)) return ReturnStrategy.CACHE_ENTRY; else return ReturnStrategy.ONE_VALUE; } /** * @param cls Class 1. * @param mtd Method. */ private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) { Type[] actualTypeArguments = ((ParameterizedType)mtd.getGenericReturnType()).getActualTypeArguments(); if (actualTypeArguments.length == 0) return false; if (actualTypeArguments[0] instanceof ParameterizedType) { ParameterizedType type = (ParameterizedType)actualTypeArguments[0]; Class<?> type1 = (Class)type.getRawType(); return type1.isAssignableFrom(cls); } if (actualTypeArguments[0] instanceof Class) { Class typeArg = (Class)actualTypeArguments[0]; return typeArg.isAssignableFrom(cls); } return false; } /** * @param prmtrs Prmtrs. * @param qryCursor Query cursor. */ @Nullable private Object transformQueryCursor(Object[] prmtrs, QueryCursor qryCursor) { if (this.qry.isFieldQuery()) { Iterable<ArrayList> qryIter = (Iterable<ArrayList>)qryCursor; switch (returnStgy) { case LIST_OF_VALUES: ArrayList list = new ArrayList(); for (ArrayList entry : qryIter) list.add(entry.get(0)); return list; case ONE_VALUE: Iterator<ArrayList> iter = qryIter.iterator(); if (iter.hasNext()) return iter.next().get(0); return null; case SLICE_OF_VALUES: ArrayList content = new ArrayList(); for (ArrayList entry : qryIter) content.add(entry.get(0)); return new SliceImpl(content, (Pageable)prmtrs[prmtrs.length - 1], true); case SLICE_OF_LISTS: return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true); case LIST_OF_LISTS: return qryCursor.getAll(); default: throw new IllegalStateException(); } } else { Iterable<CacheEntryImpl> qryIter = (Iterable<CacheEntryImpl>)qryCursor; switch (returnStgy) { case LIST_OF_VALUES: ArrayList list = new ArrayList(); for (CacheEntryImpl entry : qryIter) list.add(entry.getValue()); return list; case ONE_VALUE: Iterator<CacheEntryImpl> iter1 = qryIter.iterator(); if (iter1.hasNext()) return iter1.next().getValue(); return null; case CACHE_ENTRY: Iterator<CacheEntryImpl> iter2 = qryIter.iterator(); if (iter2.hasNext()) return iter2.next(); return null; case SLICE_OF_VALUES: ArrayList content = new ArrayList(); for (CacheEntryImpl entry : qryIter) content.add(entry.getValue()); return new SliceImpl(content, (Pageable)prmtrs[prmtrs.length - 1], true); case SLICE_OF_CACHE_ENTRIES: return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true); case LIST_OF_CACHE_ENTRIES: return qryCursor.getAll(); default: throw new IllegalStateException(); } } } /** * @param prmtrs Prmtrs. * @return prepared query for execution */ @NotNull private Query prepareQuery(Object[] prmtrs) { Object[] parameters = prmtrs; String sql = qry.sql(); Query query; switch (qry.options()) { case SORTING: sql = IgniteQueryGenerator.addSorting(new StringBuilder(sql), (Sort)parameters[parameters.length - 1]).toString(); parameters = Arrays.copyOfRange(parameters, 0, parameters.length - 1); break; case PAGINATION: sql = IgniteQueryGenerator.addPaging(new StringBuilder(sql), (Pageable)parameters[parameters.length - 1]).toString(); parameters = Arrays.copyOfRange(parameters, 0, parameters.length - 1); break; } if (qry.isFieldQuery()) { SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(sql); sqlFieldsQry.setArgs(parameters); query = sqlFieldsQry; } else { SqlQuery sqlQry = new SqlQuery(type, sql); sqlQry.setArgs(parameters); query = sqlQry; } return query; } }