Coverage Report - org.jaudiotagger.audio.generic.AudioFileWriter
 
Classes in this File Line Coverage Branch Coverage Complexity
AudioFileWriter
56%
89/160
41%
30/74
10
 
 1  
 /*
 2  
  * Entagged Audio Tag library
 3  
  * Copyright (c) 2003-2005 Rapha�l Slinckx <raphael@slinckx.net>
 4  
  * 
 5  
  * This library is free software; you can redistribute it and/or
 6  
  * modify it under the terms of the GNU Lesser General Public
 7  
  * License as published by the Free Software Foundation; either
 8  
  * version 2.1 of the License, or (at your option) any later version.
 9  
  *  
 10  
  * This library is distributed in the hope that it will be useful,
 11  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
  * Lesser General Public License for more details.
 14  
  * 
 15  
  * You should have received a copy of the GNU Lesser General Public
 16  
  * License along with this library; if not, write to the Free Software
 17  
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 18  
  */
 19  
 package org.jaudiotagger.audio.generic;
 20  
 
 21  
 import org.jaudiotagger.audio.AudioFile;
 22  
 import org.jaudiotagger.audio.exceptions.CannotReadException;
 23  
 import org.jaudiotagger.audio.exceptions.CannotWriteException;
 24  
 import org.jaudiotagger.audio.exceptions.ModifyVetoException;
 25  
 import org.jaudiotagger.audio.exceptions.UnableToRenameFileException;
 26  
 import org.jaudiotagger.logging.ErrorMessage;
 27  
 import org.jaudiotagger.tag.Tag;
 28  
 
 29  
 import java.io.File;
 30  
 import java.io.IOException;
 31  
 import java.io.RandomAccessFile;
 32  
 import java.util.logging.Level;
 33  
 import java.util.logging.Logger;
 34  
 
 35  
 /**
 36  
  * This abstract class is the skeleton for tag writers.
 37  
  * <p/>
 38  
  * <p>It handles the creation/closing of the randomaccessfile objects and then call the subclass
 39  
  * method writeTag or deleteTag. These two method have to be implemented in the
 40  
  * subclass.
 41  
  *
 42  
  * @author Raphael Slinckx
 43  
  * @version $Id: AudioFileWriter.java,v 1.18 2008/11/28 22:14:44 paultaylor Exp $
 44  
  * @since v0.02
 45  
  */
 46  336
 public abstract class AudioFileWriter
 47  
 {
 48  
     private static final String TEMP_FILENAME_SUFFIX = ".tmp";
 49  
     private static final String WRITE_MODE = "rw";
 50  
     private static final int MINIMUM_FILESIZE = 150;
 51  
 
 52  
     // Logger Object
 53  42
     public static Logger logger = Logger.getLogger("org.jaudiotagger.audio.generic");
 54  
 
 55  
     /**
 56  
      * If not <code>null</code>, this listener is used to notify the listener
 57  
      * about modification events.<br>
 58  
      */
 59  336
     private AudioFileModificationListener modificationListener = null;
 60  
 
 61  
 
 62  
     /**
 63  
      * Delete the tag (if any) present in the given file
 64  
      *
 65  
      * @param af The file to process
 66  
      * @throws CannotWriteException if anything went wrong
 67  
      */
 68  
     public synchronized void delete(AudioFile af) throws CannotReadException, CannotWriteException
 69  
     {
 70  17
         if (!af.getFile().canWrite())
 71  
         {
 72  0
             throw new CannotWriteException(ErrorMessage.GENERAL_DELETE_FAILED.getMsg(af.getFile().getPath()));
 73  
         }
 74  
 
 75  17
         if (af.getFile().length() <= MINIMUM_FILESIZE)
 76  
         {
 77  0
             throw new CannotWriteException(ErrorMessage.GENERAL_DELETE_FAILED.getMsg(af.getFile().getPath()));
 78  
         }
 79  
 
 80  17
         RandomAccessFile raf = null;
 81  17
         RandomAccessFile rafTemp = null;
 82  17
         File tempF = null;
 83  
 
 84  
         //Will be set to true on VetoException, causing the finally block to discard the tempfile.
 85  17
         boolean revert = false;
 86  
 
 87  
         try
 88  
         {
 89  
 
 90  17
             tempF = File.createTempFile(af.getFile().getName().replace('.', '_'), TEMP_FILENAME_SUFFIX, af.getFile().getParentFile());
 91  17
             rafTemp = new RandomAccessFile(tempF, WRITE_MODE);
 92  17
             raf = new RandomAccessFile(af.getFile(), WRITE_MODE);
 93  17
             raf.seek(0);
 94  17
             rafTemp.seek(0);
 95  
 
 96  
             try
 97  
             {
 98  17
                 if (this.modificationListener != null)
 99  
                 {
 100  17
                     this.modificationListener.fileWillBeModified(af, true);
 101  
                 }
 102  17
                 deleteTag(raf, rafTemp);
 103  17
                 if (this.modificationListener != null)
 104  
                 {
 105  17
                     this.modificationListener.fileModified(af, tempF);
 106  
                 }
 107  
             }
 108  0
             catch (ModifyVetoException veto)
 109  
             {
 110  0
                 throw new CannotWriteException(veto);
 111  17
             }
 112  
 
 113  17
         }
 114  0
         catch (Exception e)
 115  
         {
 116  0
             revert = true;
 117  0
             throw new CannotWriteException("\"" + af.getFile().getAbsolutePath() + "\" :" + e, e);
 118  
         }
 119  
         finally
 120  
         {
 121  
             // will be set to the remaining file.
 122  0
             File result = af.getFile();
 123  
             try
 124  
             {
 125  17
                 if (raf != null)
 126  
                 {
 127  17
                     raf.close();
 128  
                 }
 129  17
                 if (rafTemp != null)
 130  
                 {
 131  17
                     rafTemp.close();
 132  
                 }
 133  
 
 134  
 
 135  17
                 if (tempF.length() > 0 && !revert)
 136  
                 {
 137  15
                     boolean deleteResult = af.getFile().delete();
 138  15
                     if (deleteResult == false)
 139  
                     {
 140  0
                         logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_ORIGINAL_FILE.getMsg(af.getFile().getPath(), tempF.getPath()));
 141  0
                         throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_ORIGINAL_FILE.getMsg(af.getFile().getPath(), tempF.getPath()));
 142  
                     }
 143  15
                     boolean renameResult = tempF.renameTo(af.getFile());
 144  15
                     if (renameResult == false)
 145  
                     {
 146  0
                         logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE.getMsg(af.getFile().getPath(), tempF.getPath()));
 147  0
                         throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE.getMsg(af.getFile().getPath(), tempF.getPath()));
 148  
                     }
 149  15
                     result = tempF;
 150  
 
 151  
                     //If still exists we can now delete
 152  15
                     if(tempF.exists())
 153  
                     {
 154  0
                         if(!tempF.delete())
 155  
                         {
 156  
                             //Non critical failed deletion
 157  0
                             logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE.getMsg(tempF.getPath()));
 158  
                         }
 159  
                     }
 160  15
                 }
 161  
                 else
 162  
                 {
 163  
                     //It was created but never used
 164  2
                     if(!tempF.delete())
 165  
                     {
 166  
                         //Non critical failed deletion
 167  0
                         logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE.getMsg(tempF.getPath()));
 168  
                     }
 169  
                 }
 170  
             }
 171  0
             catch (Exception ex)
 172  
             {
 173  0
                 logger.severe("AudioFileWriter exception cleaning up delete:" + af.getFile().getPath() + " or" + tempF.getAbsolutePath() + ":" + ex);
 174  17
             }
 175  
             // Notify listener
 176  17
             if (this.modificationListener != null)
 177  
             {
 178  17
                 this.modificationListener.fileOperationFinished(result);
 179  
             }
 180  34
         }
 181  17
     }
 182  
 
 183  
     /**
 184  
      * Delete the tag (if any) present in the given randomaccessfile, and do not
 185  
      * close it at the end.
 186  
      *
 187  
      * @param raf     The source file, already opened in r-write mode
 188  
      * @param tempRaf The temporary file opened in r-write mode
 189  
      * @throws CannotWriteException if anything went wrong
 190  
      */
 191  
     public synchronized void delete(RandomAccessFile raf, RandomAccessFile tempRaf) throws CannotReadException, CannotWriteException, IOException
 192  
     {
 193  0
         raf.seek(0);
 194  0
         tempRaf.seek(0);
 195  0
         deleteTag(raf, tempRaf);
 196  0
     }
 197  
 
 198  
     /**
 199  
      * Same as above, but delete tag in the file.
 200  
      *
 201  
      * @throws IOException          is thrown when the RandomAccessFile operations throw it
 202  
      *                              (you should never throw them manually)
 203  
      * @throws CannotWriteException when an error occured during the deletion of the tag
 204  
      */
 205  
     protected abstract void deleteTag(RandomAccessFile raf, RandomAccessFile tempRaf) throws CannotReadException, CannotWriteException, IOException;
 206  
 
 207  
     /**
 208  
      * This method sets the {@link AudioFileModificationListener}.<br>
 209  
      * There is only one listener allowed, if you want more instances to be
 210  
      * supported, use the {@link ModificationHandler} to broadcast those events.<br>
 211  
      *
 212  
      * @param listener The listener. <code>null</code> allowed to deregister.
 213  
      */
 214  
     public synchronized void setAudioFileModificationListener(AudioFileModificationListener listener)
 215  
     {
 216  336
         this.modificationListener = listener;
 217  336
     }
 218  
 
 219  
     /**
 220  
      * Prechecks before normal write
 221  
      * <p/>
 222  
      * <ul>
 223  
      * <li>If the tag is actually empty, remove the tag</li>
 224  
      * <li>if the file is not writable, throw exception<li>
 225  
      * <li>If the file is too small to be a valid file, throw exception<li>
 226  
      * </ul>
 227  
      *
 228  
      * @param af
 229  
      * @throws CannotWriteException
 230  
      */
 231  
     private void precheckWrite(AudioFile af) throws CannotWriteException
 232  
     {
 233  
         // Preliminary checks
 234  
         try
 235  
         {
 236  92
             if (af.getTag().isEmpty())
 237  
             {
 238  1
                 delete(af);
 239  1
                 return;
 240  
             }
 241  
         }
 242  0
         catch (CannotReadException re)
 243  
         {
 244  0
             throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED.getMsg(af.getFile().getPath()));
 245  91
         }
 246  
 
 247  91
         if (!af.getFile().canWrite())
 248  
         {
 249  0
             logger.severe(ErrorMessage.GENERAL_WRITE_FAILED.getMsg(af.getFile().getPath()));
 250  0
             throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED.getMsg(af.getFile().getPath()));
 251  
         }
 252  
 
 253  91
         if (af.getFile().length() <= MINIMUM_FILESIZE)
 254  
         {
 255  0
             logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_IS_TOO_SMALL.getMsg(af.getFile().getPath()));
 256  0
             throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_IS_TOO_SMALL.getMsg(af.getFile().getPath()));
 257  
         }
 258  91
     }
 259  
 
 260  
     /**
 261  
      * Write the tag (if not empty) present in the AudioFile in the associated
 262  
      * File
 263  
      *
 264  
      * @param af The file we want to process
 265  
      * @throws CannotWriteException if anything went wrong
 266  
      */
 267  
     //TODO Creates temp file in same folder as the original file, this is safe but would impose a performance
 268  
     //overhead if the original file is on a networked drive
 269  
     public synchronized void write(AudioFile af) throws CannotWriteException
 270  
     {
 271  92
         logger.info("Started writing tag data for file:" + af.getFile().getName());
 272  
 
 273  
         //Prechecks
 274  92
         precheckWrite(af);
 275  
 
 276  92
         RandomAccessFile raf = null;
 277  92
         RandomAccessFile rafTemp = null;
 278  92
         File newFile = null;
 279  92
         File result = null;
 280  
 
 281  
         //Create temporary File
 282  
         try
 283  
         {
 284  92
             newFile = File.createTempFile(af.getFile().getName().replace('.', '_'), TEMP_FILENAME_SUFFIX, af.getFile().getParentFile());
 285  
         }
 286  
         //Unable to create temporary file, can happen in Vista if have Create Files/Write Data set to Deny
 287  0
         catch(IOException ioe)
 288  
         {
 289  0
             logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER.getMsg(af.getFile().getName(), af.getFile().getParentFile().getAbsolutePath()), ioe);
 290  0
             throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER.getMsg(af.getFile().getName(),
 291  
                     af.getFile().getParentFile().getAbsolutePath()));
 292  92
         }
 293  
 
 294  
         //Open temporary file and actual file for Editing
 295  
         try
 296  
         {
 297  92
             rafTemp = new RandomAccessFile(newFile, WRITE_MODE);
 298  92
             raf     = new RandomAccessFile(af.getFile(), WRITE_MODE);
 299  
 
 300  
         }
 301  
         //Unable to write to writable file, can happen in Vista if have Create Folders/Append Data set to Deny
 302  0
         catch(IOException ioe)
 303  
         {
 304  0
             logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(af.getFile().getAbsolutePath()), ioe);
 305  
 
 306  
             //If we managed to open either file, delete it.
 307  
             try
 308  
             {
 309  0
                 if (raf != null)
 310  
                 {
 311  0
                     raf.close();
 312  
                 }
 313  0
                 if (rafTemp != null)
 314  
                 {
 315  0
                     rafTemp.close();
 316  
                 }
 317  
             }
 318  0
             catch (IOException ioe2)
 319  
             {
 320  
                 //Warn but assume has worked okay
 321  0
                 logger.log(Level.WARNING, ErrorMessage.GENERAL_WRITE_PROBLEM_CLOSING_FILE_HANDLE.getMsg(af.getFile(), ioe.getMessage()), ioe2);
 322  0
             }
 323  
 
 324  
             //Delete the temp file ( we cannot delet until closed correpsonding rafTemp)
 325  0
             if(!newFile.delete())
 326  
             {
 327  
                 //Non critical failed deletion
 328  0
                 logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE.getMsg(newFile.getAbsolutePath()));
 329  
             }
 330  
 
 331  0
             throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(af.getFile().getAbsolutePath()));
 332  92
         }
 333  
 
 334  
 
 335  
         //Write data to File
 336  
         try
 337  
         {
 338  
 
 339  92
             raf.seek(0);
 340  92
             rafTemp.seek(0);
 341  
             try
 342  
             {
 343  92
                 if (this.modificationListener != null)
 344  
                 {
 345  92
                     this.modificationListener.fileWillBeModified(af, false);
 346  
                 }
 347  92
                 writeTag(af.getTag(), raf, rafTemp);
 348  92
                 if (this.modificationListener != null)
 349  
                 {
 350  92
                     this.modificationListener.fileModified(af, newFile);
 351  
                 }
 352  
             }
 353  0
             catch (ModifyVetoException veto)
 354  
             {
 355  0
                 throw new CannotWriteException(veto);
 356  92
             }
 357  92
         }
 358  0
         catch (Exception e)
 359  
         {
 360  0
             logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE.getMsg(af.getFile(), e.getMessage()), e);
 361  
 
 362  
             try
 363  
             {
 364  0
                 if (raf != null)
 365  
                 {
 366  0
                     raf.close();
 367  
                 }
 368  0
                 if (rafTemp != null)
 369  
                 {
 370  0
                     rafTemp.close();
 371  
                 }
 372  
             }
 373  0
             catch (IOException ioe)
 374  
             {
 375  
                 //Warn but assume has worked okay
 376  0
                 logger.log(Level.WARNING, ErrorMessage.GENERAL_WRITE_PROBLEM_CLOSING_FILE_HANDLE.getMsg(af.getFile().getAbsolutePath(), ioe.getMessage()), ioe);
 377  0
             }
 378  
             
 379  
             //Delete the temporary file because either it was never used so lets just tidy up or we did start writing to it but
 380  
             //the write failed and we havent renamed it back to the original file so we can just delete it.
 381  0
             if(!newFile.delete())
 382  
             {
 383  
                  //Non critical failed deletion
 384  0
                  logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE.getMsg(newFile.getAbsolutePath()));   
 385  
             }
 386  0
             throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE.getMsg(af.getFile(), e.getMessage()));
 387  
         }
 388  
         finally
 389  
         {
 390  0
             try
 391  
             {
 392  92
                 if (raf != null)
 393  
                 {
 394  92
                     raf.close();
 395  
                 }
 396  92
                 if (rafTemp != null)
 397  
                 {
 398  92
                     rafTemp.close();
 399  
                 }
 400  
             }
 401  0
             catch (IOException ioe)
 402  
             {
 403  
                 //Warn but assume has worked okay
 404  0
                 logger.log(Level.WARNING, ErrorMessage.GENERAL_WRITE_PROBLEM_CLOSING_FILE_HANDLE.getMsg(af.getFile().getAbsolutePath(), ioe.getMessage()), ioe);
 405  184
             }
 406  92
         }
 407  
 
 408  
         //Result held in this file
 409  92
         result = af.getFile();
 410  
 
 411  
          //If the temporary file was used
 412  92
         if (newFile.length() > 0)
 413  
         {
 414  
             //Rename Original File
 415  
             //Can fail on Vista if have Special Permission 'Delete' set Deny
 416  87
             File originalFileBackup = new File(af.getFile().getParentFile().getPath(), AudioFile.getBaseFilename(af.getFile())+ ".old");
 417  87
             boolean renameResult = af.getFile().renameTo(originalFileBackup);
 418  87
             if (renameResult == false)
 419  
             {
 420  0
                 logger.log(Level.SEVERE,ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_FILE_TO_BACKUP.getMsg(af.getFile().getPath(), originalFileBackup.getName()));
 421  0
                 throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_FILE_TO_BACKUP.getMsg(af.getFile().getPath(), originalFileBackup.getName()));
 422  
             }
 423  
 
 424  
             //Rename Temp File to Original File
 425  87
             renameResult = newFile.renameTo(af.getFile());
 426  87
             if (!renameResult)
 427  
             {
 428  
                  //Renamed failed so lets do some checks rename the backup back to the original file
 429  
                 //New File doesnt exist
 430  0
                 if(!newFile.exists())
 431  
                 {
 432  0
                     logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_NEW_FILE_DOESNT_EXIST.getMsg(newFile.getAbsolutePath()));
 433  
                 }
 434  
 
 435  
                 //Rename the backup back to the original
 436  0
                 if(!originalFileBackup.renameTo(af.getFile()))
 437  
                 {
 438  
                     //TODO now if this happens we are left with testfile.old instead of testfile.mp4
 439  0
                     logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_BACKUP_TO_ORIGINAL.getMsg(originalFileBackup.getAbsolutePath(),af.getFile().getName()));
 440  
                 }
 441  
 
 442  0
                 logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE.getMsg(af.getFile().getAbsolutePath(), newFile.getName()));
 443  0
                 throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE.getMsg(af.getFile().getAbsolutePath(), newFile.getName()));
 444  
             }
 445  
             else
 446  
             {
 447  
                 //Rename was okay so we can now delete the backup of the original
 448  87
                 boolean deleteResult=originalFileBackup.delete();
 449  87
                 if(!deleteResult)
 450  
                 {
 451  
                     //Not a disaster but can't delete the backup so make a warning
 452  0
                     logger.warning(ErrorMessage.GENERAL_WRITE_WARNING_UNABLE_TO_DELETE_BACKUP_FILE.getMsg(originalFileBackup.getAbsolutePath()));
 453  
                 }
 454  
             }
 455  
 
 456  
             //Delete the temporary file if still exists
 457  87
             if(newFile.exists())
 458  
             {
 459  0
                 if(!newFile.delete())
 460  
                 {
 461  
                     //Non critical failed deletion
 462  0
                     logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE.getMsg(newFile.getPath()));
 463  
                 }
 464  
             }
 465  87
         }
 466  
         else
 467  
         {
 468  
             //Delete the temporary file that wasn't ever used           
 469  5
             if(!newFile.delete())
 470  
             {
 471  
                 //Non critical failed deletion
 472  0
                 logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE.getMsg(newFile.getPath()));
 473  
             }
 474  
         }
 475  
 
 476  92
         if (this.modificationListener != null)
 477  
         {
 478  92
             this.modificationListener.fileOperationFinished(result);
 479  
         }
 480  92
     }
 481  
 
 482  
     /**
 483  
      * This is called when a tag has to be written in a file. Three parameters
 484  
      * are provided, the tag to write (not empty) Two randomaccessfiles, the
 485  
      * first points to the file where we want to write the given tag, and the
 486  
      * second is an empty temporary file that can be used if e.g. the file has
 487  
      * to be bigger than the original.
 488  
      * <p/>
 489  
      * If something has been written in the temporary file, when this method
 490  
      * returns, the original file is deleted, and the temporary file is renamed
 491  
      * the the original name
 492  
      * <p/>
 493  
      * If nothing has been written to it, it is simply deleted.
 494  
      * <p/>
 495  
      * This method can assume the raf, rafTemp are pointing to the first byte of
 496  
      * the file. The subclass must not close these two files when the method
 497  
      * returns.
 498  
      *
 499  
      * @throws IOException          is thrown when the RandomAccessFile operations throw it
 500  
      *                              (you should never throw them manually)
 501  
      * @throws CannotWriteException when an error occured during the generation of the tag
 502  
      */
 503  
     protected abstract void writeTag(Tag tag, RandomAccessFile raf, RandomAccessFile rafTemp) throws CannotReadException, CannotWriteException, IOException;
 504  
 
 505  
 }