/*
* 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);
}
}