SimpleUnsafeTable.java

package org.microspace.table;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.microspace.annotation.IndexType;
import org.microspace.space.AutoTimeStamped;
import org.microspace.specific.CurrentTimeProvider;
import org.microspace.specific.MicroSpaceAccessor;
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.table.query.TableQuery;


/**
 * This map that can hold objects of a type for easy lookup using multiple keys.  
 * <p>
 * The objects that are written to this map will be serialized to freeze the indexes.
 * While serialized object is used used during for search, the reference
 * stored by the last write operation will be returned.
 * <p>
 * Searching this map is done using a pattern. 
 * The  class will be scanned for get methods with {@link org.microspace.annotation.Indexed Indexed} annotation.
 * All the fields of the pattern object must match, 
 * unless they are null.  
 * <p>
 * The objects are returned in FIFO order. Every write moves the update time of the object.
 * <p>
 * The type of the map that is used is determined by {@link IndexType IndexType}. If 
 * {@link IndexType IndexType.AUTO} is specified,
 * Comparable fields will use SortedMap and SortedSet, otherwise they will use HasMap and HasSet
 * to store the fields internally.
 * <p>
 * Here is a sample object that can be stored in this map.
<pre>
public class SampleObject implements Serializable {
	String id;
	Name name;
	int    age;
	
	{@literal @}Indexed(primaryKey=true, type=IndexType.HASH)
	constant public String getId() {
		return id;
	}
	{@literal @}Indexed
	constant public Name getName() {
		return name;
	}
	{@literal @}Indexed
	{@literal @}NullValue("-1");
	public int getAge() {
		return age;
	}
}
</pre>
 * You probably noticed that there is no constant keyword in java. That is java's problem.
 * This already have led us to a nasty, complex  world of getters / setters and observing 
 * classes which only partly solve this basic design fault. Making the class final does not
 * solve this problem, a final class can be modified internally without compiler warning.
 * <p>
 * I used this keyword here to indicate that you should swear to God that you wont modify 
 * what is returned by this method.
 * <p>
 * This class is not synchronized.
 *
 @author Gaspar Sinai - {@literal gaspar.sinai@microspace.org}
 * @version 2012-06-01
 */
public class SimpleUnsafeTable<T> implements Table<T> {
	
	/** The getter list of the class. */
	private Accessor<T> accessor;
	
	/** Objects can be looked up using their primary key here. */
	protected final IndexedMap<Object, Entry<T>> entries;
	
	/** Indexed fields of the objects can be looked up using their method name. If the value is null it is not indexed.*/
	protected final ColumnReferences<T> indexedColumns[];
	
	/** Every write increments this counter. */
	protected AtomicBigInteger updateCounter;

	protected final CurrentTimeProvider currentTimeProvider;
	
	/**
	 * Create a new BasicMap using our internal {@link Accessor GetterList}, 
	 * based on  {@link org.microspace.annotation.Indexed Indexed} annotations.
	 * @param clazz is the base class.
	 */
	public SimpleUnsafeTable(Class<T> clazz) {
		this (new MicroSpaceAccessor<T> (clazz));
	}

	/**
	 * Create a new BasicMap using our internal {@link Accessor GetterList}, 
	 * based on  {@link org.microspace.annotation.Indexed Indexed} annotations.
	 * @param clazz is the base class.
	 * @param currentTimeProvider The CurrentTimeProvider.
	 */
	public SimpleUnsafeTable(Class<T> clazz, CurrentTimeProvider currentTimeProvider) {
		this (new MicroSpaceAccessor<T> (clazz), currentTimeProvider);
	}
	
	/**
	 * Create a new BasicMap using external getters.
	 * @param accessor the external accessor
	 */
	public SimpleUnsafeTable (Accessor<T> accessor) {
		this (accessor, null);
	}
	
