diff --git a/src/net/java/sip/communicator/impl/contactlist/MclStorageManager.java b/src/net/java/sip/communicator/impl/contactlist/MclStorageManager.java index fdaf6bc1d..50221e1fd 100644 --- a/src/net/java/sip/communicator/impl/contactlist/MclStorageManager.java +++ b/src/net/java/sip/communicator/impl/contactlist/MclStorageManager.java @@ -90,6 +90,11 @@ public class MclStorageManager * A reference to the file containing the locally stored meta contact list. */ private File contactlistFile = null; + + /** + * A reference to the failsafe transaction used with the contactlist file. + */ + private FailSafeTransaction contactlistTrans = null; /** * A regerence to the MetaContactListServiceImpl that created and started us. @@ -283,33 +288,14 @@ void start(BundleContext bc, +". error was:" + ex.getMessage()); } - // try to see if any backup remains from the last execution - try - { - File backup = faService.getPrivatePersistentFile(fileName + ".bak"); - - // if the backup exists, simply use it as a normal file - if (backup.exists()) - { - FileInputStream in = new FileInputStream(backup); - FileOutputStream out = new FileOutputStream(contactlistFile); - - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) - { - out.write(buf, 0, len); - } - - in.close(); - out.close(); - - backup.delete(); - } - } - catch (Exception e) - { - logger.error("Failed to restore the backup contact list file", e); + // create the failsafe transaction and restore the file if needed + try { + contactlistTrans = new FailSafeTransaction(this.contactlistFile); + contactlistTrans.restoreFile(); + } catch (NullPointerException e) { + logger.error("the contactlist file is null", e); + } catch (IllegalStateException e) { + logger.error("The contactlist file can't be found", e); } try @@ -390,34 +376,12 @@ private void storeContactList0() throws IOException + isModified); if(isStarted()) { - // copy the contact list before write on it to ensure - // a safe modification - File backup = new File (contactlistFile.getAbsolutePath() - + ".bak.part"); - FileInputStream in = new FileInputStream(contactlistFile); - FileOutputStream out = new FileOutputStream(backup); - - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) - { - out.write(buf, 0, len); - } - - in.close(); - out.close(); - - // note the end of the transfert - File final_backup = new File(contactlistFile.getAbsolutePath() - + ".bak"); - - // this should not happen, but if it's the case, the rename - // operation can fail - if (final_backup.exists()) { - final_backup.delete(); + // begin a new transaction + try { + contactlistTrans.beginTransaction(); + } catch (IllegalStateException e) { + logger.error("the contactlist file is missing", e); } - - backup.renameTo(final_backup); // really write the modification OutputStream stream = new FileOutputStream(contactlistFile); @@ -425,8 +389,12 @@ private void storeContactList0() throws IOException stream); stream.close(); - // once done, delete the backup file - final_backup.delete(); + // commit the changes + try { + contactlistTrans.commit(); + } catch (IllegalStateException e) { + logger.error("the contactlist file is missing", e); + } } } diff --git a/src/net/java/sip/communicator/util/FailSafeTransaction.java b/src/net/java/sip/communicator/util/FailSafeTransaction.java new file mode 100644 index 000000000..4cd0612b7 --- /dev/null +++ b/src/net/java/sip/communicator/util/FailSafeTransaction.java @@ -0,0 +1,199 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +package net.java.sip.communicator.util; + +import java.io.*; + +/** + * A failsafe transaction utility class. By failsafe we mean here that the file + * concerned always stays in a coherent state. This class use the transactional + * model. + * + * @author Benoit Pradelle + */ +public class FailSafeTransaction +{ + + /** + * Original file used by the transaction + */ + private File file; + + /** + * Backup file used by the transaction + */ + private File backup; + + /** + * Extension of a partial file + */ + private static final String PART_EXT = ".part"; + + /** + * Extension of a backup copy + */ + private static final String BAK_EXT = ".bak"; + + /** + * Creates a new transaction. + * + * @param file The file associated with this transaction + * + * @throws NullPointerException if the file is null + */ + public FailSafeTransaction(File file) + throws NullPointerException + { + if (file == null) { + throw new NullPointerException("null file provided"); + } + + this.file = file; + this.backup = null; + } + + /** + * Ensure that the file accessed is in a coherent state. This function is + * useful to do a failsafe read without starting a transaction. + * + * @throws IllegalStateException if the file doesn't exists anymore + * @throws IOException if an IOException occurs during the file restoration + */ + public void restoreFile() + throws IllegalStateException, IOException + { + File back = new File(this.file.getAbsolutePath() + BAK_EXT); + + // if a backup copy is still present, simply restore it + if (back.exists()) { + failsafeCopy(back.getAbsolutePath(), + this.file.getAbsolutePath()); + + back.delete(); + } + } + + /** + * Begins a new transaction. If a transaction is already active, commits the + * changes and begin a new transaction. + * A transaction can be closed by a commit or rollback operation. + * When the transaction begins, the file is restored to a coherent state if + * needed. + * + * @return The fail-safe file in which every modification must be done. + * + * @throws IllegalStateException if the file doesn't exists anymore + * @throws IOException if an IOException occurs during the transaction + * creation + */ + public void beginTransaction() + throws IllegalStateException, IOException + { + // if the last transaction hasn't been closed, commit it + if (this.backup != null) { + this.commit(); + } + + // if needed, restore the file in its previous state + restoreFile(); + + this.backup = new File(this.file.getAbsolutePath() + BAK_EXT); + + // else backup the current file + failsafeCopy(this.file.getAbsolutePath(), + this.backup.getAbsolutePath()); + } + + /** + * Closes the transaction and commit the changes. Everything written in the + * file during the transaction is saved. + * + * @throws IllegalStateException if the file doesn't exists anymore + * @throws IOException if an IOException occurs during the operation + */ + public void commit() + throws IllegalStateException, IOException + { + if (this.backup == null) { + return; + } + + // simply delete the backup file + this.backup.delete(); + this.backup = null; + } + + /** + * Closes the transation and cancel the changes. Everything written in the + * file during the transaction is NOT saved. + * @throws IllegalStateException if the file doesn't exists anymore + * @throws IOException if an IOException occurs during the operation + */ + public void rollback() + throws IllegalStateException, IOException + { + if (this.backup == null) { + return; + } + + // restore the backup and delete it + failsafeCopy(this.backup.getAbsolutePath(), + this.file.getAbsolutePath()); + this.backup.delete(); + this.backup = null; + } + + /** + * Copy a file in a fail-safe way. The destination is created in an atomic + * way. + * + * @param from The file to copy + * @param to The copy to create + * + * @throws IllegalStateException if the file doesn't exists anymore + * @throws IOException if an IOException occurs during the operation + */ + private void failsafeCopy(String from, String to) + throws IllegalStateException, IOException + { + FileInputStream in = null; + FileOutputStream out = null; + + // to ensure a perfect copy, delete the destination if it exists + File toF = new File(to); + if (toF.exists()) { + toF.delete(); + } + + File ptoF = new File(to + PART_EXT); + if (ptoF.exists()) { + ptoF.delete(); + } + + try { + in = new FileInputStream(from); + out = new FileOutputStream(to + PART_EXT); + } catch (FileNotFoundException e) { + throw new IllegalStateException(e.getMessage()); + } + + // actually copy the file + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) + { + out.write(buf, 0, len); + } + + in.close(); + out.close(); + + // once done, rename the partial file to the final copy + ptoF.renameTo(toF); + } +} diff --git a/test/net/java/sip/communicator/slick/slickless/SlicklessTests.java b/test/net/java/sip/communicator/slick/slickless/SlicklessTests.java index d4eae48a0..e927afa27 100644 --- a/test/net/java/sip/communicator/slick/slickless/SlicklessTests.java +++ b/test/net/java/sip/communicator/slick/slickless/SlicklessTests.java @@ -43,6 +43,7 @@ public void start(BundleContext bundleContext) throws Exception addTestSuite(TestXMLUtils.class); addTestSuite(TestBase64.class); + addTestSuite(TestFailSafeTransaction.class); bundleContext.registerService(getClass().getName(), this, properties); logger.debug("Successfully registered " + getClass().getName()); diff --git a/test/net/java/sip/communicator/slick/slickless/util/TestFailSafeTransaction.java b/test/net/java/sip/communicator/slick/slickless/util/TestFailSafeTransaction.java new file mode 100644 index 000000000..4726d64ce --- /dev/null +++ b/test/net/java/sip/communicator/slick/slickless/util/TestFailSafeTransaction.java @@ -0,0 +1,229 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +package net.java.sip.communicator.slick.slickless.util; + +import java.io.*; +import net.java.sip.communicator.util.*; +import junit.framework.*; + +/** + * Tests for the fail safe transactions + * + * @author Benoit Pradelle + */ +public class TestFailSafeTransaction + extends TestCase +{ + /** + * Test data to write in the original file + */ + private static final String origData = "this is a test for the fail safe " + + "transaction ability in SIP Communicator"; + + /** + * Test data to add to the file + */ + private static final String addedData = " which is the greatest IM client " + + "in the world !"; + + /** + * Test data to never write in the file + */ + private static final String wrongData = "all the file is damaged now !"; + + /** + * The base for the name of the temp file + */ + private static String tempName = "wzsxedcrfv" + System.currentTimeMillis(); + + /** + * Tests the commit operation + */ + public void testCommit() { + try { + // setup a temp file + File temp = File.createTempFile(tempName + "a", null); + FileOutputStream out = new FileOutputStream(temp); + + out.write(origData.getBytes()); + + // write a modification during a transaction + FailSafeTransaction trans = new FailSafeTransaction(temp); + trans.beginTransaction(); + + out.write(addedData.getBytes()); + + trans.commit(); + + out.close(); + + // test if the two writes are ok + // file length + assertEquals("the file hasn't the right size after a commit", + temp.length(), + origData.length() + addedData.length()); + + FileInputStream in = new FileInputStream(temp); + byte[] buffer = new byte[in.available()]; + in.read(buffer); + in.close(); + String content = new String(buffer); + + // file content + assertEquals("the file content isn't correct", + origData + addedData, + content); + + temp.delete(); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + + /** + * Tests the rollback operation + */ + public void testRollback() { + try { + // setup a temp file + File temp = File.createTempFile(tempName + "b", null); + FileOutputStream out = new FileOutputStream(temp); + + out.write(origData.getBytes()); + + // write a modification during a transaction + FailSafeTransaction trans = new FailSafeTransaction(temp); + trans.beginTransaction(); + + out.write(wrongData.getBytes()); + + trans.rollback(); + + out.close(); + + // test if the two writes are ok + // file length + assertEquals("the file hasn't the right size after a commit", + temp.length(), + origData.length()); + + FileInputStream in = new FileInputStream(temp); + byte[] buffer = new byte[in.available()]; + in.read(buffer); + in.close(); + String content = new String(buffer); + + // file content + assertEquals("the file content isn't correct", + origData, + content); + + temp.delete(); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + + /** + * Tests if the file is commited when we start a new transaction + */ + public void testCommitOnReOpen() { + try { + // setup a temp file + File temp = File.createTempFile(tempName + "c", null); + FileOutputStream out = new FileOutputStream(temp); + + out.write(origData.getBytes()); + + // write a modification during a transaction + FailSafeTransaction trans = new FailSafeTransaction(temp); + trans.beginTransaction(); + + out.write(addedData.getBytes()); + + // this transaction isn't closed, it should commit the changes + trans.beginTransaction(); + + // just to be sure to clean everything + // the rollback must rollback nothing + trans.rollback(); + + out.close(); + + // test if the two writes are ok + // file length + assertEquals("the file hasn't the right size after a commit", + temp.length(), + origData.length() + addedData.length()); + + FileInputStream in = new FileInputStream(temp); + byte[] buffer = new byte[in.available()]; + in.read(buffer); + in.close(); + String content = new String(buffer); + + // file content + assertEquals("the file content isn't correct", + origData + addedData, + content); + + temp.delete(); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + + /** + * Tests if the file is rollback-ed if the transaction is never closed + */ + public void testRollbackOnFailure() { + try { + // setup a temp file + File temp = File.createTempFile(tempName + "d", null); + FileOutputStream out = new FileOutputStream(temp); + + out.write(origData.getBytes()); + + // write a modification during a transaction + FailSafeTransaction trans = new FailSafeTransaction(temp); + FailSafeTransaction trans2 = new FailSafeTransaction(temp); + trans.beginTransaction(); + + out.write(wrongData.getBytes()); + + // we suppose here that SC crashed without closing properly the + // transaction. When it restarts, the modification must have been + // rollback-ed + + trans2.restoreFile(); + + out.close(); + + // test if the two writes are ok + // file length + assertEquals("the file hasn't the right size after a commit", + temp.length(), + origData.length()); + + FileInputStream in = new FileInputStream(temp); + byte[] buffer = new byte[in.available()]; + in.read(buffer); + in.close(); + String content = new String(buffer); + + // file content + assertEquals("the file content isn't correct", + origData, + content); + + temp.delete(); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } +}