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;
}
}