	/**
	 * Create a new BasicMap using external getters.
	 * @param accessor the external accessor
	 * @param currentTimeProvider The CurrentTimeProvider.
	 */
	@SuppressWarnings("unchecked")
	public SimpleUnsafeTable (Accessor<T> accessor, CurrentTimeProvider currentTimeProvider) {
		this.accessor = accessor;
		this.currentTimeProvider = currentTimeProvider;
		entries = new IndexedMap<Object, Entry<T>> (accessor.getPrimaryKeyGetSetPair().getIndexType());
		List<GetSetPair<T>> getters = accessor.getGetSetPairs();
		indexedColumns = new ColumnReferences [getters.size()];
		for (int i=0; i<getters.size(); i++) {
			Getter<T> fieldGetter = getters.get(i);
			if (fieldGetter.getIndexType() == null) continue;
			indexedColumns[i] = new ColumnReferences<T>(accessor.getPrimaryKeyGetSetPair(), fieldGetter);
		}
		updateCounter = new AtomicBigInteger(BigInteger.ZERO);
	}
	
	public int size () {
		return entries.size();
	}
	
	/* (non-Javadoc)
	 * @see org.microspace.map.MultikeyMap#write(T)
	 */
	public void write (T object) {
		Object id = accessor.getPrimaryKeyGetSetPair().get(object);
		if (id == null) {
			throw new IllegalArgumentException ("primary key is null");
		}
		autoTimestamp(object, currentTimeProvider);
		BigInteger nextCount = updateCounter.incrementAndGet();
		// GASPAR 2017-10-12 setentry also sets update counter, but the code is more readable.
		Entry<T> container = new Entry<T> (object, accessor, nextCount);
		setEntry (container);
	}
	
