/*
Copyright (C) 2010 maik.jablonski@gmail.com
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jfix.db4o;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import jfix.db4o.engine.PersistenceEngine;
import jfix.functor.Command;
import jfix.functor.Functors;
import jfix.functor.Predicate;
import jfix.functor.Procedure;
import jfix.functor.Supplier;
import jfix.util.Reflections;
public class ObjectDatabase {
public static int MAINTENANCE_INTERVAL = 1 * 3600 * 1000;
private ReadWriteLock lock;
private ObjectRepository objectRepository;
private PersistenceEngine persistenceEngine;
private Timer maintenanceTimer;
private boolean maintenanceScheduled;
private boolean transactionInProgress;
private Map<Supplier, Object> supplierCache;
private List<Blob> blobsToSave;
private List<Blob> blobsToDelete;
public ObjectDatabase(PersistenceEngine persistenceEngine) {
this.lock = new ReentrantReadWriteLock();
this.objectRepository = new ObjectRepository();
this.supplierCache = new HashMap();
this.persistenceEngine = persistenceEngine;
}
public void open() {
lock.writeLock().lock();
try {
maintenanceScheduled = false;
transactionInProgress = false;
populateObjectRepository();
startMaintenanceTimer(MAINTENANCE_INTERVAL);
} finally {
lock.writeLock().unlock();
}
}
private void populateObjectRepository() {
String blobDirectory = getBlobDirectory();
for (Object obj : persistenceEngine.query()) {
if (obj instanceof Persistent) {
objectRepository.put((Persistent) obj);
if (obj instanceof Blob) {
((Blob) obj).initPath(blobDirectory);
}
}
}
}
private void startMaintenanceTimer(int period) {
maintenanceTimer = new Timer(true);
maintenanceTimer.schedule(new TimerTask() {
public void run() {
if (maintenanceScheduled) {
gc();
// need to lock for consistent backup
lock.readLock().lock();
try {
persistenceEngine.backup();
} finally {
lock.readLock().unlock();
}
maintenanceScheduled = false;
}
}
// Minimize IO by distributing backups for different databases
}, (int) (Math.random() * period), period);
}
private void stopMaintenanceTimer() {
if (maintenanceTimer != null) {
maintenanceTimer.cancel();
maintenanceTimer = null;
}
}
public String getBlobDirectory() {
return persistenceEngine.getBlobDirectory();
}
public void close() {
lock.writeLock().lock();
try {
stopMaintenanceTimer();
if (persistenceEngine != null) {
persistenceEngine.close();
}
persistenceEngine = null;
objectRepository = null;
supplierCache = null;
} finally {
lock.writeLock().unlock();
}
}
public void gc() {
// No lock so other threads can proceed with their work.
// If we miss an orphaned value this time, we will collect it on the
// next run.
for (Object orphanedValue : objectRepository
.getGarbage(Persistent.Value.class)) {
if (orphanedValue instanceof Persistent.Value) {
deleteDeliberately((Persistent) orphanedValue);
}
}
}
public <E> List<E> query(Class<E> clazz) {
lock.readLock().lock();
try {
return (List<E>) objectRepository.get(clazz);
} finally {
lock.readLock().unlock();
}
}
public <E> List<E> query(Class<E> clazz, Predicate<E> predicate) {
lock.readLock().lock();
try {
return Functors.filter((List<E>) objectRepository.get(clazz),
predicate);
} finally {
lock.readLock().unlock();
}
}
public <E> E queryUnique(Class<E> clazz, Predicate<E> predicate) {
lock.readLock().lock();
try {
List<E> result = query(clazz, predicate);
if (result == null || result.size() != 1) {
return null;
}
return result.get(0);
} finally {
lock.readLock().unlock();
}
}
public <E> boolean isUnique(E entity, Predicate<E> predicate) {
lock.readLock().lock();
try {
List<E> result = query((Class<E>) entity.getClass(), predicate);
if (result == null) {
return true;
}
if (result.size() == 1 && result.get(0) != entity) {
return false;
}
if (result.size() > 1) {
return false;
}
return true;
} finally {
lock.readLock().unlock();
}
}
public boolean isStored(Persistent object) {
lock.readLock().lock();
try {
return objectRepository.get(object.getClass()).contains(object);
} finally {
lock.readLock().unlock();
}
}
public List<Persistent> queryReferrers(Persistent reference) {
lock.readLock().lock();
try {
return new ArrayList<Persistent>(objectRepository
.getReferrers(reference));
} finally {
lock.readLock().unlock();
}
}
public <E> E query(Supplier<E> supplier) {
lock.readLock().lock();
try {
E result = (E) supplierCache.get(supplier);
if (result == null) {
result = supplier.get();
supplierCache.put(supplier, result);
}
return result;
} finally {
lock.readLock().unlock();
}
}
public void read(Command transaction) {
lock.readLock().lock();
try {
transaction.run();
} finally {
lock.readLock().unlock();
}
}
public void write(Command transaction) {
lock.writeLock().lock();
try {
transactionInProgress = true;
transaction.run();
if (blobsToSave != null) {
String blobDirectory = getBlobDirectory();
for (Blob blob : blobsToSave) {
blob.initPath(blobDirectory);
}
}
if (blobsToDelete != null) {
for (Blob blob : blobsToDelete) {
blob.getFile().delete();
}
}
persistenceEngine.commit();
} catch (Throwable e) {
persistenceEngine.rollback();
close();
e.printStackTrace();
throw new RuntimeException(e);
} finally {
blobsToSave = null;
blobsToDelete = null;
maintenanceScheduled = true;
transactionInProgress = false;
if (supplierCache != null) {
supplierCache.clear();
}
lock.writeLock().unlock();
}
}
public void save(final Persistent persistent) {
lock.writeLock().lock();
try {
if (transactionInProgress) {
traverseAndSave(persistent);
} else {
write(new Command() {
public void run() {
save(persistent);
}
});
}
} finally {
lock.writeLock().unlock();
}
}
public void delete(final Persistent persistent) {
lock.writeLock().lock();
try {
int refcount = queryReferrers(persistent).size();
if (refcount != 0) {
// Garbage collection to remove possible dangling references by
// values.
gc();
refcount = queryReferrers(persistent).size();
if (refcount != 0) {
throw new RuntimeException("Deletion not possible: \""
+ String.valueOf(persistent)
+ "\" is still referenced by " + refcount
+ " referrers.");
}
}
if (transactionInProgress) {
traverseAndDelete(persistent);
} else {
write(new Command() {
public void run() {
delete(persistent);
}
});
}
} finally {
lock.writeLock().unlock();
}
}
public void deleteDeliberately(final Persistent persistent) {
lock.writeLock().lock();
try {
if (transactionInProgress) {
traverseAndDelete(persistent);
} else {
write(new Command() {
public void run() {
deleteDeliberately(persistent);
}
});
}
} finally {
lock.writeLock().unlock();
}
}
private void traverseAndSave(Object candidate) {
if (candidate != null) {
traverseAndExecute(candidate, new Procedure() {
public void execute(Object object) {
traverseAndSave(object);
}
});
if (candidate instanceof Persistent) {
objectRepository.put(candidate);
persistenceEngine.save(candidate);
if (candidate instanceof Blob) {
if (blobsToSave == null) {
blobsToSave = new ArrayList();
}
blobsToSave.add((Blob) candidate);
}
return;
}
}
}
private void traverseAndDelete(Object candidate) {
if (candidate != null) {
// Don't cascade delete on values.
if (!(candidate instanceof Persistent.Value)) {
traverseAndExecute(candidate, new Procedure() {
public void execute(Object object) {
traverseAndDelete(object);
}
});
}
if (candidate instanceof Persistent) {
objectRepository.remove(candidate);
persistenceEngine.delete(candidate);
if (candidate instanceof Blob) {
if (blobsToDelete == null) {
blobsToDelete = new ArrayList();
}
blobsToDelete.add((Blob) candidate);
}
}
}
}
private void traverseAndExecute(Object candidate, Procedure procedure) {
try {
for (Field field : Reflections.getFields(candidate.getClass())) {
executeOnValues(field.get(candidate), procedure);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private void executeOnValues(Object candidate, Procedure procedure) {
if (candidate == null) {
return;
}
if (candidate instanceof Persistent.Value || candidate instanceof Blob) {
procedure.execute(candidate);
return;
}
if (candidate instanceof Persistent.Value[]
|| candidate instanceof Blob[]) {
for (Object arrayItem : (Object[]) candidate) {
executeOnValues(arrayItem, procedure);
}
return;
}
}
}