/*
* Copyright 2009-2016 Tilmann Zaeschke. All rights reserved.
*
* This file is part of ZooDB.
*
* ZooDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ZooDB is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ZooDB. If not, see <http://www.gnu.org/licenses/>.
*
* See the README and COPYING files for further information.
*/
package org.zoodb.internal.util;
import java.util.HashMap;
import java.util.Map;
import org.zoodb.api.impl.ZooPC;
import org.zoodb.internal.Session;
/**
* This class serves as a replacement for transient fields in persistent classes that
* cannot be allowed to be garbage collected. It provides <tt>transient</tt>
* behavior for a <code>static</code> field. <b>Instances of this class must
* always be referenced via the <tt>static</tt> modifier (see example below).
* It needs to be <code>static</code> because <code>transient</code> is not
* reliable (see below) and because it can't be 'normal' (persistent).</b>
* <br>
* In cases, where the data cannot be regenerated, this class
* can be used to store transient attributes safely for the lifetime
* of the Java VM.
* <p>
* The example below shows hot to use the <code>TransientField</code> class.
* <br>
* <code>
* class Example {<br>
* //A transient field of type "String"<br>
* private final static TransientField<String> _tempName =
* new TransientField<String>("default name");<br>
* private final static TransientField<Boolean> _tempBool =
* new TransientField<Boolean>(true);<br>
* private final static TransientField<Number> _tempNumber =
* new TransientField<Number>();<br>
* <br>
* public String getTempName() {<br>
* return _tempName.get(this);<br>
* }<br>
* <br>
* public void setTempName(String name) {<br>
* _tempName.set(this, name);<br>
* }<br>
* <br>
* public boolean getTempBoolean() {<br>
* return _tempBool.get(this);<br>
* }<br>
* <br>
* public void setTempBoolean(boolean b) {<br>
* _tempBool.set(this, b);<br>
* }<br>
* <br>
* public void setTempLong(Long l) {<br>
* _tempNumber.set(this, l);<br>
* }<br>
* <br>
* public void setTempDouble(Double d) {<br>
* _tempNumber.set(this, d);<br>
* }<br>
* <br>
* public void finalize() {<br>
* try {<br>
* _tempName.cleanIfTransient(this);<br>
* _tempBool.cleanIfTransient(this);<br>
* _tempNumber.cleanIfTransient(this);<br>
* } finally {<br>
* super.finalize();<br>
* }<br>
* }<br>
* </code>
* <p>
* This class is optimized to reference as few owners as possible. It stores
* owner/value pairs only if the value differs from the default value. If a
* value is set to the default value, then the owner/value pair is removed.
* <p>
* This class does not require the owner nor the value to be persistent class.
* <p>
* Each <code>TransientField</code> has a type and a value. The type is
* defined via generics <code><T></Code> in the declaration. The default
* value can be set via the constructor, otherwise it is <tt>null</tt>.
* <p>
* Internally in this class, persistent objects are identified via their OID,
* transient (all not persistent) objects are identified via their identity in
* the Java VM. This means persistent objects are unique with respect to their
* ObjectStore, whereas non-persistent objects are shared across the whole Java
* VM.<br>
* E.g. instances of an persistent Object X can be loaded in different Stores.
* The Java VM treats them as separate objects, and they do not share their
* values in the transient fields managed by TransientField.
* <p>
* The implementation of this class is not very performant due to the way the
* <code>ObjectStore</code> is accessed.
* <p>
* <b>Garbage Collection</b><br>
* To allow garbage collection of the values and the owner, please set the
* values to their default (method 1) or call the cleanup method (method 2).
* E.g.:<br>
* <code>
* class Example {<br>
* private final static TransientField<String> _tempName =
* new TransientField<String>();//default = null<br>
* <br>
* public void allowGarbageCollectionLaternative1() {<br>
* _tempName.set(this, null);<br>
* }<br>
* <br>
* public void allowGarbageCollectionAlternative2() {<br>
* _tempName.deregisterOwner(this);<br>
* <br>
* public void finalize() {<br>
* try {<br>
* _tempName.cleanIfTransient(this);<br>
* } finally {<br>
* super.finalize();<br>
* }<br>
* }<br>
* }<br>
* </code><br>
* Otherwise neither the owner nor the value can be garbage collected.
*
* @author Tilmann Zaeschke
* @param <T> Type of the fields value.
*/
public class TransientField<T> {
//A list of all Transient fields
private static final Map<TransientField<?>, Object> allFields =
new WeakIdentityHashMap<TransientField<?>, Object>();
//A map to have one OidMap per Transaction.
//Each OidMap maps all instances to their transient value.
//Note that the PersitenceManager may never be garbage collected as long as
//it is in this list. This is because the persistent objects in the OidMap
//appear to have a hard reference to the PersistenceManager.
//We make the map 'weak' anyway.
private final Map<Session, OidMap<Object, T>> txMap =
new WeakIdentityHashMap<Session, OidMap<Object, T>>();
//have a field to avoid garbage collection
private final OidMap<Object, T> noTx = new OidMapTrans<Object, T>();
{
txMap.put(null, noTx);
}
private final T defaultValue;
//The constructors are only called once per class/field, when the
//instance of Class object of the referencing class is loaded.
/**
* Initialises the field with the given value.
* @param defaultValue Default value, can be <code>null</code>.
*/
public TransientField(T defaultValue) {
super();
synchronized (allFields) {
allFields.put(this, null);
}
this.defaultValue = defaultValue;
}
private T getValue(Object key) {
if (key == null) {
throw new NullPointerException("Invalid value for owner: null");
}
Session pm = Session.getSession(key);
//try shortcut
if (txMap.containsKey(pm) && txMap.get(pm).containsKey(key)) {
return txMap.get(pm).get(key, pm);
}
//Okay, start searching
//This also treats case where it could be moved from one TX to another.
for (Session pm2: txMap.keySet()) {
OidMap<Object, T> om = txMap.get(pm2);
if (om.containsKey(key)) {
if (!pm.equals(pm2)) { //use != ?
//persistent state must have changed!
//ensure that _txMap.get(pm) exists!!!
if (!txMap.containsKey(pm)) {
txMap.put(pm, new OidMapPers<Object, T>());
}
txMap.get(pm).put(key, om.remove(key, pm2));
}
return txMap.get(pm).get(key, pm);
}
}
//okay, doesn't exist, so we return the default
return this.defaultValue;
}
private final void setValue(Object key, T value) {
if (key == null) {
throw new NullPointerException("Invalid value for owner: null");
}
//Works e.g. for null:
if (value == this.defaultValue) {
remove(key);
return;
}
if (value == null || this.defaultValue == null) {
//can't be equal, see previous 'if'.
put(key, value);
return;
}
//TODO remove this if clause
//Test non-persistent classes with .equals(): String, Long, FineTime..
if (!ZooPC.class.isAssignableFrom(value.getClass())) {
//check null
if (value.equals(this.defaultValue)) {
remove(key);
return;
}
}
put(key, value);
}
private void put(Object key, T value) {
if (key == null) {
throw new NullPointerException("Invalid value for owner: null");
}
Session pm = Session.getSession(key);
//ensure that _txMap.get(pm) exists!!!
if (!txMap.containsKey(pm)) {
if (pm == null) {
throw new IllegalStateException();
}
txMap.put(pm, new OidMapPers<Object, T>());
}
//check current persistence manager (may be null)
if (txMap.get(pm).containsKey(key)) {
txMap.get(pm).put(key, value);
return;
}
//Okay, start searching
//This also treats cases where it could be moved from one TX to another.
for (Session pm2: txMap.keySet()) {
OidMap<Object, T> om = txMap.get(pm2);
if (om.containsKey(key)) {
//persistent state must have changed!
om.remove(key, pm2);
break;
}
}
//okay, doesn't exist
txMap.get(pm).put(key, value);
}
private void remove(Object key) {
if (key == null) {
throw new NullPointerException("Invalid value for owner: null");
}
Session pm = Session.getSession(key);
//try shortcut
if (txMap.containsKey(pm) && txMap.get(pm).containsKey(key)) {
txMap.get(pm).remove(key, pm);
if (txMap.get(pm).isEmpty() && pm != null) {
txMap.remove(pm);
}
return;
}
//Okay, start searching
//This also treats cases where it could be moved from one TX to another.
for (Session pm2: txMap.keySet()) {
OidMap<Object, T> om = txMap.get(pm2);
if (om.containsKey(key)) {
txMap.get(pm2).remove(key, pm2);
if (txMap.get(pm2).isEmpty() && pm != null) {
txMap.remove(pm2);
}
return;
}
}
//okay, doesn't exist
}
/**
* Returns the object stored in this field.
* <code>null</code> is returned if the stored value is <code>null</code>.
* @param owner - The owner of this field.
* @return Returns the stored object.
*/
public final synchronized T get(Object owner) {
return getValue(owner);
}
/**
* This method stores the given value for the given
* owner/field combination.
* @param owner - The owner of this field.
* @param value - The value to set, can be <code>null</code>.
*/
public final synchronized void set(Object owner, T value) {
setValue(owner, value);
}
/**
* This command should be called at the end of the owners lifetime to
* free up associated objects and allow the owner to be garbage collected.
* <br>
* If the owner was never registered, this method simply returns.
* <br>
* Every owner should call this method for every referenced <code>
* TransientField</code>.
* <br>
* There is no harm in de-registering an object, a following call
* to <code>get()</code> will return the default value unless <code>
* set()</code> is used.
* <br>
* Another way of de-registering an object is by setting it to its
* default value, as supplied in the constructor. The object is
* automatically re-registered when the <code>set(...)</code> method
* is called with a value different from the default value.
* @param owner - The owner of this field.
*/
public final synchronized void deregisterOwner(Object owner) {
//Nothing happens if owner was never registered.
remove(owner);
}
/**
* Returns <code>null</code> if the object is transient or not
* persistent capable.
* @param obj
* @return <code>null</code> if the object is transient or not
* persistent capable.
*/
static final Object getObjectId(Object obj) {
if (obj == null) {
throw new NullPointerException();
}
if (!(obj instanceof ZooPC)) {
//non-peristent class
return null;
}
long id = Session.getObjectId(obj);
//non-persistent object --> null
return id >= 0 ? id : null;
}
/**
* <b>Not to be called by client.</b>
* This is internally called by the transaction manager.
* This method frees up all transient fields that are associated with the
* given Transaction.
* @param tx
*/
public static void deregisterTx(Session tx) {
if (tx == null) {
return;
}
synchronized (allFields) {
for (TransientField<?> f: allFields.keySet()) {
//returns null if key does not exist.
synchronized (f) {
if (f.txMap.containsKey(tx)) {
f.txMap.remove(tx).clear();
}
}
}
}
}
public static void deregisterPm(Session pm) {
if (pm == null) {
return;
}
synchronized (allFields) {
for (TransientField<?> f: allFields.keySet()) {
//returns null if key does not exist.
synchronized (f) {
if (f.txMap.containsKey(pm)) {
f.txMap.remove(pm).clear();
}
}
}
}
}
/**
* @param pm
* @return Number of objects registered with this persistence manager.
*/
public int size(Session pm) {
if (!txMap.containsKey(pm)) {
return 0;
}
return txMap.get(pm).size();
}
/**
* Each TransientField provides one OidMap per PersistenceManager.
* Each entry maps one object to it's field's transient value.
*
* @author Tilmann Zaeschke
*
* @param <K>
* @param <V>
*/
private static abstract class OidMap<K, V> {
abstract int size();
abstract void clear();
boolean isEmpty() { return size() == 0; }
abstract V get(Object key, Session pm);
abstract Object put(K key, V remove);
abstract V remove(Object key, Session pm);
abstract boolean containsKey(Object key);
}
final static class OidMapPers<K, V> extends OidMap<K, V> {
//Maps for persistent keys.
private HashMap<Object, OidMapEntry<V>> pMap =
new HashMap<Object, OidMapEntry<V>>();
final boolean containsKey(Object key) {
Object oid = TransientField.getObjectId(key);
if (oid == null) {
//Becoming transient: the Object will retain it's OID, so we
//can find previously set values. No need to throw an Excep.
//throw new IllegalArgumentException();
return false;
}
return pMap.containsKey(oid);
}
final V get(Object key, Session pm) {
Object oid = TransientField.getObjectId(key);
return pMap.get(oid).getValue(pm);
}
final Object put(K key, V value) {
Object oid = TransientField.getObjectId(key);
if (oid == null) {
throw new IllegalArgumentException();
}
return pMap.put(oid, new OidMapEntry<V>(value));
}
final V remove(Object key, Session pm) {
Object oid = TransientField.getObjectId(key);
if (oid == null) {
throw new IllegalArgumentException();
}
return pMap.remove(oid).getValue(pm);
}
int size() {
return pMap.size();
}
@Override
void clear() {
pMap.clear();
}
}
final static class OidMapTrans<K, V> extends OidMap<K, V> {
//Maps for transient and persistent keys.
//Allow garbage collection of keys!
private Map<K, OidMapEntry<V>> tMap =
new WeakIdentityHashMap<K, OidMapEntry<V>>();
final boolean containsKey(Object key) {
return tMap.containsKey(key);
}
final V get(Object key, Session pm) {
return tMap.get(key).getValue(pm);
}
final Object put(K key, V value) {
return tMap.put(key, new OidMapEntry<V>(value));
}
final V remove(Object key, Session pm) {
if (!tMap.containsKey(key)) {
return null;
}
return tMap.remove(key).getValue(pm);
}
int size() {
return tMap.size();
}
@Override
void clear() {
tMap.clear();
}
}
/**
* OidMapEntries can refer to persistent values or non-persistent values.
* Non-persistent values should not be garbage collectible.
* Persistent values should be garbage collectible, but only their OID is
* stored anyway.
*/
private final static class OidMapEntry<T> {
private boolean isPersistent = false;
private Object value;
OidMapEntry(Object val) {
if (val == null) {
value = null;
return;
}
Object oid = TransientField.getObjectId(val);
if (oid != null) {
value = oid;
isPersistent = true;
} else {
value = val;
}
}
@SuppressWarnings("unchecked")
final T getValue(Session pm) {
if (!isPersistent) {
return (T)value;
}
return (T)pm.getObjectById(value);
}
}
/**
* This method should be called in the <tt>finalize()</tt> method of all
* classes that use TransientFields.
* <p>
* This method cleans up the WeakHashMap that references the transient values
* of non-persistent objects.Because if the owner is not persistent, then the
* TransientField should be removed if the object is garbage collected.
* <p>
* It should be noted that contrary to the SUN javadoc, only the keys
* in a WeakHashMap are garbage collectible. But the Entries and values
* are <b>not</b> immediately available for garbage collection when the key
* is removed..
* Looking at the WeakHashMap code makes this obvious, a bug has been
* raised on SUN Java 1.5.0.
* @param owner
*/
public void cleanIfTransient(Object owner) {
//We do not check for PM here, because it is not really necessary.
noTx.remove(owner, null);
}
}