/* Copyright (C) 2004 - 2006 db4objects Inc. http://www.db4o.com
This file is part of the db4o open source object database.
db4o is free software; you can redistribute it and/or modify it under
the terms of version 2 of the GNU General Public License as published
by the Free Software Foundation and as clarified by db4objects' GPL
interpretation policy, available at
http://www.db4o.com/about/company/legalpolicies/gplinterpretation/
Alternatively you can write to db4objects, Inc., 1900 S Norfolk Street,
Suite 350, San Mateo, CA 94403, USA.
db4o 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, write to the Free Software Foundation, Inc.,
59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
package com.db4o.defragment;
import java.io.*;
import com.db4o.*;
import com.db4o.config.Configuration;
import com.db4o.ext.*;
import com.db4o.foundation.*;
import com.db4o.foundation.io.*;
import com.db4o.internal.*;
import com.db4o.internal.btree.*;
import com.db4o.internal.classindex.*;
import com.db4o.internal.handlers.*;
/**
* defragments database files.
*
* <br><br>db4o structures storage inside database files as free and occupied slots, very
* much like a file system - and just like a file system it can be fragmented.<br><br>
*
* The simplest way to defragment a database file:<br><br>
*
* <code>Defragment.defrag("sample.yap");</code><br><br>
*
* This will move the file to "sample.yap.backup", then create a defragmented
* version of this file in the original position, using a temporary file
* "sample.yap.mapping". If the backup file already exists, this will throw an
* exception and no action will be taken.<br><br>
*
* For more detailed configuration of the defragmentation process, provide a
* DefragmentConfig instance:<br><br>
*
* <code>DefragmentConfig config=new DefragmentConfig("sample.yap","sample.bap",new BTreeIDMapping("sample.map"));<br>
* config.forceBackupDelete(true);<br>
* config.storedClassFilter(new AvailableClassFilter());<br>
* config.db4oConfig(db4oConfig);<br>
* Defragment.defrag(config);</code><br><br>
*
* This will move the file to "sample.bap", then create a defragmented version
* of this file in the original position, using a temporary file "sample.map" for BTree mapping.
* If the backup file already exists, it will be deleted. The defragmentation
* process will skip all classes that have instances stored within the yap file,
* but that are not available on the class path (through the current
* classloader). Custom db4o configuration options are read from the
* {@link com.db4o.config.Configuration Configuration} passed as db4oConfig.
*
* <strong>Note:</strong> For some specific, non-default configuration settings like
* UUID generation, etc., you <strong>must</strong> pass an appropriate db4o configuration,
* just like you'd use it within your application for normal database operation.
*/
public class Defragment {
/**
* Renames the file at the given original path to a backup file and then
* builds a defragmented version of the file in the original place.
*
* @param origPath
* The path to the file to be defragmented.
* @throws IOException
* if the original file cannot be moved to the backup location
*/
public static void defrag(String origPath) throws IOException {
defrag(new DefragmentConfig(origPath), new NullListener());
}
/**
* Renames the file at the given original path to the given backup file and
* then builds a defragmented version of the file in the original place.
*
* @param origPath
* The path to the file to be defragmented.
* @param backupPath
* The path to the backup file to be created.
* @throws IOException
* if the original file cannot be moved to the backup location
*/
public static void defrag(String origPath, String backupPath)
throws IOException {
defrag(new DefragmentConfig(origPath, backupPath), new NullListener());
}
/**
* Renames the file at the configured original path to the configured backup
* path and then builds a defragmented version of the file in the original
* place.
*
* @param config
* The configuration for this defragmentation run.
* @throws IOException
* if the original file cannot be moved to the backup location
*/
public static void defrag(DefragmentConfig config) throws IOException {
defrag(config, new NullListener());
}
/**
* Renames the file at the configured original path to the configured backup
* path and then builds a defragmented version of the file in the original
* place.
*
* @param config
* The configuration for this defragmentation run.
* @param listener
* A listener for status notifications during the defragmentation
* process.
* @throws IOException
* if the original file cannot be moved to the backup location
*/
public static void defrag(DefragmentConfig config, DefragmentListener listener) throws IOException {
File backupFile = new File(config.backupPath());
if (backupFile.exists()) {
if (!config.forceBackupDelete()) {
throw new IOException("Could not use '" + config.backupPath()
+ "' as backup path - file exists.");
}
backupFile.delete();
}
File4.rename(config.origPath(), config.backupPath());
if(config.fileNeedsUpgrade()) {
upgradeFile(config);
}
DefragContextImpl context = new DefragContextImpl(config, listener);
int newClassCollectionID = 0;
int targetIdentityID = 0;
int targetUuidIndexID = 0;
try {
firstPass(context, config);
secondPass(context, config);
defragUnindexed(context);
newClassCollectionID = context.mappedID(context
.sourceClassCollectionID());
context.targetClassCollectionID(newClassCollectionID);
int sourceIdentityID = context
.databaseIdentityID(DefragContextImpl.SOURCEDB);
targetIdentityID = context.mappedID(sourceIdentityID,0);
targetUuidIndexID = context
.mappedID(context.sourceUuidIndexID(), 0);
} catch (CorruptionException exc) {
exc.printStackTrace();
} finally {
context.close();
}
if(targetIdentityID>0) {
setIdentity(config.origPath(), targetIdentityID, targetUuidIndexID, config.blockSize());
}
else {
listener.notifyDefragmentInfo(new DefragmentInfo("No database identity found in original file."));
}
}
private static void upgradeFile(DefragmentConfig config) {
File4.copy(config.backupPath(),config.tempPath());
Configuration db4oConfig=(Configuration)((Config4Impl)config.db4oConfig()).deepClone(null);
db4oConfig.allowVersionUpdates(true);
ObjectContainer db=Db4o.openFile(db4oConfig,config.tempPath());
db.close();
}
private static void defragUnindexed(DefragContextImpl context)
throws CorruptionException {
Iterator4 unindexedIDs = context.unindexedIDs();
while (unindexedIDs.moveNext()) {
final int origID = ((Integer) unindexedIDs.current()).intValue();
ReaderPair.processCopy(context, origID, new SlotCopyHandler() {
public void processCopy(ReaderPair readers)
throws CorruptionException {
ClassMetadata.defragObject(readers);
}
}, true);
}
}
private static void setIdentity(String targetFile, int targetIdentityID,
int targetUuidIndexID, int blockSize) {
LocalObjectContainer targetDB = (LocalObjectContainer) Db4o.openFile(DefragmentConfig
.vanillaDb4oConfig(blockSize), targetFile);
try {
Db4oDatabase identity = (Db4oDatabase) targetDB
.getByID(targetIdentityID);
targetDB.setIdentity(identity);
targetDB.systemData().uuidIndexId(targetUuidIndexID);
} finally {
targetDB.close();
}
}
private static void firstPass(DefragContextImpl context,
DefragmentConfig config) throws CorruptionException {
// System.out.println("FIRST");
pass(context, config, new FirstPassCommand());
}
private static void secondPass(final DefragContextImpl context,
DefragmentConfig config) throws CorruptionException {
// System.out.println("SECOND");
pass(context, config, new SecondPassCommand(config.objectCommitFrequency()));
}
private static void pass(DefragContextImpl context,
DefragmentConfig config, PassCommand command)
throws CorruptionException {
command.processClassCollection(context);
StoredClass[] classes = context
.storedClasses(DefragContextImpl.SOURCEDB);
for (int classIdx = 0; classIdx < classes.length; classIdx++) {
ClassMetadata yapClass = (ClassMetadata) classes[classIdx];
if (!config.storedClassFilter().accept(yapClass)) {
continue;
}
processYapClass(context, yapClass, command);
command.flush(context);
if(config.objectCommitFrequency()>0) {
context.targetCommit();
}
}
BTree uuidIndex = context.sourceUuidIndex();
if (uuidIndex != null) {
command.processBTree(context, uuidIndex);
}
command.flush(context);
context.targetCommit();
}
// TODO order of class index/object slot processing is crucial:
// - object slots before field indices (object slots register addresses for
// use by string indices)
// - class index before object slots, otherwise phantom btree entries from
// deletions appear in the source class index?!?
// reproducable with SelectiveCascadingDeleteTestCase and ObjectSetTestCase
// - investigate.
private static void processYapClass(final DefragContextImpl context,
final ClassMetadata curClass, final PassCommand command)
throws CorruptionException {
processClassIndex(context, curClass, command);
if (!parentHasIndex(curClass)) {
processObjectsForYapClass(context, curClass, command);
}
processYapClassAndFieldIndices(context, curClass, command);
}
private static boolean parentHasIndex(ClassMetadata curClass) {
ClassMetadata parentClass = curClass.i_ancestor;
while (parentClass != null) {
if (parentClass.hasIndex()) {
return true;
}
parentClass = parentClass.i_ancestor;
}
return false;
}
private static void processObjectsForYapClass(
final DefragContextImpl context, final ClassMetadata curClass,
final PassCommand command) {
final boolean withStringIndex = withFieldIndex(curClass);
context.traverseAll(curClass, new Visitor4() {
public void visit(Object obj) {
int id = ((Integer) obj).intValue();
try {
command.processObjectSlot(context, curClass, id,
withStringIndex);
} catch (CorruptionException e) {
e.printStackTrace();
}
}
});
}
private static boolean withFieldIndex(ClassMetadata clazz) {
Iterator4 fieldIter = clazz.fields();
while (fieldIter.moveNext()) {
FieldMetadata curField = (FieldMetadata) fieldIter.current();
if (curField.hasIndex()
&& (curField.getHandler() instanceof StringHandler)) {
return true;
}
}
return false;
}
private static void processYapClassAndFieldIndices(
final DefragContextImpl context, final ClassMetadata curClass,
final PassCommand command) throws CorruptionException {
int sourceClassIndexID = 0;
int targetClassIndexID = 0;
if (curClass.hasIndex()) {
sourceClassIndexID = curClass.index().id();
targetClassIndexID = context.mappedID(sourceClassIndexID, -1);
}
command.processClass(context, curClass, curClass.getID(),
targetClassIndexID);
}
private static void processClassIndex(final DefragContextImpl context,
final ClassMetadata curClass, final PassCommand command)
throws CorruptionException {
if (curClass.hasIndex()) {
BTreeClassIndexStrategy indexStrategy = (BTreeClassIndexStrategy) curClass
.index();
final BTree btree = indexStrategy.btree();
command.processBTree(context, btree);
}
}
static class NullListener implements DefragmentListener {
public void notifyDefragmentInfo(DefragmentInfo info) {
}
}
}