diff --git a/src/playlist/PlaylistManager.java b/src/playlist/PlaylistManager.java index d50f7e03..8eb5bb49 100644 --- a/src/playlist/PlaylistManager.java +++ b/src/playlist/PlaylistManager.java @@ -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 ******************************************************************************/ 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() - { - @Override - public void receive( AliasEvent t ) - { - schedulePlaylistSave(); - } - } ); - - mChannelMapModel.addListener( new Listener() - { - @Override - public void receive( ChannelMapEvent t ) - { - schedulePlaylistSave(); - } - } ); + private Path mPlaylistFolderPath; + private Path mPlaylistCurrentPath; + private Path mPlaylistBackupPath; + private Path mPlaylistLockPath; - mBroadcastModel.addListener(new Listener() - { - @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() { - 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() { - mLog.error( "couldn't open outputstream to save playlist [" + - mCurrentPlaylistPath.toString() + "]", e ); + @Override + public void receive(ChannelMapEvent t) + { + schedulePlaylistSave(); + } + }); + + mBroadcastModel.addListener(new Listener() + { + @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(); + } + } }