package com.venky.swf.db.table;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import com.venky.core.checkpoint.Mergeable;
import com.venky.core.collections.SequenceSet;
import com.venky.core.log.SWFLogger;
import com.venky.core.log.TimerStatistics.Timer;
import com.venky.core.util.ObjectUtil;
import com.venky.swf.db.Database;
import com.venky.swf.db.model.Model;
import com.venky.swf.routing.Config;
import com.venky.swf.sql.Expression;
import com.venky.swf.sql.Operator;
import com.venky.swf.sql.Select;
public class QueryCache implements Mergeable<QueryCache> , Cloneable{
private TreeSet<Record> cachedRecords = new TreeSet<Record>();
private HashMap<Expression, SequenceSet<Record>> queryCache = new HashMap<Expression, SequenceSet<Record>>();
private Table<? extends Model> table;
private String loggerName = null;
public Table<? extends Model> getTable() {
return table;
}
public boolean isEmpty(){
return cachedRecords.isEmpty();
}
public QueryCache(String tableName) {
this(Database.getTable(tableName));
}
private <M extends Model> QueryCache(Table<M> table){
this.table = table;
this.loggerName = QueryCache.class.getName() + "." + table.getModelClass().getSimpleName();
}
public void registerLockRelease(){
if (hasLockedRecords){
for (Record record: cachedRecords){
record.setLocked(false);
}
hasLockedRecords = false;
}
}
@SuppressWarnings("unchecked")
public QueryCache clone(){
try {
QueryCache clone = (QueryCache) super.clone();
clone.cachedRecords = (TreeSet<Record>) cachedRecords.clone();
clone.queryCache = (HashMap<Expression, SequenceSet<Record>>) queryCache.clone();
ObjectUtil.cloneValues(clone.cachedRecords);
ObjectUtil.cloneValues(clone.queryCache);
return clone;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
private static final Level defaultLevel = Level.FINE;
public SequenceSet<Record> getCachedResult(Expression where, int maxRecords, boolean locked) {
SWFLogger cat = Config.instance().getLogger(loggerName);
Timer timer = cat.startTimer(null,Config.instance().isTimerAdditive());
StringBuilder debug = new StringBuilder();
try {
if (where != null && where.isEmpty()) {
where = null;
}
boolean requireFilteringForLockedRecords = true;
String queryCriteria = (where == null ? "null" : where.getRealSQL());
SequenceSet<Record> result = queryCache.get(where);
if (cat.isLoggable(defaultLevel) && result != null ){
debug.append("Cache for " + getTable().getRealTableName() + " has criteria:" + queryCriteria);
}
if (result == null) {
synchronized (queryCache) {
result = queryCache.get(where);
if (cat.isLoggable(defaultLevel) && result != null ){
debug.append("Cache for " + getTable().getRealTableName() + " has criteria:" + queryCriteria);
}
boolean fullTableScanPerformed = queryCache.containsKey(null);
if (result == null){
if (cat.isLoggable(Level.FINER)){
debug.append("Cache for " + getTable().getRealTableName() + " does not have criteria:" + queryCriteria);
debug.append("\nChecking against available cachedRecords if there are enough records satifying the criteria");
debug.append("\nWas full table scanned ever?:" + fullTableScanPerformed);
}
if (fullTableScanPerformed) {
result = new SequenceSet<Record>();
filter(cachedRecords,where, result, Select.MAX_RECORDS_ALL_RECORDS,false);
setCachedResult(where, result);
}else if (maxRecords > 0 ){
SequenceSet<Record> tmpResult = new SequenceSet<Record>();
filter(cachedRecords,where, tmpResult, maxRecords,locked);
if (tmpResult.size() >= maxRecords){
result = tmpResult;
requireFilteringForLockedRecords = false;
}
}
}
}
}
if (result != null && locked && requireFilteringForLockedRecords){
debug.append(" Checking for locked records from cache!");
SequenceSet<Record> tmpResult = new SequenceSet<Record>();
filter(result, null, tmpResult, maxRecords, locked);
if (tmpResult.size() < result.size()){
result = null;
if (maxRecords > 0 && tmpResult.size() >= maxRecords){
result = tmpResult;
}
}
}
if (cat.isLoggable(defaultLevel)){
if (result == null || result.isEmpty()) {
debug.append("NOT ");
}
debug.append("Enough " + (locked ? "locked" : "" ) + "records found in cache.");
cat.log(defaultLevel,debug.toString());
}
return result;
}finally{
timer.stop();
}
}
private void filter( Set<Record> cachedRecords, Expression where, Set<Record> target, int maxRecords,boolean locked) {
for (Iterator<Record> i = cachedRecords.iterator(); i.hasNext() && (maxRecords == Select.MAX_RECORDS_ALL_RECORDS || target.size() < maxRecords) ;) {
Record m = i.next();
if (where == null || where.isEmpty() || where.eval(m)) {
if (!locked || (locked == m.isLocked())){
target.add(m);
}else if (locked && maxRecords == Select.MAX_RECORDS_ALL_RECORDS){
// m is not locked.
target.clear();
return;
}
}
}
}
public Record getCachedRecord(Record record) {
SortedSet<Record> tail = cachedRecords.tailSet(record);
if (tail == null || tail.isEmpty()) {
return null;
}
Record recordInCache = tail.first();
if (recordInCache.compareTo(record) == 0) {
return recordInCache;
}
return null;
}
public void setCachedResult(Expression where, SequenceSet<Record> result) {
if (where != null && where.isEmpty()) {
where = null;
}
if (!queryCache.containsKey(where)){
queryCache.put(where, result);
if (where == null){
for (Record record:result){
for (String column: getTable().getReflector().getIndexedColumns()) {
Expression indexWhere = getIndexWhereClause(record,column);
SequenceSet<Record> records = queryCache.get(indexWhere);
if (records == null){
records = new SequenceSet<Record>();
queryCache.put(indexWhere, records);
}
records.add(record);
}
}
}
}
}
private Expression getIdWhereClause(int id) {
return new Expression(getTable().getReflector().getPool(),"ID", Operator.EQ, id);
}
private Expression getIdWhereClause(Record record) {
if (record != null) {
Integer id = record.getId();
if (id != null) {
return getIdWhereClause(id);
}
}
throw new NullPointerException("Record doesnot have ID !");
}
private Expression getIndexWhereClause(Record record, String column) {
Object value = record.get(column);
if (value != null){
return new Expression(getTable().getReflector().getPool(),column,Operator.EQ,value);
}else {
return new Expression(getTable().getReflector().getPool(),column,Operator.EQ);
}
}
private boolean hasLockedRecords = false;
public boolean add(Record record) {
boolean ret = cachedRecords.add(record);
if (ret){
SequenceSet<Record> set = new SequenceSet<Record>();
set.add(record);
setCachedResult(getIdWhereClause(record),set);
/*
* This itself is a performance hog and not need most of the time.
for (Expression ukCondition: getTable().getReflector().getUniqueKeyConditions(record)){
setCachedResult(ukCondition, set);
}*/
}
hasLockedRecords = hasLockedRecords || record.isLocked();
return ret;
}
public boolean remove(Record record) {
boolean ret = cachedRecords.remove(record);
ret = ret || (queryCache.remove(getIdWhereClause(record)) != null);
return ret;
}
public void registerInsert(Record record) {
if (add(record)) {
for (Expression cacheKey : queryCache.keySet()) {
if (cacheKey == null || cacheKey.eval(record)) {
Set<Record> values = queryCache.get(cacheKey);
values.add(record);
}
}
}
}
public Record registerUpdate(Record updatedRecord) {
Record recordInCache = getCachedRecord(updatedRecord);
Record record = null;
if (recordInCache != null){
if (recordInCache != updatedRecord){ // No need to merge is the updatedRecord is already an object in the cache.
recordInCache.merge(updatedRecord);// Will get used only in nested transactions.
}
record = recordInCache;
}else {
record = updatedRecord;
}
for (Expression cacheKey: queryCache.keySet()){
if (recordInCache == null){
if (cacheKey == null || cacheKey.eval(record)){
queryCache.get(cacheKey).add(record); // Will not be added if already exists.
}
}else if (cacheKey != null && !cacheKey.eval(record)){
queryCache.get(cacheKey).remove(record); // Will not be removed if it doesnot exist.
}
}
if (recordInCache == null){
add(record);
}
return record;
}
public void registerDestroy(Record record) {
if (remove(record)) {
for (Expression cacheKey : queryCache.keySet()) {
if (cacheKey == null || cacheKey.eval(record)) {
Set<Record> values = queryCache.get(cacheKey);
values.remove(record);
}
}
}
}
// Called from db record insert.
public <M extends Model> void registerInsert(M m) {
registerInsert(m.getRawRecord());
}
public <M extends Model> void registerUpdate(M m) {
registerUpdate(m.getRawRecord());
}
// Called from db record destroy.
public <M extends Model> void registerDestroy(M m) {
registerDestroy(m.getRawRecord());
}
public void clear() {
queryCache.clear();
cachedRecords.clear();
}
public void merge(QueryCache completedTransactionCache) {
Map<Record,Record> mergedRecords = new HashMap<Record,Record>();
for (Expression exp: completedTransactionCache.queryCache.keySet()){
Set<Record> recentRecords = completedTransactionCache.queryCache.get(exp);
if (queryCache.containsKey(exp)){
Set<Record> oldRecords = new HashSet<Record>(queryCache.get(exp));
for (Record old:oldRecords){
if (!recentRecords.contains(old)){
registerDestroy(old);
}
}
}
SequenceSet<Record> currentRecords = new SequenceSet<Record>();
for (Record record:recentRecords){
Record mergedRecord = mergedRecords.get(record);
if (mergedRecord == null){
mergedRecord = registerUpdate(record);
mergedRecords.put(record,mergedRecord);
}
currentRecords.add(mergedRecord);
}
if (!queryCache.containsKey(exp)){
queryCache.put(exp, currentRecords);
}//else{
// registerUpdate would have fixed the map value against exp.
//}
}
}
public QueryCache copy(){
QueryCache cache = new QueryCache(table);
cache.cachedRecords.addAll(cachedRecords);
cache.queryCache.putAll(queryCache);
return cache;
}
}