/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.capedwarf.datastore.query; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Properties; import com.google.appengine.api.datastore.DatastoreNeedIndexException; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Projection; import com.google.appengine.api.datastore.PropertyProjection; import com.google.appengine.api.datastore.Query; import com.google.appengine.api.datastore.RawValue; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.hibernate.search.SearchException; import org.infinispan.query.CacheQuery; import org.infinispan.query.ProjectionConstants; import org.jboss.capedwarf.shared.config.IndexesXml; import org.jboss.capedwarf.shared.reflection.ReflectionUtils; /** * Handle query projections. * * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a> * @author <a href="mailto:mluksa@redhat.com">Marko Luksa</a> */ class Projections { private static final String TYPES_FIELD = "__capedwarf___TYPES___"; private static final int OFFSET = 2; private Properties bridges = new Properties(); Projections() { } /** * Apply GAE projections onto Cache projections. * * @param gaeQuery the GAE query * @param cacheQuery the cache query */ static void applyProjections(Query gaeQuery, CacheQuery cacheQuery, IndexesXml.Index index) { List<String> projections = getProjections(gaeQuery); if (projections.isEmpty() == false) { cacheQuery.projection(projections.toArray(new String[projections.size()])); if (!gaeQuery.isKeysOnly()) { if (index != null) { try { cacheQuery.enableFullTextFilter(index.getName()); } catch (SearchException e) { throw new DatastoreNeedIndexException("No matching index found (FullTextFilterName: " + index.getName() + ")"); } if (gaeQuery.getSortPredicates().isEmpty()) { addDefaultSort(cacheQuery, index); } } } } } public static void addDefaultSort(CacheQuery cacheQuery, IndexesXml.Index index) { if (index.getProperties().isEmpty()) { return; } List<SortField> sortFields = new ArrayList<>(); for (IndexesXml.Property property : index.getProperties()) { boolean reverse = "desc".equals(property.getDirection()); sortFields.add(new SortField(property.getName(), SortField.STRING, reverse)); } cacheQuery.sort(new Sort(sortFields.toArray(new SortField[sortFields.size()]))); } private static List<String> getProjections(Query gaeQuery) { List<String> projections = new ArrayList<String>(); if (gaeQuery.isKeysOnly()) { projections.add(ProjectionConstants.KEY); projections.add(TYPES_FIELD); projections.addAll(getPropertiesRequiredOnlyForSorting(gaeQuery)); } else if (gaeQuery.getProjections().size() > 0) { projections.add(ProjectionConstants.KEY); projections.add(TYPES_FIELD); for (Projection projection : gaeQuery.getProjections()) { projections.add(getPropertyName(projection)); } projections.addAll(getPropertiesRequiredOnlyForSorting(gaeQuery)); } return projections; } public static List<String> getPropertiesRequiredOnlyForSorting(Query gaeQuery) { List<String> list = new ArrayList<String>(); QueryResultProcessor processor = new QueryResultProcessor(gaeQuery); if (processor.isProcessingNeeded()) { for (String propertyName : processor.getPropertiesUsedInIn()) { if (isOnlyNeededForSorting(propertyName, gaeQuery)) { list.add(propertyName); } } } return list; } public static String getPropertyName(Projection projection) { if (projection instanceof PropertyProjection) { PropertyProjection propertyProjection = (PropertyProjection) projection; return propertyProjection.getName(); } else { throw new IllegalStateException("Unsupported projection type: " + projection.getClass()); } } /** * Store property's bridge. * * @param propertyName the property name * @param bridge the bridge */ void storePropertyBridge(String propertyName, Bridge bridge) { bridges.put(propertyName, String.valueOf(bridge.name())); } /** * Store bridges to document. * * @param document the Lucene document */ void finish(Document document) { try { StringWriter writer = new StringWriter(); bridges.store(writer, null); document.add(new Field(TYPES_FIELD, writer.toString(), Field.Store.YES, Field.Index.NO)); } catch (IOException e) { throw new IllegalArgumentException("Cannot store bridges!", e); } } /** * Read bridges. * * @param field the types field * @return bridges */ static Properties readPropertiesBridges(String field) { try { Properties bridges = new Properties(); bridges.load(new StringReader(field)); return bridges; } catch (IOException e) { throw new IllegalArgumentException("Cannot read bridges!", e); } } /** * Convert to entity. * * @param query the GAE query * @param result the current result * @return Entity instance */ static Entity convertToEntity(Query query, Object result) { if (result instanceof Entity) { return Entity.class.cast(result); } final Object[] row = (Object[]) result; final Entity entity = new Entity((Key) row[0]); if (row.length > 1) { final Properties bridges = readPropertiesBridges(row[1].toString()); int i = OFFSET; for (Projection projection : query.getProjections()) { if (projection instanceof PropertyProjection) { PropertyProjection pp = (PropertyProjection) projection; String propertyName = pp.getName(); Object value; Bridge bridge = getBridge(propertyName, bridges); if (mustBeWrappedInRawValue(pp)) { value = bridge.getValue((String) row[i]); value = newRawValue(value); } else { Class<?> type = pp.getType(); if (type != null && bridge.isAssignableTo(type) == false) { throw new IllegalArgumentException("Wrong projection type: " + pp); } value = convert(bridge, row[i]); } entity.setProperty(propertyName, value); } else { throw new IllegalStateException("Unsupported projection type: " + projection.getClass()); } i++; } for (String propertyName : getPropertiesRequiredOnlyForSorting(query)) { Object value = convert(propertyName, row[i], bridges); entity.setProperty(propertyName, value); i++; } } return entity; } private static boolean mustBeWrappedInRawValue(PropertyProjection propertyProjection) { return propertyProjection.getType() == null; } @SuppressWarnings("SimplifiableIfStatement") private static boolean isOnlyNeededForSorting(String propertyName, Query query) { if (query.isKeysOnly()) { return true; } else if (query.getProjections().size() > 0) { return isProjectedProperty(propertyName, query) == false; } else { return false; } } private static boolean isProjectedProperty(String propertyName, Query query) { for (Projection projection : query.getProjections()) { if (projection instanceof PropertyProjection) { PropertyProjection propertyProjection = (PropertyProjection) projection; if (propertyProjection.getName().equals(propertyName)) { return true; } } } return false; } private static Object convert(Bridge bridge, Object o) { if (o instanceof String) { return bridge.stringToObject(o.toString()); } return o; } private static Object convert(String propertyName, Object o, Properties bridges) { if (o instanceof String) { final Bridge bridge = getBridge(propertyName, bridges); return bridge.stringToObject(o.toString()); } return o; } private static RawValue newRawValue(Object value) { return ReflectionUtils.newInstance(RawValue.class, new Class[]{Object.class}, new Object[]{value}); } private static Bridge getBridge(String propertyName, Properties bridges) { String bridgeName = bridges.getProperty(propertyName); return Bridge.valueOf(bridgeName); } }