Added checking for new releases

This commit is contained in:
Vhati 2013-09-05 02:14:44 -04:00
parent 40c522ec8f
commit ff6f5b70bc
6 changed files with 290 additions and 28 deletions

View file

@ -48,6 +48,7 @@ public class FTLModManager {
config.setProperty( "use_default_ui", "false" ); config.setProperty( "use_default_ui", "false" );
config.setProperty( "remember_geometry", "true" ); config.setProperty( "remember_geometry", "true" );
// "update_catalog" doesn't have a default. // "update_catalog" doesn't have a default.
// "update_app" doesn't have a default.
// "manager_geometry" doesn't have a default. // "manager_geometry" doesn't have a default.
// Read the config file. // Read the config file.
@ -128,19 +129,30 @@ public class FTLModManager {
} }
// Prompt if update_catalog is invalid or hasn't been set. // Prompt if update_catalog is invalid or hasn't been set.
boolean askAboutUpdates = false;
String catalogUpdateInterval = config.getProperty( "update_catalog" );
String appUpdateInterval = config.getProperty( "update_app" );
String updateCatalog = config.getProperty( "update_catalog" ); if ( catalogUpdateInterval == null || !catalogUpdateInterval.matches("^\\d+$") )
if ( updateCatalog == null || !updateCatalog.matches("^true|false$") ) { askAboutUpdates = true;
if ( appUpdateInterval == null || !appUpdateInterval.matches("^\\d+$") )
askAboutUpdates = true;
if ( askAboutUpdates ) {
String message = ""; String message = "";
message += "Would you like Slipstream to periodically\n"; message += "Would you like Slipstream to periodically\n";
message += "download descriptions for the latest mods?\n\n"; message += "check for updates and download descriptions\n";
message += "for the latest mods?\n\n";
message += "You can change this later in modman.cfg."; message += "You can change this later in modman.cfg.";
int response = JOptionPane.showConfirmDialog(null, message, "Catalog Updates", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); int response = JOptionPane.showConfirmDialog(null, message, "Updates", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if ( response == JOptionPane.YES_OPTION ) if ( response == JOptionPane.YES_OPTION ) {
config.setProperty( "update_catalog", "true" ); config.setProperty( "update_catalog", "7" );
else config.setProperty( "update_app", "4" );
config.setProperty( "update_catalog", "false" ); } else {
config.setProperty( "update_catalog", "0" );
config.setProperty( "update_app", "0" );
}
} }

View file

@ -0,0 +1,45 @@
package net.vhati.modmanager.core;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.vhati.modmanager.core.ComparableVersion;
/**
* Holds info about available updates.
*/
public class AutoUpdateInfo {
private ComparableVersion latestVersion = null;
private Map<String,String> latestURLs = new TreeMap<String,String>();
private Map<ComparableVersion,List<String>> changelog = new TreeMap<ComparableVersion,List<String>>( Collections.reverseOrder() );
public void setLatestVersion( ComparableVersion version ) {
latestVersion = version;
}
public ComparableVersion getLatestVersion() {
return latestVersion;
}
public void putLatestURL( String os, String url ) {
latestURLs.put( os, url );
}
public void putChanges( ComparableVersion version, List<String> changeList ) {
changelog.put( version, changeList );
}
public Map<String,String> getLatestURLs() {
return latestURLs;
}
public Map<ComparableVersion,List<String>> getChangelog() {
return changelog;
}
}

View file

@ -29,6 +29,14 @@ public class SlipstreamConfig {
return config.setProperty( key, value ); return config.setProperty( key, value );
} }
public int getPropertyAsInt( String key, int defaultValue ) {
String s = config.getProperty( key );
if ( s != null && s.matches("^\\d+$") )
return Integer.parseInt( s );
else
return defaultValue;
}
public String getProperty( String key, String defaultValue ) { public String getProperty( String key, String defaultValue ) {
return config.getProperty( key, defaultValue ); return config.getProperty( key, defaultValue );
} }
@ -48,7 +56,8 @@ public class SlipstreamConfig {
configComments += " allow_zip - Sets whether to treat .zip files as .ftl files. Default: false.\n"; configComments += " allow_zip - Sets whether to treat .zip files as .ftl files. Default: false.\n";
configComments += " ftl_dats_path - The path to FTL's resources folder. If invalid, you'll be prompted.\n"; configComments += " ftl_dats_path - The path to FTL's resources folder. If invalid, you'll be prompted.\n";
configComments += " never_run_ftl - If true, there will be no offer to run FTL after patching. Default: false.\n"; configComments += " never_run_ftl - If true, there will be no offer to run FTL after patching. Default: false.\n";
configComments += " update_catalog - If true, periodically download descriptions for the latest mods.\n"; configComments += " update_catalog - If a number greater than 0, check for new mod descriptions every N days.\n";
configComments += " update_app - If a number greater than 0, check for new app version every N days.\n";
configComments += " use_default_ui - If true, no attempt will be made to resemble a native GUI. Default: false.\n"; configComments += " use_default_ui - If true, no attempt will be made to resemble a native GUI. Default: false.\n";
configComments += " remember_geometry - If true, window geometry will be saved on exit and restored on startup.\n"; configComments += " remember_geometry - If true, window geometry will be saved on exit and restored on startup.\n";
configComments += "\n"; configComments += "\n";

View file

@ -0,0 +1,77 @@
package net.vhati.modmanager.json;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.vhati.modmanager.core.AutoUpdateInfo;
import net.vhati.modmanager.core.ComparableVersion;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class JacksonAutoUpdateReader {
private static final Logger log = LogManager.getLogger(JacksonAutoUpdateReader.class);
public static AutoUpdateInfo parse( File jsonFile ) {
AutoUpdateInfo aui = new AutoUpdateInfo();
Exception exception = null;
try {
ObjectMapper mapper = new ObjectMapper();
mapper.configure( JsonParser.Feature.ALLOW_SINGLE_QUOTES, true );
mapper.setVisibility( PropertyAccessor.FIELD, Visibility.ANY );
JsonNode rootNode = mapper.readTree( jsonFile );
JsonNode historiesNode = rootNode.get( "history_versions" );
JsonNode historyNode = historiesNode.get( "1" );
JsonNode latestNode = historyNode.get( "latest" );
aui.setLatestVersion( new ComparableVersion( latestNode.get( "version" ).textValue() ) );
Iterator<Map.Entry<String,JsonNode>> fieldIt = latestNode.get( "urls" ).fields();
while ( fieldIt.hasNext() ) {
Map.Entry<String,JsonNode> entry = fieldIt.next();
aui.putLatestURL( entry.getKey(), entry.getValue().textValue() );
}
JsonNode changelogNode = historyNode.get( "changelog" );
for ( JsonNode releaseNode : changelogNode ) {
String releaseVersion = releaseNode.get( "version" ).textValue();
List<String> changeList = new ArrayList<String>( releaseNode.get( "changes" ).size() );
for ( JsonNode changeNode : releaseNode.get( "changes" ) ) {
changeList.add( changeNode.textValue() );
}
aui.putChanges( new ComparableVersion( releaseVersion ), changeList );
}
}
catch ( JsonProcessingException e ) {
exception = e;
}
catch ( IOException e ) {
exception = e;
}
if ( exception != null ) {
log.error( exception );
return null;
}
return aui;
}
}

View file

@ -76,7 +76,7 @@ public class URLFetcher {
int responseCode = httpConn.getResponseCode(); int responseCode = httpConn.getResponseCode();
if ( responseCode == HttpURLConnection.HTTP_NOT_MODIFIED ) { if ( responseCode == HttpURLConnection.HTTP_NOT_MODIFIED ) {
log.debug( String.format( "The server's \"%s\" has not been modified since the previous check.", httpConn.getURL().getFile() ) ); log.debug( String.format( "No need to update \"%s\", the server's copy has not been modified since the previous check.", localFile.getName() ) );
// Update the local file's timestamp as if it had downloaded. // Update the local file's timestamp as if it had downloaded.
localFile.setLastModified( new Date().getTime() ); localFile.setLastModified( new Date().getTime() );
@ -98,7 +98,7 @@ public class URLFetcher {
} }
} }
else { else {
log.error( String.format( "Download request failed: HTTP Code %d (%s).", responseCode, httpConn.getResponseMessage() ) ); log.error( String.format( "Download request failed for \"%s\": HTTP Code %d (%s).", httpConn.getURL(), responseCode, httpConn.getResponseMessage() ) );
return false; return false;
} }
} }

