Merge pull request #88 from DSheirer/#86_playlist_file_corruption_on_save

#86 - updates playlist configuration file save operations to 1)create…
This commit is contained in:
Denny Sheirer
2017-01-07 10:26:41 -05:00
committed by GitHub

View File

@@ -1,48 +1,26 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* Copyright (C) 2014-2017 Dennis Sheirer
*
* 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 playlist;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import playlist.version1.PlaylistConverterV1ToV2;
import properties.SystemProperties;
import sample.Listener;
import alias.AliasEvent;
import alias.AliasModel;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import controller.ThreadPoolManager;
import controller.channel.Channel.ChannelType;
import controller.channel.ChannelEvent;
@@ -50,358 +28,413 @@ import controller.channel.ChannelEventListener;
import controller.channel.ChannelModel;
import controller.channel.map.ChannelMapEvent;
import controller.channel.map.ChannelMapModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import playlist.version1.PlaylistConverterV1ToV2;
import properties.SystemProperties;
import sample.Listener;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class PlaylistManager implements ChannelEventListener
{
private final static Logger mLog =
LoggerFactory.getLogger( PlaylistManager.class );
private final static Logger mLog = LoggerFactory.getLogger(PlaylistManager.class);
private ThreadPoolManager mThreadPoolManager;
private AliasModel mAliasModel;
private BroadcastModel mBroadcastModel;
private ChannelModel mChannelModel;
private ChannelMapModel mChannelMapModel;
private Path mCurrentPlaylistPath;
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
private boolean mPlaylistLoading = false;
/**
* Playlist manager - manages all channel configurations, channel maps, and
* alias lists and handles loading or persisting to a playlist.xml file
*
* Monitors playlist changes to automatically save configuration changes
* after they occur.
*
* @param threadPoolManager
* @param channelModel
*/
public PlaylistManager( ThreadPoolManager threadPoolManager,
AliasModel aliasModel,
BroadcastModel broadcastModel,
ChannelModel channelModel,
ChannelMapModel channelMapModel)
{
mThreadPoolManager = threadPoolManager;
mAliasModel = aliasModel;
mBroadcastModel = broadcastModel;
mChannelModel = channelModel;
mChannelMapModel = channelMapModel;
private ThreadPoolManager mThreadPoolManager;
private AliasModel mAliasModel;
private BroadcastModel mBroadcastModel;
private ChannelModel mChannelModel;
private ChannelMapModel mChannelMapModel;
//Register for alias, channel and channel map events so that we can
//save the playlist when there are any changes
mChannelModel.addListener( this );
mAliasModel.addListener( new Listener<AliasEvent>()
{
@Override
public void receive( AliasEvent t )
{
schedulePlaylistSave();
}
} );
mChannelMapModel.addListener( new Listener<ChannelMapEvent>()
{
@Override
public void receive( ChannelMapEvent t )
{
schedulePlaylistSave();
}
} );
private Path mPlaylistFolderPath;
private Path mPlaylistCurrentPath;
private Path mPlaylistBackupPath;
private Path mPlaylistLockPath;
mBroadcastModel.addListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
{
schedulePlaylistSave();
}
});
}
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
private boolean mPlaylistLoading = false;
/**
* Loads playlist from the current playlist file, or the default playlist file,
* as specified in the current SDRTRunk system settings
*/
public void init()
{
SystemProperties props = SystemProperties.getInstance();
Path playlistFolder = props.getApplicationFolder( "playlist" );
String defaultPlaylistFile =
props.get( "playlist.v2.defaultfilename", "playlist_v2.xml" );
String playlistFile =
props.get( "playlist.v2.currentfilename", defaultPlaylistFile );
/**
* Playlist manager - manages all channel configurations, channel maps, and
* alias lists and handles loading or persisting to a playlist.xml file
*
* Monitors playlist changes to automatically save configuration changes
* after they occur.
*
* @param threadPoolManager
* @param channelModel
*/
public PlaylistManager(ThreadPoolManager threadPoolManager,
AliasModel aliasModel,
BroadcastModel broadcastModel,
ChannelModel channelModel,
ChannelMapModel channelMapModel)
{
mThreadPoolManager = threadPoolManager;
mAliasModel = aliasModel;
mBroadcastModel = broadcastModel;
mChannelModel = channelModel;
mChannelMapModel = channelMapModel;
mCurrentPlaylistPath = playlistFolder.resolve( playlistFile );
//Register for alias, channel and channel map events so that we can
//save the playlist when there are any changes
mChannelModel.addListener(this);
PlaylistV2 playlist = load( mCurrentPlaylistPath );
boolean saveRequired = false;
if( playlist == null )
{
mLog.info( "Couldn't find version 2 playlist - looking for "
+ "version 1 playlist to convert" );
Path playlistV1Path = playlistFolder.resolve( "playlist.xml" );
PlaylistConverterV1ToV2 converter =
new PlaylistConverterV1ToV2( playlistV1Path );
if( converter.hasErrorMessages() )
{
mLog.error( "Playlist version 1 conversion errors: " +
converter.getErrorMessages() );
}
playlist = converter.getConvertedPlaylist();
saveRequired = true;
}
transferPlaylistToModels( playlist );
if( saveRequired )
{
schedulePlaylistSave();
}
}
/**
* Transfers data from persisted playlist into system models
*/
private void transferPlaylistToModels( PlaylistV2 playlist )
{
if( playlist != null )
{
mPlaylistLoading = true;
mAliasModel.addAliases( playlist.getAliases() );
mBroadcastModel.addBroadcastConfigurations(playlist.getBroadcastConfigurations());
mChannelModel.addChannels( playlist.getChannels() );
mChannelMapModel.addChannelMaps( playlist.getChannelMaps() );
mPlaylistLoading = false;
}
}
/**
* Channel event listener method. Monitors channel events for events that
* indicate that the playlist has changed and queues automatic playlist
* saving.
*/
@Override
public void channelChanged( ChannelEvent event )
{
//Only save playlist for changes to standard channels (not traffic)
if( event.getChannel().getChannelType() == ChannelType.STANDARD )
{
switch( event.getEvent() )
{
case NOTIFICATION_ADD:
case NOTIFICATION_CONFIGURATION_CHANGE:
case NOTIFICATION_DELETE:
case NOTIFICATION_PROCESSING_START:
case NOTIFICATION_PROCESSING_STOP:
schedulePlaylistSave();
break;
case NOTIFICATION_ENABLE_REJECTED:
case NOTIFICATION_SELECTION_CHANGE:
case NOTIFICATION_STATE_RESET:
case REQUEST_DELETE:
case REQUEST_DESELECT:
case REQUEST_DISABLE:
case REQUEST_ENABLE:
case REQUEST_SELECT:
//Do nothing for these event types
break;
default:
//When a new event enum entry is added and received, throw an
//exception here to ensure developer adds support for the
//use case
throw new IllegalArgumentException( "Unrecognized Channel "
+ "Event [" + event.getEvent().name() + "]" );
}
}
}
public void save()
{
PlaylistV2 playlist = new PlaylistV2();
playlist.setAliases( mAliasModel.getAliases() );
playlist.setBroadcastConfigurations(mBroadcastModel.getBroadcastConfigurations());
playlist.setChannels( mChannelModel.getChannels() );
playlist.setChannelMaps( mChannelMapModel.getChannelMaps() );
JAXBContext context = null;
if( mCurrentPlaylistPath == null )
{
SystemProperties props = SystemProperties.getInstance();
Path playlistPath = props.getApplicationFolder( "playlist" );
String playlistDefault = props.get( "playlist.defaultfilename",
"playlist_v2.xml" );
String playlistCurrent = props.get( "playlist.currentfilename",
playlistDefault );
mCurrentPlaylistPath = playlistPath.resolve( playlistCurrent );
}
File outputFile = new File( mCurrentPlaylistPath.toString() );
try
{
if( !outputFile.exists() )
{
outputFile.createNewFile();
}
}
catch( Exception e )
{
mLog.error( "couldn't create file to save "
+ "playlist [" + mCurrentPlaylistPath.toString() + "]" );
}
OutputStream out = null;
try
mAliasModel.addListener(new Listener<AliasEvent>()
{
out = new FileOutputStream( outputFile );
try
{
context = JAXBContext.newInstance( PlaylistV2.class );
@Override
public void receive(AliasEvent t)
{
schedulePlaylistSave();
}
});
Marshaller m = context.createMarshaller();
m.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, true );
m.marshal( playlist, out );
}
catch ( JAXBException e )
{
mLog.error( "jaxb exception while saving playlist: ", e );
}
}
catch ( Exception e )
mChannelMapModel.addListener(new Listener<ChannelMapEvent>()
{
mLog.error( "couldn't open outputstream to save playlist [" +
mCurrentPlaylistPath.toString() + "]", e );
@Override
public void receive(ChannelMapEvent t)
{
schedulePlaylistSave();
}
});
mBroadcastModel.addListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
{
schedulePlaylistSave();
}
});
}
/**
* Loads playlist from the current playlist file, or the default playlist file,
* as specified in the current SDRTRunk system settings
*/
public void init()
{
PlaylistV2 playlist = load();
boolean saveRequired = false;
if(playlist == null)
{
mLog.info("Couldn't find version 2 playlist - looking for version 1 playlist to convert");
Path playlistV1Path = getPlaylistFolderPath().resolve("playlist.xml");
PlaylistConverterV1ToV2 converter = new PlaylistConverterV1ToV2(playlistV1Path);
if(converter.hasErrorMessages())
{
mLog.error("Playlist version 1 conversion errors: " + converter.getErrorMessages());
}
playlist = converter.getConvertedPlaylist();
saveRequired = true;
}
finally
{
if( out != null )
{
try
transferPlaylistToModels(playlist);
if(saveRequired)
{
schedulePlaylistSave();
}
}
/**
* Transfers data from persisted playlist into system models
*/
private void transferPlaylistToModels(PlaylistV2 playlist)
{
if(playlist != null)
{
mPlaylistLoading = true;
mAliasModel.addAliases(playlist.getAliases());
mBroadcastModel.addBroadcastConfigurations(playlist.getBroadcastConfigurations());
mChannelModel.addChannels(playlist.getChannels());
mChannelMapModel.addChannelMaps(playlist.getChannelMaps());
mPlaylistLoading = false;
}
}
/**
* Channel event listener method. Monitors channel events for events that indicate that the playlist has changed
* and queues automatic playlist saving.
*/
@Override
public void channelChanged(ChannelEvent event)
{
//Only save playlist for changes to standard channels (not traffic)
if(event.getChannel().getChannelType() == ChannelType.STANDARD)
{
switch(event.getEvent())
{
case NOTIFICATION_ADD:
case NOTIFICATION_CONFIGURATION_CHANGE:
case NOTIFICATION_DELETE:
schedulePlaylistSave();
break;
}
}
}
/**
* Folder where playlist, backup and lock file are stored
*/
private Path getPlaylistFolderPath()
{
if(mPlaylistFolderPath == null)
{
SystemProperties props = SystemProperties.getInstance();
mPlaylistFolderPath = props.getApplicationFolder("playlist");
}
return mPlaylistFolderPath;
}
/**
* Path to current playlist
*/
private Path getPlaylistPath()
{
if(mPlaylistCurrentPath == null)
{
SystemProperties props = SystemProperties.getInstance();
String playlistDefault = props.get("playlist.defaultfilename", "playlist_v2.xml");
String playlistCurrent = props.get("playlist.currentfilename", playlistDefault);
mPlaylistCurrentPath = getPlaylistFolderPath().resolve(playlistCurrent);
}
return mPlaylistCurrentPath;
}
/**
* Path to most recent playlist backup
*/
private Path getPlaylistBackupPath()
{
if(mPlaylistBackupPath == null)
{
SystemProperties props = SystemProperties.getInstance();
String playlistDefault = props.get("playlist.defaultfilename", "playlist_v2.xml");
String playlistCurrent = props.get("playlist.currentfilename", playlistDefault);
String playlistBackup = playlistCurrent.replace(".xml", ".bak");
mPlaylistBackupPath = getPlaylistFolderPath().resolve(playlistBackup);
}
return mPlaylistBackupPath;
}
/**
* Path to playlist lock file that is created prior to saving a playlist and removed immediately thereafter.
* Presence of a lock file indicates an incomplete or corrupt playlist file on startup.
*/
private Path getPlaylistLockPath()
{
if(mPlaylistLockPath == null)
{
SystemProperties props = SystemProperties.getInstance();
String playlistDefault = props.get("playlist.defaultfilename", "playlist_v2.xml");
String playlistCurrent = props.get("playlist.currentfilename", playlistDefault);
String playlistLock = playlistCurrent.replace(".xml", ".lock");
mPlaylistLockPath = getPlaylistFolderPath().resolve(playlistLock);
}
return mPlaylistLockPath;
}
/**
* Saves the current playlist
*/
private void save()
{
PlaylistV2 playlist = new PlaylistV2();
playlist.setAliases(mAliasModel.getAliases());
playlist.setBroadcastConfigurations(mBroadcastModel.getBroadcastConfigurations());
playlist.setChannels(mChannelModel.getChannels());
playlist.setChannelMaps(mChannelMapModel.getChannelMaps());
JAXBContext context = null;
//Create a backup copy of the current playlist
if(Files.exists(getPlaylistPath()))
{
try
{
Files.copy(getPlaylistPath(), getPlaylistBackupPath(), StandardCopyOption.REPLACE_EXISTING);
}
catch(Exception e)
{
mLog.error("Error creating backup copy of current playlist prior to saving updates [" +
getPlaylistPath().toString() + "]", e);
}
}
//Create a temporary lock file to signify that we're in the process of updating the playlist
if(!Files.exists(getPlaylistLockPath()))
{
try
{
Files.createFile(getPlaylistLockPath());
}
catch(IOException e)
{
mLog.error("Error creating temporary lock file prior to saving playlist [" +
getPlaylistLockPath().toString() + "]", e);
}
}
try(OutputStream out = Files.newOutputStream(getPlaylistPath()))
{
context = JAXBContext.newInstance(PlaylistV2.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
m.marshal(playlist, out);
out.flush();
//Remove the playlist lock file to indicate that we successfully saved the file
if(Files.exists(getPlaylistLockPath()))
{
Files.delete(getPlaylistLockPath());
}
}
catch(JAXBException je)
{
mLog.error("JAXB exception while serializing the playlist to a file [" + getPlaylistPath().toString() +
"]", je);
}
catch(IOException ioe)
{
mLog.error("IO error while writing the playlist to a file [" + getPlaylistPath().toString() + "]", ioe);
}
catch(Exception e)
{
mLog.error("Error while saving playlist [" + getPlaylistPath().toString() + "]", e);
}
}
/**
* Loads a version 2 playlist
*/
public PlaylistV2 load()
{
mLog.info("loading version 2 playlist file [" + getPlaylistPath().toString() + "]");
PlaylistV2 playlist = null;
//Check for a lock file that indicates the previous save attempt was incomplete or had an error
if(Files.exists(getPlaylistLockPath()))
{
try
{
//Remove the previous playlist
Files.delete(getPlaylistPath());
//Copy the backup file to restore the previous playlist
if(Files.exists(getPlaylistBackupPath()))
{
out.close();
Files.copy(getPlaylistBackupPath(), getPlaylistPath());
}
catch ( IOException e )
{
e.printStackTrace();
}
}
}
}
/**
* Loads a version 2 playlist
*/
public PlaylistV2 load( Path playlistPath )
{
mLog.info( "loading version 2 playlist file [" + playlistPath.toString() + "]" );
PlaylistV2 playlist = null;
//Remove the lock file
Files.delete(getPlaylistLockPath());
}
catch(IOException ioe)
{
mLog.error("Previous playlist save attempt was incomplete and there was an error restoring the " +
"playlist backup file", ioe);
}
}
if( Files.exists( playlistPath ) )
{
JAXBContext context = null;
InputStream in = null;
try
{
in = new FileInputStream( playlistPath.toString() );
try
{
context = JAXBContext.newInstance( PlaylistV2.class );
if(Files.exists(getPlaylistPath()))
{
JAXBContext context = null;
Unmarshaller m = context.createUnmarshaller();
try(InputStream in = Files.newInputStream(getPlaylistPath()))
{
context = JAXBContext.newInstance(PlaylistV2.class);
playlist = (PlaylistV2)m.unmarshal( in );
}
catch ( JAXBException e )
{
mLog.error( "jaxb exception while loading playlist: ", e );
}
}
catch ( Exception e )
{
mLog.error( "couldn't open input stream to load playlist [" + playlistPath.toString() + "]", e );
}
finally
{
if( in != null )
{
try
{
in.close();
}
catch ( IOException e )
{
mLog.error( "exception while closing " +
"the playlist file inputstream reader", e );
}
}
}
}
else
{
mLog.info( "PlaylistManager - playlist not found at [" +
playlistPath.toString() + "]" );
}
Unmarshaller m = context.createUnmarshaller();
return playlist;
}
playlist = (PlaylistV2) m.unmarshal(in);
}
catch(JAXBException je)
{
mLog.error("JAXB exception while loading/unmarshalling playlist", je);
}
catch(IOException ioe)
{
mLog.error("IO error while reading playlist file", ioe);
}
}
else
{
mLog.info("PlaylistManager - playlist not found at [" + getPlaylistPath().toString() + "]");
}
/**
* Schedules a playlist save task. Subsequent calls to this method will be
* ignored until the save event occurs, thus limiting repetitive playlist
* saving to a minimum.
*/
private void schedulePlaylistSave()
{
if( !mPlaylistLoading )
{
if( mPlaylistSavePending.compareAndSet( false, true ) )
{
mThreadPoolManager.scheduleOnce( new PlaylistSaveTask(),
2, TimeUnit.SECONDS );
}
}
}
return playlist;
}
/**
* Resets the playlist save pending flag to false and proceeds to save the
* playlist.
*/
public class PlaylistSaveTask implements Runnable
{
@Override
public void run()
{
mPlaylistSavePending.set( false );
save();
}
}
/**
* Schedules a playlist save task. Subsequent calls to this method will be ignored until the save event occurs,
* thus limiting repetitive playlist saving to a minimum.
*/
private void schedulePlaylistSave()
{
if(!mPlaylistLoading)
{
if(mPlaylistSavePending.compareAndSet(false, true))
{
mThreadPoolManager.scheduleOnce(new PlaylistSaveTask(), 2, TimeUnit.SECONDS);
}
}
}
/**
* Resets the playlist save pending flag to false and proceeds to save the playlist.
*/
public class PlaylistSaveTask implements Runnable
{
@Override
public void run()
{
mPlaylistSavePending.set(false);
save();
}
}
}