TemplateQuery.java

package org.microspace.table.query;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.microspace.annotation.IndexType;
import org.microspace.exception.IllegalOperationException;
import org.microspace.space.SimpleSpace;
import org.microspace.table.Entry;
import org.microspace.table.column.Accessor;
import org.microspace.table.column.ColumnReferences;
import org.microspace.table.column.GetSetPair;
import org.microspace.table.column.Getter;
import org.microspace.table.column.IndexedMap;
import org.microspace.table.column.IndexedSet;
import org.microspace.util.PojoUtil;
import org.microspace.util.UniqueId;


/**
 * A lightweight query.
 * A query where null fields are ignored but set fields should match.
 * @author Gaspar Sinai - {@literal gaspar.sinai@microspace.org}
 * @version 2012-01-29
 */
public class TemplateQuery<T> implements TableQuery<T> {
	
	private final String id;
	private final T template;
	private final boolean indexedOnly;
	private final int maxEntries;
	private final UniqueId spaceId;

	/**
	 * An Inner class to arrange keys, In the order of the minimum matching keys
	 * @param <T>
	 */
	@SuppressWarnings("hiding")
	class IndexingOrder<T> {
		Integer size;
		ColumnReferences<T> columnReferences;
		Object property;
	}

	/**
	 * Create a query that is supposed to match non null fields.
	 * @param template is a POJO object that has a default constructor and Serializable fields.
	 */
	public TemplateQuery (T template) {
		this(template, Integer.MAX_VALUE, false);
	}
	
	/**
	 * Create a query that is supposed to match non null fields.
	 * @param template is a POJO object that has a default constructor and Serializable fields.
	 * @param spaceId is the spaceId restriction or null.
	 */
	public TemplateQuery (T template, UniqueId spaceId) {
		this(template, Integer.MAX_VALUE, false, spaceId);
	}
	
	/**
	 * Create a query that can be restricted to match indexed fields only.
	 * 
	 * The matcher throws an IllegalOperationException if we encounter a non indexed value.
	 * Good for debugging purpose.
	 * 
	 * @param template is a POJO object that has a default constructor and Serializable fields.
	 * @param maxEntries limits the maximum number of entries returned.
	 */
	public TemplateQuery (T template, int maxEntries) {
		this(template, maxEntries, false, null);
	}
	
	/**
	 * Create a query that can be restricted to match indexed fields only.
	 * 
	 * The matcher throws an IllegalOperationException if we encounter a non indexed value.
	 * Good for debugging purpose.
	 * 
	 * @param template is a POJO object that has a default constructor and Serializable fields.
	 * @param maxEntries limits the maximum number of entries returned.
	 * @param indexedOnly is true if only indexed keys are queried. This will speed up queries.
	 */
	public TemplateQuery (T template, int maxEntries, boolean indexedOnly) {
		this(template, maxEntries, indexedOnly, null);
	}
	
	/**
	 * Create a query that can be restricted to match indexed fields only.
	 * 
	 * The matcher throws an IllegalOperationException if we encounter a non indexed value.
	 * Good for debugging purpose.
	 * 
	 * @param template is a POJO object that has a default constructor and Serializable fields.
	 * @param maxEntries limits the maximum number of entries returned.
	 * @param indexedOnly is true if only indexed keys are queried. This will speed up queries.
	 * @param spaceId is the spaceId restriction or null.
	 */
	public TemplateQuery (T template, int maxEntries, boolean indexedOnly, UniqueId spaceId) {
		this.template = PojoUtil.copy(template);
		this.maxEntries = maxEntries;
		this.indexedOnly = indexedOnly;
		this.spaceId = spaceId;
		if (spaceId == null) {
			this.id = "TemplateQuery: " + PojoUtil.formatShort(template);
		} else {
			this.id = "TemplateQuery: " + spaceId.toString() + " " + PojoUtil.formatShort(template);
		}
	}
	
	/**
	 * Obtain the underlying template object.
	 * @return the template object.
	 */
	public T getTemplate() {
		return template;
	}

	/**
	 * Obtain query feature.
	 * @return query feature.
	 */
	public boolean isIndexedOnly() {
		return indexedOnly;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public IndexedMap<Object, Entry<T>> getMatchingEntries (Accessor<T> accessor, IndexedMap<Object, Entry<T>> entries, ColumnReferences<T> indexedColumns[]) {
		IndexedSet<Object> retKeys = new IndexedSet<>(accessor.getPrimaryKeyGetSetPair().getIndexType());
		IndexedMap<Object, Entry<T>> retMap = new IndexedMap<>(accessor.getPrimaryKeyGetSetPair().getIndexType());
		boolean first = true;
		List<IndexingOrder<?>> allRefs = new ArrayList<>();
		for (int i=0; i<indexedColumns.length; i++) {
			ColumnReferences<T> inf = indexedColumns[i];
			if (inf == null) continue; // Not indexed.
			Getter<T> getter = inf.getFieldGetter();
			Object property = getter.get (template);
			if (property == null || getter.isNull(property)) continue;
			int size = inf.size(property);
			if(size == 0){
				return retMap;
			}
			IndexingOrder <T> indexingOrder  = new IndexingOrder<>();
			indexingOrder.size = size;
			indexingOrder.columnReferences = inf;
			indexingOrder.property = property;
			allRefs.add(indexingOrder);
		}
		Collections.sort(allRefs, new Comparator<IndexingOrder<?>>() {
			@Override
			public int compare(IndexingOrder<?> o1, IndexingOrder<?> o2) {
				if(o1.size !=null && o2.size!=null) {
					return o1.size.compareTo(o2.size);
				}
				if(o1.size == null && o2.size == null){
					return 0;
				}
				if(o1.size == null){
					return -1;
				}
				return 1;
			}
		});
		for(IndexingOrder<?> indexingOrder : allRefs) {
			Object property = indexingOrder.property;
			ColumnReferences<?> columnReference = indexingOrder.columnReferences;
			if (first) {
				retKeys.addAll(columnReference.getMatchingKeys(property));
				first = false;
			} else {
				retKeys.retainAll(columnReference.getMatchingKeys(property));
			}
			if (retKeys.size() == 0) break;
		}
		// second round, check unIndexed fields.
		List<GetSetPair<T>> getSet = accessor.getGetSetPairs();
		for (int i=0; i<getSet.size(); i++) {
			ColumnReferences<T> inf = indexedColumns[i];
			if (inf != null) continue;
			Getter<T> getter = getSet.get(i);
			Object property = getter.get (template);
			if (property == null || getter.isNull(property)) continue;
			if (indexedOnly) {
				throw new IllegalOperationException ("This matcher is for indexed search only.");
			}
			if (first) {
				retKeys.addAll (getMatchingKeys(entries.keySet(), entries, property, i));
				first = false;
			} else {
				retKeys.retainAll (getMatchingKeys(retKeys, entries, property, i));
			}
			if (retKeys.size() == 0) break;
		}
		if (first) {
			retKeys.addAll(entries.keySet());
		}
		
		for (Object key : retKeys) {
			retMap.put(key, entries.get(key));
		}
		return retMap;
	}
	
	/**
	 * {@inheritDoc}
	 */	
	public List<Entry<T>> sortAndLimit (Accessor<T> accessor, 
				IndexedMap<Object, Entry<T>> entries) {
		ArrayList<Entry<T>> matches = new ArrayList<Entry<T>> (0);
		if (entries.size() == 0 || maxEntries <= 0) return matches;
		matches.ensureCapacity (entries.size());
		for (Object key : entries.keySet()) {
			matches.add(entries.get(key));
		}
		Collections.sort(matches);
		int size = Math.min(matches.size(), maxEntries);
		ArrayList<Entry<T>> ret = new ArrayList<Entry<T>> (size);
		for (int index=0; index<size; index++) {
			Entry<T> entry = matches.get(index);
			ret.add(entry);
		}
		return ret;
	}
	
	/**
	 * Get the matching keys of a not indexed field.
	 * @param from is the set to select from
	 * @param entries all entries
	 * @param property is the property to match.
	 * @param index is the field index.
	 * @return the objects that matched
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private IndexedSet<Object> getMatchingKeys (IndexedSet<Object> from, IndexedMap<Object, Entry<T>> entries, Object property, int index) {
		IndexedSet<Object> retKeys = new IndexedSet<Object> (from.getIndexType());
		IndexType indexType = IndexType.HASHED;
		if (property instanceof Comparable) {
			indexType = IndexType.SORTED;
		}
		for (Object key : from) {
			Entry<T> entry = entries.get(key);
			Object field = entry.getField(index);
			if (field == null) continue;
			switch (indexType){
			case HASHED:
			default:
				if (field.equals(property)) {
					retKeys.add(key);
				}
				break;
			case SORTED:
				if (((Comparable)field).compareTo((Comparable)property) == 0) {
					retKeys.add(key);
				}
				break;
			}
		}
		return retKeys;
	}
				
	public int getMaxEntries() {
		return maxEntries;
	}
	
	@SuppressWarnings("unchecked")
	public Class<T> getTableClass () {
		return (Class<T>) template.getClass();
	}
	
	@Override
    public String toString() {
    	return id;
    }

	@Override
	public boolean match(T object, Accessor<T> accessor) {
		return PojoUtil.match (template, object, accessor);
	}

	@Override
	public String getId() {
		return id;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void prepareQuery(SimpleSpace space) {
		
	}
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void finishQuery() {
	}

	@Override
	public UniqueId getSpaceId() {
		return spaceId;
	}
}