	public static void autoTimestamp (Object object, CurrentTimeProvider currentTimeProvider) {
		if (object instanceof AutoTimeStamped ) {
			AutoTimeStamped ts = (AutoTimeStamped) object;
			Date now = (currentTimeProvider == null)
					? null : currentTimeProvider.getCurrentTime();
			
            if (now == null) {
            	now = new Date();
            } else {
            	now = new Date  (now.getTime());
            }
			if (ts.getCreateTime() == null) {
				ts.setCreateTime(now);
				ts.setUpdateTime(now);
			} else {
				ts.setUpdateTime(now);
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see org.microspace.map.MultikeyMap#read(T)
	 */
	public T read (TableQuery<T> query) {
		List<T> mox = readMultiple (query);
		if (mox.size() > 0) return mox.get(0);
		return null;
	}
	
	/* (non-Javadoc)
	 * @see org.microspace.map.MultikeyMap#readMultiple(T)
	 */
	public List<T> readMultiple (TableQuery<T> query) {
		ArrayList<T> ret = new ArrayList<T> (0);
		if (entries.size() == 0) return ret;
		IndexedMap<Object, Entry<T>> map = readMultipleEntries (query);
		List<Entry<T>> list = sortAndLimitValidEntries (query, map);
		ret.ensureCapacity(list.size());
	    for (Entry<T> el : list){
	    	ret.add(el.getSpaceEntry());
	    }
		return ret;
	}
	
	/* (non-Javadoc)
	 * @see org.microspace.map.MultikeyMap#take(T)
	 */
	public T take (TableQuery<T> query) {
		IndexedMap<Object, Entry<T>> map = readMultipleEntries (query);
		List<Entry<T>> matches = sortAndLimitValidEntries (query, map);
		for (Entry<T> m : matches) {
			if (m.isRemoved()) continue;
			m.setRemoved(true);
			// This may or may not remove the entry completely.
			setEntry (m);
			return m.getSpaceEntry();
		}
		return null;
	}
	
	/* (non-Javadoc)
	 * @see org.microspace.map.MultikeyMap#takeMultiple(T)
	 */
	public List<T> takeMultiple (TableQuery<T> query) {
		ArrayList<T> ret = new ArrayList<T> (0);
		if (entries.size() == 0) return ret;
		IndexedMap<Object, Entry<T>> map = readMultipleEntries (query);
		List<Entry<T>> matches = sortAndLimitValidEntries (query, map);
		for (Entry<T> m : matches) {
			if (m.isRemoved()) continue;
			m.setRemoved(true);
			// This may or may not remove the entry completely.
			setEntry (m);
			ret.add (m.getSpaceEntry());
		}
		return ret;
	}
	

	public void clear () {
		entries.clear();
		updateCounter.set(BigInteger.ZERO);
		
	}

	/**
	 * This method is for internal use. It reads all objects that match pattern.
	 * @param query is the query.
	 * @return the raw containers in chronological order.
	 */
	IndexedMap<Object, Entry<T>> readMultipleEntries (TableQuery<T> query) {
		return query.getMatchingEntries (accessor, entries, indexedColumns); 
	 }
	 
	 /**
	  * Sort and limit set.
	  */
	 List<Entry<T>> sortAndLimitValidEntries (TableQuery<T> query, IndexedMap<Object, Entry<T>> map) {
		IndexedMap<Object, Entry<T>> validMap = new IndexedMap<>(accessor.getPrimaryKeyGetSetPair().getIndexType());
		for (Object key : map.keySet()) {
			Entry<T> localEntry = map.get(key);
			if (localEntry.isRemoved()) continue;
			validMap.put(key, localEntry);
		}
		return query.sortAndLimit(accessor, validMap);
	 }
	 
	
	/**
	 * The updateCounter at the end of the operation will show our update count.
	 * Container will be owned by this object.
	 * @param container will be moved or removed 
	 */
	public void setEntry (Entry<T> container) {
		BigInteger nextCount = updateCounter.incrementAndGet();
		container.setUpdateCount(nextCount);
		if (container.isRemoved()) {
			remove (container);
		} else {
			move (container);			
		}
	}
	/**
	 * Set the first entry without update count modification.
	 * @param container will be moved or removed 
	 */
	public void setFirstEntry (Entry<T> container) {
		if (container.isRemoved()) {
			remove (container);
		} else {
			move (container);			
		}
	}
	

	protected void move (Entry<T> container) {
		Object key = container.getPrimaryKey();
		Entry<T> oldEntry = entries.put (key, container);
		
		for (int i=0; i<indexedColumns.length; i++) {
			ColumnReferences<T> ixf = indexedColumns[i];
			if (ixf == null) continue;
			Object oldObject = oldEntry==null? null : oldEntry.getField(i);
			ixf.moveKey (key, oldObject, container.getField (i));
		}
	}
	/**
	 * This method is for internal use. Removes a raw container from the indexes. 
	 * @param container is the container.
	 */
	protected void remove (Entry<T> container) {
		remove (container, false);
	}
	
	/**
	 * Remove functionality with mark-only capability.
	 * @param container is the object container.
	 * @param markOnly is true if objects are not physically removed from the objects array.
	 */
	protected void remove (Entry<T> container, boolean markOnly) {
		Object id = container.getPrimaryKey();
		if (markOnly) {
			container.setRemoved(true);
			entries.put (id, container);
		} else {
			entries.remove (id);
		}
		for (int i=0; i<indexedColumns.length; i++) {
			ColumnReferences<T> ixf = indexedColumns[i];
			if (ixf == null) continue;
			ixf.moveKey (id, container.getField (i), null);
		}
	}
	
	public IndexedSet<Object> keySet() {
		return entries.keySet();
	}
	
	@Override
	public T readById (Object primaryKey) {
		Entry<T> entry = entries.get (primaryKey);
		if (entry == null || entry.isRemoved()) return null;
		return entry.getSpaceEntry();
	}
	
	Entry<T> getEntry (Object primaryKey) {
		if (primaryKey == null) {
			throw new IllegalArgumentException ("primary key is null");
		}
		return entries.get (primaryKey);
	}

	public Accessor<T> getAccessor () {
		return accessor;
	}

	/**
	 * Get the current Time provider.
	 * @return The current time provider for timestamps.
	 */
	public CurrentTimeProvider getCurrentTimeProvider() {
		return currentTimeProvider;
	}

	
}