/*
* Entagged Audio Tag library
* Copyright (c) 2003-2005 Raphaƫl Slinckx <raphael@slinckx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jaudiotagger.audio.generic;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.CannotWriteException;
import org.jaudiotagger.audio.exceptions.ModifyVetoException;
import org.jaudiotagger.audio.mp3.MP3File;
import org.jaudiotagger.logging.ErrorMessage;
import org.jaudiotagger.tag.Tag;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This abstract class is the skeleton for tag writers.
* <p/>
* <p/>
* It handles the creation/closing of the randomaccessfile objects and then call
* the subclass method writeTag or deleteTag. These two method have to be
* implemented in the subclass.
*
* @author Raphael Slinckx
* @version $Id: AudioFileWriter.java,v 1.21 2009/05/05 15:59:14 paultaylor Exp
* $
* @since v0.02
*/
public abstract class AudioFileWriter
{
private static final String TEMP_FILENAME_SUFFIX = ".tmp";
private static final String WRITE_MODE = "rws";
private static final int MINIMUM_FILESIZE = 150;
// Logger Object
public static Logger logger = Logger
.getLogger("org.jaudiotagger.audio.generic");
/**
* If not <code>null</code>, this listener is used to notify the listener
* about modification events.<br>
*/
private AudioFileModificationListener modificationListener = null;
/**
* Delete the tag (if any) present in the given file
*
* @param af The file to process
* @throws CannotWriteException if anything went wrong
* @throws org.jaudiotagger.audio.exceptions.CannotReadException
*/
public synchronized void delete(AudioFile af) throws CannotReadException, CannotWriteException
{
if (!af.getFile().canWrite())
{
throw new CannotWriteException(ErrorMessage.GENERAL_DELETE_FAILED
.getMsg(af.getFile().getPath()));
}
if (af.getFile().length() <= MINIMUM_FILESIZE)
{
throw new CannotWriteException(ErrorMessage.GENERAL_DELETE_FAILED
.getMsg(af.getFile().getPath()));
}
RandomAccessFile raf = null;
RandomAccessFile rafTemp = null;
File tempF = null;
// Will be set to true on VetoException, causing the finally block to
// discard the tempfile.
boolean revert = false;
try
{
tempF = File.createTempFile(af.getFile().getName()
.replace('.', '_'), TEMP_FILENAME_SUFFIX, af.getFile()
.getParentFile());
rafTemp = new RandomAccessFile(tempF, WRITE_MODE);
raf = new RandomAccessFile(af.getFile(), WRITE_MODE);
raf.seek(0);
rafTemp.seek(0);
try
{
if (this.modificationListener != null)
{
this.modificationListener.fileWillBeModified(af, true);
}
deleteTag(raf, rafTemp);
if (this.modificationListener != null)
{
this.modificationListener.fileModified(af, tempF);
}
}
catch (ModifyVetoException veto)
{
throw new CannotWriteException(veto);
}
}
catch (Exception e)
{
revert = true;
throw new CannotWriteException("\"" + af.getFile().getAbsolutePath() + "\" :" + e, e);
}
finally
{
// will be set to the remaining file.
File result = af.getFile();
try
{
if (raf != null)
{
raf.close();
}
if (rafTemp != null)
{
rafTemp.close();
}
if (tempF.length() > 0 && !revert)
{
boolean deleteResult = af.getFile().delete();
if (!deleteResult)
{
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_ORIGINAL_FILE
.getMsg(af.getFile().getPath(), tempF
.getPath()));
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_ORIGINAL_FILE
.getMsg(af.getFile().getPath(), tempF
.getPath()));
}
boolean renameResult = tempF.renameTo(af.getFile());
if (!renameResult)
{
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE
.getMsg(af.getFile().getPath(), tempF
.getPath()));
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE
.getMsg(af.getFile().getPath(), tempF
.getPath()));
}
result = tempF;
// If still exists we can now delete
if (tempF.exists())
{
if (!tempF.delete())
{
// Non critical failed deletion
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE
.getMsg(tempF.getPath()));
}
}
}
else
{
// It was created but never used
if (!tempF.delete())
{
// Non critical failed deletion
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE
.getMsg(tempF.getPath()));
}
}
}
catch (Exception ex)
{
logger.severe("AudioFileWriter exception cleaning up delete:" + af.getFile().getPath() + " or" + tempF.getAbsolutePath() + ":" + ex);
}
// Notify listener
if (this.modificationListener != null)
{
this.modificationListener.fileOperationFinished(result);
}
}
}
/**
* Delete the tag (if any) present in the given randomaccessfile, and do not
* close it at the end.
*
* @param raf The source file, already opened in r-write mode
* @param tempRaf The temporary file opened in r-write mode
* @throws CannotWriteException if anything went wrong
* @throws org.jaudiotagger.audio.exceptions.CannotReadException
* @throws java.io.IOException
*/
public synchronized void delete(RandomAccessFile raf, RandomAccessFile tempRaf) throws CannotReadException, CannotWriteException, IOException
{
raf.seek(0);
tempRaf.seek(0);
deleteTag(raf, tempRaf);
}
/**
* Same as above, but delete tag in the file.
*
* @param raf
* @param tempRaf
* @throws IOException is thrown when the RandomAccessFile operations throw it (you
* should never throw them manually)
* @throws CannotWriteException when an error occured during the deletion of the tag
* @throws org.jaudiotagger.audio.exceptions.CannotReadException
*/
protected abstract void deleteTag(RandomAccessFile raf, RandomAccessFile tempRaf) throws CannotReadException, CannotWriteException, IOException;
/**
* This method sets the {@link AudioFileModificationListener}.<br>
* There is only one listener allowed, if you want more instances to be
* supported, use the {@link ModificationHandler} to broadcast those events.<br>
*
* @param listener The listener. <code>null</code> allowed to deregister.
*/
public synchronized void setAudioFileModificationListener(AudioFileModificationListener listener)
{
this.modificationListener = listener;
}
/**
* Prechecks before normal write
* <p/>
* <ul>
* <li>If the tag is actually empty, remove the tag</li>
* <li>if the file is not writable, throw exception
* <li>
* <li>If the file is too small to be a valid file, throw exception
* <li>
* </ul>
*
* @param af
* @throws CannotWriteException
*/
private void precheckWrite(AudioFile af) throws CannotWriteException
{
// Preliminary checks
try
{
if (af.getTag().isEmpty())
{
delete(af);
return;
}
}
catch (CannotReadException re)
{
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED
.getMsg(af.getFile().getPath()));
}
if (!af.getFile().canWrite())
{
logger.severe(ErrorMessage.GENERAL_WRITE_FAILED.getMsg(af.getFile()
.getPath()));
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED
.getMsg(af.getFile().getPath()));
}
if (af.getFile().length() <= MINIMUM_FILESIZE)
{
logger
.severe(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_IS_TOO_SMALL
.getMsg(af.getFile().getPath()));
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE_FILE_IS_TOO_SMALL
.getMsg(af.getFile().getPath()));
}
}
/**
* Write the tag (if not empty) present in the AudioFile in the associated
* File
*
* @param af The file we want to process
* @throws CannotWriteException if anything went wrong
*/
// TODO Creates temp file in same folder as the original file, this is safe
// but would impose a performance overhead if the original file is on a networked drive
public synchronized void write(AudioFile af) throws CannotWriteException
{
logger.config("Started writing tag data for file:" + af.getFile().getName());
// Prechecks
precheckWrite(af);
//mp3's use a different mechanism to the other formats
if(af instanceof MP3File)
{
af.commit();
return;
}
RandomAccessFile raf = null;
RandomAccessFile rafTemp = null;
File newFile;
File result;
// Create temporary File
try
{
newFile = File.createTempFile(af.getFile().getName().replace('.', '_'), TEMP_FILENAME_SUFFIX, af.getFile().getParentFile());
}
// Unable to create temporary file, can happen in Vista if have Create
// Files/Write Data set to Deny
catch (IOException ioe)
{
logger
.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER
.getMsg(af.getFile().getName(), af
.getFile().getParentFile()
.getAbsolutePath()), ioe);
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER
.getMsg(af.getFile().getName(), af.getFile()
.getParentFile().getAbsolutePath()));
}
// Open temporary file and actual file for editing
try
{
rafTemp = new RandomAccessFile(newFile, WRITE_MODE);
raf = new RandomAccessFile(af.getFile(), WRITE_MODE);
}
// Unable to write to writable file, can happen in Vista if have Create
// Folders/Append Data set to Deny
catch (IOException ioe)
{
logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING
.getMsg(af.getFile().getAbsolutePath()), ioe);
// If we managed to open either file, delete it.
try
{
if (raf != null)
{
raf.close();
}
if (rafTemp != null)
{
rafTemp.close();
}
}
catch (IOException ioe2)
{
// Warn but assume has worked okay
logger.log(Level.WARNING, ErrorMessage.GENERAL_WRITE_PROBLEM_CLOSING_FILE_HANDLE
.getMsg(af.getFile(), ioe.getMessage()), ioe2);
}
// Delete the temp file ( we cannot delete until closed corresponding
// rafTemp)
if (!newFile.delete())
{
// Non critical failed deletion
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE
.getMsg(newFile.getAbsolutePath()));
}
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING
.getMsg(af.getFile().getAbsolutePath()));
}
// Write data to File
try
{
raf.seek(0);
rafTemp.seek(0);
try
{
if (this.modificationListener != null)
{
this.modificationListener.fileWillBeModified(af, false);
}
writeTag(af.getTag(), raf, rafTemp);
if (this.modificationListener != null)
{
this.modificationListener.fileModified(af, newFile);
}
}
catch (ModifyVetoException veto)
{
throw new CannotWriteException(veto);
}
}
catch (Exception e)
{
logger.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE
.getMsg(af.getFile(), e.getMessage()), e);
try
{
if (raf != null)
{
raf.close();
}
if (rafTemp != null)
{
rafTemp.close();
}
}
catch (IOException ioe)
{
// Warn but assume has worked okay
logger.log(Level.WARNING, ErrorMessage.GENERAL_WRITE_PROBLEM_CLOSING_FILE_HANDLE
.getMsg(af.getFile().getAbsolutePath(), ioe
.getMessage()), ioe);
}
// Delete the temporary file because either it was never used so
// lets just tidy up or we did start writing to it but
// the write failed and we havent renamed it back to the original
// file so we can just delete it.
if (!newFile.delete())
{
// Non critical failed deletion
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE
.getMsg(newFile.getAbsolutePath()));
}
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_BECAUSE.getMsg(af
.getFile(), e.getMessage()));
}
finally
{
try
{
if (raf != null)
{
raf.close();
}
if (rafTemp != null)
{
rafTemp.close();
}
}
catch (IOException ioe)
{
// Warn but assume has worked okay
logger.log(Level.WARNING, ErrorMessage.GENERAL_WRITE_PROBLEM_CLOSING_FILE_HANDLE
.getMsg(af.getFile().getAbsolutePath(), ioe
.getMessage()), ioe);
}
}
// Result held in this file
result = af.getFile();
// If the temporary file was used
if (newFile.length() > 0)
{
// Rename Original File
// Can fail on Vista if have Special Permission 'Delete' set Deny
File originalFileBackup = new File(af.getFile().getAbsoluteFile().getParentFile().getPath(),
AudioFile.getBaseFilename(af.getFile()) + ".old");
//If already exists modify the suffix
int count=1;
while(originalFileBackup.exists())
{
originalFileBackup = new File(af.getFile().getAbsoluteFile().getParentFile().getPath(), AudioFile.getBaseFilename(af.getFile())+ ".old"+count);
count++;
}
boolean renameResult = Utils.rename(af.getFile(),originalFileBackup);
if (!renameResult)
{
logger
.log(Level.SEVERE, ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_FILE_TO_BACKUP
.getMsg(af.getFile().getAbsolutePath(), originalFileBackup.getName()));
//Delete the temp file because write has failed
if(newFile!=null)
{
newFile.delete();
}
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_FILE_TO_BACKUP
.getMsg(af.getFile().getPath(), originalFileBackup.getName()));
}
// Rename Temp File to Original File
renameResult = Utils.rename(newFile,af.getFile());
if (!renameResult)
{
// Renamed failed so lets do some checks rename the backup back to the original file
// New File doesnt exist
if (!newFile.exists())
{
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_NEW_FILE_DOESNT_EXIST
.getMsg(newFile.getAbsolutePath()));
}
// Rename the backup back to the original
if (!originalFileBackup.renameTo(af.getFile()))
{
// TODO now if this happens we are left with testfile.old
// instead of testfile.mp4
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_BACKUP_TO_ORIGINAL
.getMsg(originalFileBackup
.getAbsolutePath(), af.getFile()
.getName()));
}
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE
.getMsg(af.getFile().getAbsolutePath(), newFile
.getName()));
throw new CannotWriteException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE
.getMsg(af.getFile().getAbsolutePath(), newFile
.getName()));
}
else
{
// Rename was okay so we can now delete the backup of the
// original
boolean deleteResult = originalFileBackup.delete();
if (!deleteResult)
{
// Not a disaster but can't delete the backup so make a
// warning
logger
.warning(ErrorMessage.GENERAL_WRITE_WARNING_UNABLE_TO_DELETE_BACKUP_FILE
.getMsg(originalFileBackup
.getAbsolutePath()));
}
}
// Delete the temporary file if still exists
if (newFile.exists())
{
if (!newFile.delete())
{
// Non critical failed deletion
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE
.getMsg(newFile.getPath()));
}
}
}
else
{
// Delete the temporary file that wasn't ever used
if (!newFile.delete())
{
// Non critical failed deletion
logger
.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_DELETE_TEMPORARY_FILE
.getMsg(newFile.getPath()));
}
}
if (this.modificationListener != null)
{
this.modificationListener.fileOperationFinished(result);
}
}
/**
* This is called when a tag has to be written in a file. Three parameters
* are provided, the tag to write (not empty) Two randomaccessfiles, the
* first points to the file where we want to write the given tag, and the
* second is an empty temporary file that can be used if e.g. the file has
* to be bigger than the original.
* <p/>
* If something has been written in the temporary file, when this method
* returns, the original file is deleted, and the temporary file is renamed
* the the original name
* <p/>
* If nothing has been written to it, it is simply deleted.
* <p/>
* This method can assume the raf, rafTemp are pointing to the first byte of
* the file. The subclass must not close these two files when the method
* returns.
*
* @param tag
* @param raf
* @param rafTemp
* @throws IOException is thrown when the RandomAccessFile operations throw it (you
* should never throw them manually)
* @throws CannotWriteException when an error occured during the generation of the tag
* @throws org.jaudiotagger.audio.exceptions.CannotReadException
*/
protected abstract void writeTag(Tag tag, RandomAccessFile raf, RandomAccessFile rafTemp) throws CannotReadException, CannotWriteException, IOException;
}
|