/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* !#
*/
package net.ontopia.persistence.proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* INTERNAL: A shared storage cache implementation. The cache uses a
* lookup index to store its cache entries. This cache is transaction
* independent.
*/
public class SharedCache implements StorageCacheIF, AccessRegistrarIF {
// Define a logging category.
static Logger log = LoggerFactory.getLogger(SharedCache.class.getName());
protected StorageIF storage;
protected Map<IdentityIF, CacheEntry> datacache;
protected long current_ticket_value;
protected TicketIF current_ticket;
protected int eviction;
private final boolean debug;
protected ClusterIF cluster;
protected long timestamp;
SharedCache(StorageIF storage, Map<IdentityIF, CacheEntry> datacache) {
this.storage = storage;
this.datacache = datacache;
this.debug = log.isDebugEnabled();
this.timestamp = System.currentTimeMillis();
this.current_ticket = new Ticket(current_ticket_value);
}
// -----------------------------------------------------------------------------
// Clustering
// -----------------------------------------------------------------------------
public void setCluster(ClusterIF cluster) {
this.cluster = cluster;
}
// -----------------------------------------------------------------------------
// StorageCacheIF implementation
// -----------------------------------------------------------------------------
public AccessRegistrarIF getRegistrar() {
return this;
}
public void close() {
// TODO: clear cache? and if persistent maybe delete physical
// cache. this might also be done on startup.
}
public boolean exists(StorageAccessIF access, IdentityIF identity) {
// Check to see if identity is registered here
if (datacache.get(identity) == null) {
// Need to check database for existence and load object. An
// exception will be thrown if the object does not exist in the
// database.
// This call will lead to a registerIdentity callback if the
// object exists in the data repository.
return access.loadObject(this, identity);
}
// Object exists
return true;
}
public Object getValue(StorageAccessIF access, IdentityIF identity, int field) {
CacheEntry fields = datacache.get(identity);
// Check to see if field is in the local cache
if (fields != null) {
synchronized (fields) {
if (fields.contains(field)) {
if (debug)
log.debug("Getting " + identity + " field from cache: " + field);
// Get field value from fields cache
return fields.getValue(field);
}
}
}
// Load field value(s) from database
if (debug)
log.debug("Getting " + identity + " field from store: " + field);
return access.loadField(this, identity, field);
}
public boolean isObjectLoaded(IdentityIF identity) {
// TODO: flag argument for also checking parent caches too?
if (datacache.get(identity) != null)
return true;
else
return false;
}
public boolean isFieldLoaded(IdentityIF identity, int field) {
// TODO: flag argument for also checking parent caches too?
// If identity does not exist, nor does field
CacheEntry fields = datacache.get(identity);
if (fields != null && fields.contains(field))
return true;
else
return false;
}
public void evictIdentity(IdentityIF identity, boolean notifyCluster) {
if (debug)
log.debug("SharedCache: evicting identity " + identity);
// unregister identity
datacache.remove(identity);
// notify cluster
if (cluster != null && notifyCluster) cluster.evictIdentity(identity);
}
public void evictFields(IdentityIF identity, boolean notifyCluster) {
if (debug)
log.debug("SharedCache: evicting fields " + identity);
CacheEntry fields = datacache.get(identity);
if (fields != null) {
// Drop all field values from the cache
fields.clear();
}
// notify cluster
if (cluster != null && notifyCluster) cluster.evictFields(identity);
}
public void evictField(IdentityIF identity, int field, boolean notifyCluster) {
CacheEntry fields = datacache.get(identity);
if (fields != null) {
// Drop field value from fields cache
fields.unsetValue(field, null); // NOTE: value is now null when unset
}
// notify cluster
if (cluster != null && notifyCluster) cluster.evictField(identity, field);
}
// -----------------------------------------------------------------------------
// prefetch
// -----------------------------------------------------------------------------
public int prefetch(StorageAccessIF access, Class<?> type, int field, int nextField, boolean traverse, Collection<IdentityIF> identities) {
long start = System.currentTimeMillis();
int num = identities.size();
if (debug)
log.debug("--LFM-P: s" + field + " " + num + " " + traverse + " " + type + " " + nextField);
if (traverse) {
// filter out identities that have their fields loaded, but
// not their next field loaded
Collection<IdentityIF> filtered = new ArrayList<IdentityIF>(num);
for (IdentityIF identity : identities) {
CacheEntry fields = datacache.get(identity);
if (fields == null || !fields.contains(field)) {
// prefetch if field not loaded
filtered.add(identity);
} else if (nextField >= 0) {
// prefetch if 1:1 field loaded, but next field not loaded
Object value = fields.getValue(field);
if (value != null && value instanceof IdentityIF) {
CacheEntry nfields = datacache.get((IdentityIF)value);
if (nfields == null || !nfields.contains(nextField)) {
filtered.add(identity);
}
}
}
}
num = filtered.size();
if (num > 1)
access.loadFieldMultiple(this, filtered, null, type, field);
} else {
// filter out identities that already have their *fields* loaded
Collection<IdentityIF> filtered = new ArrayList<IdentityIF>(num);
for (IdentityIF identity : identities) {
if (!isFieldLoaded(identity, field))
filtered.add(identity);
}
num = filtered.size();
if (num > 1)
access.loadFieldMultiple(this, filtered, null, type, field);
}
if (debug)
log.debug("--LFM-P: e" + field + " " + num + " (" + (System.currentTimeMillis() - start) + " ms)");
return num;
}
// -----------------------------------------------------------------------------
// AccessRegistrarIF implementation
// -----------------------------------------------------------------------------
public IdentityIF createIdentity(Class<?> type, long key) {
// do identity interning
IdentityIF identity = new LongIdentity(type, key);
CacheEntry entry = datacache.get(identity);
return (entry == null ? identity : entry.getIdentity());
}
public IdentityIF createIdentity(Class<?> type, Object key) {
IdentityIF identity = new AtomicIdentity(type, key);
CacheEntry entry = datacache.get(identity);
return (entry == null ? identity : entry.getIdentity());
}
public IdentityIF createIdentity(Class<?> type, Object[] keys) {
IdentityIF identity = new Identity(type, keys);
CacheEntry entry = datacache.get(identity);
return (entry == null ? identity : entry.getIdentity());
}
public void registerIdentity(TicketIF ticket, IdentityIF identity) {
// validate ticket
if (!ticket.isValid()) return;
if (debug) log.debug("Registering identity " + identity);
// register identity if we don't already have it.
if (datacache.get(identity) == null) {
//! System.out.println("+I " + identity);
// register with cache
datacache.put(WrappedIdentity.wrap(identity), new CacheEntry(identity, getFieldsCount(identity.getType())));
}
}
public void registerField(TicketIF ticket, IdentityIF identity, int field, Object value) {
// validate ticket
if (!ticket.isValid()) return;
// Note: object identity should be registered already.
if (debug)
log.debug("Registering " + identity + " field " + field + "=" + value);
CacheEntry fields = datacache.get(identity);
if (fields == null) {
IdentityIF wrappedIdentity = WrappedIdentity.wrap(identity);
// create new cache entry
fields = new CacheEntry(wrappedIdentity, getFieldsCount(wrappedIdentity.getType()));
fields.setValue(field, value);
// register with cache
datacache.put(wrappedIdentity, fields);
} else {
fields.setValue(field, value);
}
}
// -----------------------------------------------------------------------------
// Tickets
// -----------------------------------------------------------------------------
public TicketIF getTicket() {
return current_ticket;
}
public synchronized void registerEviction() {
eviction++;
this.current_ticket = new Ticket(++current_ticket_value);
}
public synchronized void releaseEviction() {
eviction--;
}
private synchronized int getEvictionCount() {
return eviction;
}
private synchronized long getCurrentTicket() {
return current_ticket_value;
}
private synchronized boolean isRunningEviction() {
return (eviction > 0);
}
private class Ticket implements TicketIF {
private long value;
private Ticket(long value) {
this.value = value;
}
public boolean isValid() {
return value == current_ticket_value && !isRunningEviction();
}
}
// -----------------------------------------------------------------------------
// CacheEntry initialization
// -----------------------------------------------------------------------------
protected Map<Object, Integer> field_counts = new HashMap<Object, Integer>();
protected int getFieldsCount(Class<?> type) {
synchronized (field_counts) {
Integer count = field_counts.get(type);
if (count != null)
return count.intValue();
// Otherwise compute the fields count
ClassInfoIF cinfo = storage.getMapping().getClassInfo(type);
int field_count = cinfo.getValueFieldInfos().length;
// The field counts is a map between Class and the number of
// fields that they have. This number is used to allocate room for
// field metata and field values in the data cache entries.
field_counts.put(type, new Integer(field_count));
return field_count;
}
}
// -----------------------------------------------------------------------------
// Cache reset + statistics
// -----------------------------------------------------------------------------
public void clear(boolean notifyCluster) {
this.datacache.clear();
if (cluster != null && notifyCluster) {
cluster.clearDatacache();
}
}
public void writeReport(java.io.Writer out, boolean dumpCache) throws java.io.IOException {
try {
// lock data cache while generating report
Map<Object, Integer> stats = new HashMap<Object, Integer>();
// generate statistics
int size = 0;
synchronized (datacache) {
for (IdentityIF key : datacache.keySet()) {
if (key == null) continue;
size++;
if (!stats.containsKey(key.getType())) {
stats.put(key.getType(), new Integer(1));
} else {
Integer cnt = stats.get(key.getType());
stats.put(key.getType(), new Integer(cnt.intValue() + 1));
}
}
}
out.write("<p>Cache size: " + size + "<br>\n");
out.write("Eviction: " + getEvictionCount() + " Ticket: " + getCurrentTicket() + "<br>\n");
out.write("Created: " + new Date(timestamp) + " (" + (System.currentTimeMillis()-timestamp) + " ms)</p>\n");
// output statistics
out.write("<table>\n");
for (Object key : stats.keySet()) {
Integer val = stats.get(key);
out.write("<tr><td>");
out.write((key == null ? "null" : StringUtils.escapeHTMLEntities(key.toString())));
out.write("</td><td>");
out.write((val == null ? "null" : StringUtils.escapeHTMLEntities(val.toString())));
out.write("</td></tr>\n");
}
out.write("</table><br>\n");
if (dumpCache) {
out.write("<table>\n");
synchronized (datacache) {
for (Object key : datacache.keySet()) {
if (key == null) continue;
CacheEntry val = datacache.get(key);
out.write("<tr><td>");
out.write((key == null ? "null" : StringUtils.escapeHTMLEntities(key.toString())));
out.write("</td><td>");
out.write((val == null ? "null" : StringUtils.escapeHTMLEntities(val.toString())));
out.write("</td></tr>\n");
}
out.write("</table><br>\n");
}
}
} catch (java.io.IOException e) {
throw new OntopiaRuntimeException(e);
}
}
// -----------------------------------------------------------------------------
// Misc
// -----------------------------------------------------------------------------
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("proxy.SharedCache@");
sb.append(System.identityHashCode(this));
return sb.toString();
}
}