WhereQueryCache.java
package com.birbit.android.jobqueue.persistentQueue.sqlite;
import com.birbit.android.jobqueue.Constraint;
import com.birbit.android.jobqueue.TagConstraint;
import android.support.v4.util.LruCache;
import java.util.Collection;
/**
* Internal class to cache sql queries and statements.
*/
class WhereQueryCache {
private static final int INT_SIZE = 6;
private static final int BOOL_SIZE = 1;
private static final int TAG_TYPE = 0;
private static final int TAG_COUNT = TAG_TYPE + BOOL_SIZE + BOOL_SIZE;
private static final int GROUP_COUNT = TAG_COUNT + INT_SIZE;
private static final int JOB_COUNT = GROUP_COUNT + INT_SIZE;
private static final int EXCLUDE_RUNNING = JOB_COUNT + INT_SIZE;
private static final int TIME_LIMIT = EXCLUDE_RUNNING + BOOL_SIZE;
private static final int PENDING_CANCELLATIONS = TIME_LIMIT + BOOL_SIZE;
private static final int INT_LIMIT = 1 << INT_SIZE;
static final int DEADLINE_COLUMN_INDEX = 1;
static final int NETWORK_TYPE_COLUMN_INDEX = 2;
// TODO implement some query cacheable check for queries that have way too many parameters
private final LruCache<Long, Where> queryCache = new LruCache<Long, Where>(15) {
@Override
protected void entryRemoved(boolean evicted, Long key, Where oldValue, Where newValue) {
oldValue.destroy();
}
};
private final String sessionId;
public WhereQueryCache(long sessionId) {
this.sessionId = Long.toString(sessionId);
}
public Where build(Constraint constraint, Collection<String> pendingCancellations,
StringBuilder stringBuilder) {
final boolean cacheable = isCacheable(constraint);
final long cacheKey = cacheKey(constraint, pendingCancellations);
Where where = cacheable ? queryCache.get(cacheKey) : null;
if (where == null) {
// build it
where = createWhere(cacheKey, constraint, pendingCancellations, stringBuilder);
if (cacheable) {
queryCache.put(cacheKey, where);
}
}
fillWhere(constraint, where, pendingCancellations);
return where;
}
private void fillWhere(Constraint constraint, Where where,
Collection<String> pendingCancellations) {
int count = 0;
where.args[count++] = Long.toString(constraint.getNowInNs());
where.args[count++] = Integer.toString(constraint.getMaxNetworkType());
if (constraint.getTimeLimit() != null) {
where.args[count++] = Long.toString(constraint.getTimeLimit());
}
if (constraint.getTagConstraint() != null) {
for (String tag : constraint.getTags()) {
where.args[count++] = tag;
}
}
for (String group : constraint.getExcludeGroups()) {
where.args[count++] = group;
}
for (String jobId : constraint.getExcludeJobIds()) {
where.args[count++] = jobId;
}
for (String cancelled : pendingCancellations) {
where.args[count++] = cancelled;
}
if (constraint.excludeRunning()) {
where.args[count++] = sessionId;
}
if (count != where.args.length) {
throw new IllegalStateException("something is wrong with where query cache for "
+ where.query);
}
}
private Where createWhere(long cacheKey, Constraint constraint,
Collection<String> pendingCancellations, StringBuilder reusedStringBuilder) {
reusedStringBuilder.setLength(0);
int argCount = 0;
reusedStringBuilder
.append("( (")
// deadline 4ever check is necessary to filter these from next job queries
.append(DbOpenHelper.DEADLINE_COLUMN.columnName)
.append(" != ").append(Where.FOREVER)
.append(" AND ")
.append(DbOpenHelper.DEADLINE_COLUMN.columnName)
.append(" <= ?) OR ");
argCount ++;
reusedStringBuilder
.append(DbOpenHelper.REQUIRED_NETWORK_TYPE_OLUMN.columnName)
.append(" <= ?)");
argCount++;
if (constraint.getTimeLimit() != null) {
reusedStringBuilder
.append(" AND ")
.append(DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnName)
.append(" <= ?");
argCount++;
}
if (constraint.getTagConstraint() != null) {
if (constraint.getTags().isEmpty()) {
reusedStringBuilder.append(" AND 0 ");
} else {
reusedStringBuilder
.append(" AND ")
.append(DbOpenHelper.ID_COLUMN.columnName).append(" IN ( SELECT ")
.append(DbOpenHelper.TAGS_JOB_ID_COLUMN.columnName).append(" FROM ")
.append(DbOpenHelper.JOB_TAGS_TABLE_NAME).append(" WHERE ")
.append(DbOpenHelper.TAGS_NAME_COLUMN.columnName).append(" IN (");
SqlHelper.addPlaceholdersInto(reusedStringBuilder,
constraint.getTags().size());
reusedStringBuilder.append(")");
if (constraint.getTagConstraint() == TagConstraint.ANY) {
reusedStringBuilder.append(")");
} else if (constraint.getTagConstraint() == TagConstraint.ALL) {
reusedStringBuilder.append(" GROUP BY (`")
.append(DbOpenHelper.TAGS_JOB_ID_COLUMN.columnName).append("`)")
.append(" HAVING count(*) = ")
.append(constraint.getTags().size()).append(")");
} else {
// have this in place in case we change number of constraints
throw new IllegalArgumentException("unknown constraint " + constraint);
}
argCount += constraint.getTags().size();
}
}
if (!constraint.getExcludeGroups().isEmpty()) {
reusedStringBuilder
.append(" AND (")
.append(DbOpenHelper.GROUP_ID_COLUMN.columnName)
.append(" IS NULL OR ")
.append(DbOpenHelper.GROUP_ID_COLUMN.columnName)
.append(" NOT IN(");
SqlHelper.addPlaceholdersInto(reusedStringBuilder,
constraint.getExcludeGroups().size());
reusedStringBuilder.append("))");
argCount += constraint.getExcludeGroups().size();
}
if (!constraint.getExcludeJobIds().isEmpty()) {
reusedStringBuilder
.append(" AND ")
.append(DbOpenHelper.ID_COLUMN.columnName)
.append(" NOT IN(");
SqlHelper.addPlaceholdersInto(reusedStringBuilder,
constraint.getExcludeJobIds().size());
reusedStringBuilder.append(")");
argCount += constraint.getExcludeJobIds().size();
}
if (!pendingCancellations.isEmpty()) {
reusedStringBuilder
.append(" AND ")
.append(DbOpenHelper.ID_COLUMN.columnName)
.append(" NOT IN(");
SqlHelper.addPlaceholdersInto(reusedStringBuilder,
pendingCancellations.size());
reusedStringBuilder.append(")");
argCount += pendingCancellations.size();
}
if (constraint.excludeRunning()) {
reusedStringBuilder
.append(" AND ")
.append(DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnName)
.append(" != ?");
argCount++;
}
String[] args = new String[argCount];
//noinspection UnnecessaryLocalVariable
Where where = new Where(cacheKey, reusedStringBuilder.toString(), args);
return where;
}
private boolean isCacheable(Constraint constraint) {
return constraint.getTags().size() < INT_LIMIT &&
constraint.getExcludeGroups().size() < INT_LIMIT &&
constraint.getExcludeJobIds().size() < INT_LIMIT;
}
private long cacheKey(Constraint constraint, Collection<String> pendingCancelations) {
long key;
//noinspection PointlessBitwiseExpression
key = (constraint.getTagConstraint() == null ? 2 : constraint.getTagConstraint().ordinal()) << TAG_TYPE
| constraint.getTags().size() << TAG_COUNT
| constraint.getExcludeGroups().size() << GROUP_COUNT
| constraint.getExcludeJobIds().size() << JOB_COUNT
| (constraint.excludeRunning() ? 1 : 0) << EXCLUDE_RUNNING
| (constraint.getTimeLimit() == null ? 1 : 0) << TIME_LIMIT
| pendingCancelations.size() << PENDING_CANCELLATIONS;
return key;
}
}