View file

@ -33,6 +33,7 @@ import java.util.HashMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
@ -59,6 +60,7 @@ import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import net.vhati.ftldat.FTLDat; import net.vhati.ftldat.FTLDat;
import net.vhati.modmanager.core.AutoUpdateInfo;
import net.vhati.modmanager.core.ComparableVersion; import net.vhati.modmanager.core.ComparableVersion;
import net.vhati.modmanager.core.FTLUtilities; import net.vhati.modmanager.core.FTLUtilities;
import net.vhati.modmanager.core.HashObserver; import net.vhati.modmanager.core.HashObserver;
@ -72,6 +74,7 @@ import net.vhati.modmanager.core.ModUtilities;
import net.vhati.modmanager.core.Report; import net.vhati.modmanager.core.Report;
import net.vhati.modmanager.core.Report.ReportFormatter; import net.vhati.modmanager.core.Report.ReportFormatter;
import net.vhati.modmanager.core.SlipstreamConfig; import net.vhati.modmanager.core.SlipstreamConfig;
import net.vhati.modmanager.json.JacksonAutoUpdateReader;
import net.vhati.modmanager.json.JacksonGrognakCatalogReader; import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
import net.vhati.modmanager.json.URLFetcher; import net.vhati.modmanager.json.URLFetcher;
import net.vhati.modmanager.ui.ChecklistTableModel; import net.vhati.modmanager.ui.ChecklistTableModel;
@ -92,26 +95,30 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
private static final Logger log = LogManager.getLogger(ManagerFrame.class); private static final Logger log = LogManager.getLogger(ManagerFrame.class);
public static final String CATALOG_URL = "https://raw.github.com/Vhati/Slipstream-Mod-Manager/master/skel_common/backup/current_catalog.json"; public static final String CATALOG_URL = "https://raw.github.com/Vhati/Slipstream-Mod-Manager/master/skel_common/backup/current_catalog.json";
public static final String AUTOUPDATE_URL = "https://raw.github.com/Vhati/Slipstream-Mod-Manager/master/auto-update.json"; public static final String APP_UPDATE_URL = "https://raw.github.com/Vhati/Slipstream-Mod-Manager/master/auto_update.json";
private File backupDir = new File( "./backup/" ); private File backupDir = new File( "./backup/" );
private File modsDir = new File( "./mods/" ); private File modsDir = new File( "./mods/" );
private int catalogFetchInterval = 7; // Days.
private File catalogFile = new File( backupDir, "current_catalog.json" ); private File catalogFile = new File( backupDir, "current_catalog.json" );
private File catalogETagFile = new File( backupDir, "current_catalog_etag.txt" ); private File catalogETagFile = new File( backupDir, "current_catalog_etag.txt" );
private File appUpdateFile = new File( backupDir, "auto_update.json" );
private File appUpdateETagFile = new File( backupDir, "auto_update_etag.txt" );
private SlipstreamConfig appConfig; private SlipstreamConfig appConfig;
private String appName; private String appName;
private ComparableVersion appVersion; private ComparableVersion appVersion;
private String appURL; private String appURL;
private String appAuthor; private String appAuthor;
private NerfListener nerfListener = new NerfListener( this );
private HashMap<File,String> modFileHashes = new HashMap<File,String>(); private HashMap<File,String> modFileHashes = new HashMap<File,String>();
private ModDB modDB = new ModDB(); private ModDB modDB = new ModDB();
private AutoUpdateInfo appUpdateInfo = null;
private NerfListener nerfListener = new NerfListener( this );
private ChecklistTableModel<ModFileInfo> localModsTableModel; private ChecklistTableModel<ModFileInfo> localModsTableModel;
private JTable localModsTable; private JTable localModsTable;
@ -128,6 +135,7 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
private JButton toggleAllBtn; private JButton toggleAllBtn;
private JButton validateBtn; private JButton validateBtn;
private JButton modsFolderBtn; private JButton modsFolderBtn;
private JButton updateBtn;
private JSplitPane splitPane; private JSplitPane splitPane;
private ModInfoArea infoArea; private ModInfoArea infoArea;
@ -198,9 +206,16 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
modsFolderBtn.setEnabled( Desktop.isDesktopSupported() ); modsFolderBtn.setEnabled( Desktop.isDesktopSupported() );
modActionsPanel.add( modsFolderBtn ); modActionsPanel.add( modsFolderBtn );
updateBtn = new JButton("Update");
updateBtn.setMargin( actionInsets );
updateBtn.addMouseListener( new StatusbarMouseListener( this, String.format( "Show info about the latest version of %s.", appName ) ) );
updateBtn.addActionListener(this);
updateBtn.setEnabled( false );
modActionsPanel.add( updateBtn );
topPanel.add( modActionsPanel, BorderLayout.EAST ); topPanel.add( modActionsPanel, BorderLayout.EAST );
JButton[] actionBtns = new JButton[] {patchBtn, toggleAllBtn, validateBtn, modsFolderBtn }; JButton[] actionBtns = new JButton[] {patchBtn, toggleAllBtn, validateBtn, modsFolderBtn, updateBtn };
int actionBtnWidth = Integer.MIN_VALUE; int actionBtnWidth = Integer.MIN_VALUE;
int actionBtnHeight = Integer.MIN_VALUE; int actionBtnHeight = Integer.MIN_VALUE;
for ( JButton btn : actionBtns ) { for ( JButton btn : actionBtns ) {
@ -402,6 +417,7 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
List<String> preferredOrder = loadModOrder(); List<String> preferredOrder = loadModOrder();
rescanMods( preferredOrder ); rescanMods( preferredOrder );
int catalogUpdateInterval = appConfig.getPropertyAsInt( "update_catalog", 0 );
boolean needNewCatalog = false; boolean needNewCatalog = false;
if ( catalogFile.exists() ) { if ( catalogFile.exists() ) {
@ -409,25 +425,26 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile ); ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
if ( currentDB != null ) modDB = currentDB; if ( currentDB != null ) modDB = currentDB;
if ( catalogUpdateInterval > 0 ) {
// Check if the downloaded catalog is stale. // Check if the downloaded catalog is stale.
Date catalogDate = new Date( catalogFile.lastModified() ); Date catalogDate = new Date( catalogFile.lastModified() );
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.add( Calendar.DATE, catalogFetchInterval * -1 ); cal.add( Calendar.DATE, catalogUpdateInterval * -1 );
if ( catalogDate.before( cal.getTime() ) ) { if ( catalogDate.before( cal.getTime() ) ) {
log.debug( String.format( "Catalog is older than %d days.", catalogFetchInterval ) ); log.debug( String.format( "Catalog is older than %d days.", catalogUpdateInterval ) );
needNewCatalog = true; needNewCatalog = true;
} else { } else {
log.debug( "Catalog isn't stale yet." ); log.debug( "Catalog isn't stale yet." );
} }
} }
}
else { else {
// Catalog file doesn't exist. // Catalog file doesn't exist.
needNewCatalog = true; needNewCatalog = true;
} }
// Don't update if the user doesn't want to. // Don't update if the user doesn't want to.
String updatesAllowed = appConfig.getProperty( "update_catalog", "false" ); if ( catalogUpdateInterval <= 0 ) needNewCatalog = false;
if ( !updatesAllowed.equals("true") ) needNewCatalog = false;
if ( needNewCatalog ) { if ( needNewCatalog ) {
Runnable fetchTask = new Runnable() { Runnable fetchTask = new Runnable() {
@ -441,11 +458,56 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
Thread fetchThread = new Thread( fetchTask ); Thread fetchThread = new Thread( fetchTask );
fetchThread.start(); fetchThread.start();
} }
int appUpdateInterval = appConfig.getPropertyAsInt( "update_app", 0 );
boolean needAppUpdate = false;
if ( appUpdateFile.exists() ) {
// Load the info first, before downloading.
AutoUpdateInfo aui = JacksonAutoUpdateReader.parse( appUpdateFile );
if ( aui != null ) {
appUpdateInfo = aui;
updateBtn.setEnabled( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
}
if ( appUpdateInterval > 0 ) {
// Check if the app update info is stale.
Date catalogDate = new Date( appUpdateFile.lastModified() );
Calendar cal = Calendar.getInstance();
cal.add( Calendar.DATE, catalogUpdateInterval * -1 );
if ( catalogDate.before( cal.getTime() ) ) {
log.debug( String.format( "App update info is older than %d days.", appUpdateInterval ) );
needAppUpdate = true;
} else {
log.debug( "App update info isn't stale yet." );
}
}
}
else {
// App update file doesn't exist.
needAppUpdate = true;
}
// Don't update if the user doesn't want to.
if ( appUpdateInterval <= 0 ) needAppUpdate = false;
if ( needAppUpdate ) {
Runnable fetchTask = new Runnable() {
@Override
public void run() {
boolean fetched = URLFetcher.refetchURL( APP_UPDATE_URL, appUpdateFile, appUpdateETagFile );
if ( fetched ) reloadAppUpdateInfo();
}
};
Thread fetchThread = new Thread( fetchTask );
fetchThread.start();
}
} }
/** /**
* Reparses and replace the downloaded ModDB catalog. (thread-safe) * Reparses and replaces the downloaded ModDB catalog. (thread-safe)
*/ */
public void reloadCatalog() { public void reloadCatalog() {
SwingUtilities.invokeLater(new Runnable() { SwingUtilities.invokeLater(new Runnable() {
@ -459,6 +521,24 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
}); });
} }
/**
* Reparses info about available app updates. (thread-safe)
*/
public void reloadAppUpdateInfo() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if ( appUpdateFile.exists() ) {
AutoUpdateInfo aui = JacksonAutoUpdateReader.parse( appUpdateFile );
if ( aui != null ) {
appUpdateInfo = aui;
updateBtn.setEnabled( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
}
}
}
});
}
/** /**
* Returns a mod list with names sorted in a preferred order. * Returns a mod list with names sorted in a preferred order.
@ -579,6 +659,42 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
infoArea.setDescription( appName, appAuthor, appVersion.toString(), appURL, body ); infoArea.setDescription( appName, appAuthor, appVersion.toString(), appURL, body );
} }
public void showAppUpdateInfo() {
StringBuilder buf = new StringBuilder();
for ( Map.Entry<ComparableVersion,List<String>> entry : appUpdateInfo.getChangelog().entrySet() ) {
if ( appVersion.compareTo( entry.getKey() ) >= 0 ) break;
if ( buf.length() > 0 ) buf.append( "\n" );
buf.append( entry.getKey() ).append( ":\n" );
for ( String change : entry.getValue() ) {
buf.append( " - " ).append( change ).append( "\n" );
}
}
try {
infoArea.clear();
infoArea.appendTitleText( "What's New\n" );
infoArea.appendRegularText( String.format( "Version %s: ", appUpdateInfo.getLatestVersion().toString() ) );
boolean first = true;
for ( Map.Entry<String,String> entry : appUpdateInfo.getLatestURLs().entrySet() ) {
if ( !first ) infoArea.appendRegularText( " " );
infoArea.appendRegularText( "[" );
infoArea.appendLinkText( entry.getValue(), entry.getKey() );
infoArea.appendRegularText( "]" );
first = false;
}
infoArea.appendRegularText( "\n" );
infoArea.appendRegularText( "\n" );
infoArea.appendRegularText( buf.toString() );
infoArea.setCaretPosition( 0 );
}
catch ( Exception e ) {
log.error( "Error filling info text area.", e );
}
}
/** /**
* Shows info about a local mod in the text area. * Shows info about a local mod in the text area.
*/ */
@ -720,6 +836,9 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
log.error( "Error opening mods/ folder.", f ); log.error( "Error opening mods/ folder.", f );
} }
} }
else if ( source == updateBtn ) {
showAppUpdateInfo();
}
else if ( source == rescanMenuItem ) { else if ( source == rescanMenuItem ) {
setStatusText( "" ); setStatusText( "" );
if ( rescanMenuItem.isEnabled() == false ) return; if ( rescanMenuItem.isEnabled() == false ) return;