Added support for embedded descriptions

This commit is contained in:
Vhati 2013-09-19 13:00:48 -04:00
parent 2cdba9062e
commit 5cd19480ad
22 changed files with 889 additions and 333 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View file

@ -1,13 +1,3 @@
Drop any .ftl files here and restart Slipstream in order to be able to patch them in.
The last-used order is remembered with modorder.txt, if it exists here.
"Beginning Scrap Advantage" is a simple example mod. All it does is give you some extra scrap when you first start a game. Rename .ftl to .zip and extract it in order to study it further.
"Engi Scrap Advantage" is another example mod demonstrating advanced XML tags to chain-load existing events. All it does is give you some extra scrap when you enter an Engi sector.
See also: "readme_modders.txt".
Those special tags require Slipstream Mod Manager 1.2+.

View file

@ -10,6 +10,7 @@ Changelog
- Fixed sloppy parser Validate error about things not allowed at root
- Added a Preferences dialog as an alternative to editing modman.cfg
- Added a troubleshooting note about Java 1.7.0_25 to readmes
- Added support for embedded descriptions in *.ftl files
1.2:
- Added a commandline interface

View file

@ -3,23 +3,29 @@ Mod Developer Notes
Creating an .ftl File
An .ftl file is simply a renamed .zip with a specific file structure.
For an example, try renaming and unpacking the example .ftl file that
comes with the program.
For an example, try renaming and unpacking the example mods.
The root of the ZIP file should contain one or more of these folders:
data/
audio/
fonts/
img/
mod-appendix/
You should ONLY put in the files that you want to modify. This keeps
mod sizes low and prevents major conflict between mods.
The "mod-appendix/" folder is for extra files that will not be inserted
into the game's resources. Slipstream will look for the following inside.
metadata.xml
Optional embedded description. (See the example mods.)
The Append Extension
Any file in your .ftl with the extension .xml.append will be appended to
its respective vanilla file. (See the example mod.)
its respective vanilla file. (See the example mods.)
It is highly recommended that you take advantage of this as much as
possible. As a rule of thumb, if you're editing an event xml file,

View file

@ -3,8 +3,10 @@ package net.vhati.modmanager.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.vhati.modmanager.core.ModInfo;
import net.vhati.modmanager.core.ModsInfo;
public class ModDB {
@ -13,12 +15,25 @@ public class ModDB {
public static final String FUZZY = "fuzzy";
// Accociates Forum thread urls with hashes of their forst post's content.
private HashMap<String,String> threadHashMap = new HashMap<String,String>();
private List<ModInfo> catalog = new ArrayList<ModInfo>();
public ModDB() {
}
/**
* Constructs a shallow copy of an existing ModDB.
*
* Different catalog list, same ModInfos.
*/
public ModDB( ModDB srcDB ) {
threadHashMap.putAll( srcDB.getThreadHashMap() );
catalog.addAll( srcDB.getCatalog() );
}
/**
* Returns mod info for a given file hash.
*/
@ -58,8 +73,16 @@ public class ModDB {
catalog.clear();
}
/**
* Returns the internal ArrayList of mod info.
* Returns the internal Map of forum thread urls and hashes of their first posts' content.
*/
public Map<String,String> getThreadHashMap() {
return threadHashMap;
}
/**
* Returns the internal List of mod info.
*/
public List<ModInfo> getCatalog() {
return catalog;
@ -97,4 +120,41 @@ public class ModDB {
return resultsMap;
}
/**
* Collects ModInfo objects that differ only in version, and creates ModsInfo objects.
*/
public List<ModsInfo> getCollatedModInfo() {
List<ModsInfo> results = new ArrayList<ModsInfo>();
List<ModInfo> seenList = new ArrayList<ModInfo>();
for ( ModInfo modInfo : catalog ) {
if ( seenList.contains( modInfo ) ) continue;
seenList.add( modInfo );
ModsInfo modsInfo = new ModsInfo();
modsInfo.setTitle( modInfo.getTitle() );
modsInfo.setAuthor( modInfo.getAuthor() );
modsInfo.setThreadURL( modInfo.getURL() );
modsInfo.setDescription( modInfo.getDescription() );
String threadHash = getThreadHash( modInfo.getURL() );
modsInfo.setThreadHash( ( threadHash != null ? threadHash : "???" ) );
modsInfo.putVersion( modInfo.getFileHash(), modInfo.getVersion() );
Map<String,List<ModInfo>> similarMods = getSimilarMods( modInfo );
for ( ModInfo altInfo : similarMods.get( ModDB.EXACT ) ) {
if ( seenList.contains( altInfo ) ) continue;
seenList.add( altInfo );
modsInfo.putVersion( altInfo.getFileHash(), altInfo.getVersion() );
}
results.add( modsInfo );
}
return results;
}
}

View file

@ -5,7 +5,7 @@ public class ModInfo {
private String title = "???";
private String author = "???";
private String url = "???";
private String description = "";
private String description = "???";
private String fileHash = "???";
private String version = "???";
@ -25,6 +25,19 @@ public class ModInfo {
public String getVersion() { return this.version; }
/**
* Returns true if all fields, aside from fileHash, are "???".
*/
public boolean isBlank() {
if ( !getTitle().equals( "???" ) ) return false;
if ( !getAuthor().equals( "???" ) ) return false;
if ( !getURL().equals( "???" ) ) return false;
if ( !getDescription().equals( "???" ) ) return false;
if ( !getVersion().equals( "???" ) ) return false;
return true;
}
@Override
public String toString() {
return getTitle();

View file

@ -1,5 +1,6 @@
package net.vhati.modmanager.core;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
@ -163,6 +164,7 @@ public class ModPatchThread extends Thread {
topFolderMap.put( "audio", resP );
topFolderMap.put( "fonts", resP );
topFolderMap.put( "img", resP );
topFolderMap.put( "mod-appendix", null );
// Track modified innerPaths in case they're clobbered.
List<String> moddedItems = new ArrayList<String>();
@ -182,16 +184,21 @@ public class ModPatchThread extends Thread {
for ( File modFile : modFiles ) {
if ( !keepRunning ) return false;
FileInputStream fis = null;
ZipInputStream zis = null;
try {
log.info( "" );
log.info( String.format( "Installing mod: %s", modFile.getName() ) );
observer.patchingMod( modFile );
zis = new ZipInputStream( new FileInputStream( modFile ) );
fis = new FileInputStream( modFile );
zis = new ZipInputStream( new BufferedInputStream( fis ) );
ZipEntry item;
while ( (item = zis.getNextEntry()) != null ) {
if ( item.isDirectory() ) continue;
if ( item.isDirectory() ) {
zis.closeEntry();
continue;
}
String innerPath = item.getName();
innerPath = innerPath.replace( '\\', '/' ); // Non-standard zips.
@ -209,7 +216,8 @@ public class ModPatchThread extends Thread {
AbstractPack ftlP = topFolderMap.get( topFolder );
if ( ftlP == null ) {
log.warn( String.format( "Unexpected innerPath: %s", innerPath ) );
if ( !topFolderMap.containsKey( topFolder ) )
log.warn( String.format( "Unexpected innerPath: %s", innerPath ) );
zis.closeEntry();
continue;
}
@ -289,7 +297,10 @@ public class ModPatchThread extends Thread {
}
}
finally {
try {if (zis != null) zis.close();}
try {if ( zis != null ) zis.close();}
catch ( Exception e ) {}
try {if ( fis != null ) fis.close();}
catch ( Exception e ) {}
System.gc();
@ -320,10 +331,10 @@ public class ModPatchThread extends Thread {
return true;
}
finally {
try {if (dataP != null) dataP.close();}
try {if ( dataP != null ) dataP.close();}
catch( Exception e ) {}
try {if (resP != null) resP.close();}
try {if ( resP != null ) resP.close();}
catch( Exception e ) {}
}
}

View file

@ -316,7 +316,7 @@ public class ModUtilities {
Pattern junkFilePtn = Pattern.compile( "[.]DS_Store$|^thumbs[.]db$" );
Pattern validRootDirPtn = Pattern.compile( "^(?:audio|data|fonts|img)/" );
Pattern validRootDirPtn = Pattern.compile( "^(?:audio|data|fonts|img|mod-appendix)/" );
List<String> seenJunkDirs = new ArrayList<String>();
CharsetEncoder asciiEncoder = Charset.forName("US-ASCII").newEncoder();

View file

@ -0,0 +1,46 @@
package net.vhati.modmanager.core;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Combined information from several similar ModInfo objects of varying versions.
*/
public class ModsInfo {
public String title = null;
public String author = null;
public String threadURL = null;
public String threadHash = null;
public String description = null;
private Map<String,String> versionsMap = new LinkedHashMap<String,String>();
public ModsInfo() {
}
public void setTitle( String s ) { this.title = s; }
public void setAuthor( String s ) { this.author = s; }
public void setThreadURL( String s ) { this.threadURL = s; }
public void setThreadHash( String s ) { this.threadHash = s; }
public void setDescription( String s ) { this.description = s; }
public String getTitle() { return this.title; }
public String getAuthor() { return this.author; }
public String getThreadURL() { return this.threadURL; }
public String getThreadHash() { return this.threadHash; }
public String getDescription() { return this.description; }
public void putVersion( String fileHash, String fileVersion ) {
versionsMap.put( fileHash, fileVersion );
}
/**
* Returns the internal Map of mod file hashes and version strings.
*/
public Map<String,String> getVersionsMap() {
return versionsMap;
}
}

View file

@ -0,0 +1,24 @@
package net.vhati.modmanager.core;
import java.io.File;
import net.vhati.modmanager.core.ModDB;
public interface ModsScanObserver {
/**
* A file's hash has been calculated.
*/
public void hashCalculated( File f, String hash );
/**
* A new ModDB of cached metadata is ready to use.
*/
public void localModDBUpdated( ModDB newDB );
/**
* Mods scanning ended.
*/
public void modsScanEnded();
}

View file

@ -0,0 +1,91 @@
package net.vhati.modmanager.core;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.vhati.ftldat.FTLDat;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.ModInfo;
import net.vhati.modmanager.core.ModsScanObserver;
import net.vhati.modmanager.xml.JDOMModMetadataReader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* A thread to calculate MD5 hashes of files in the background.
*
* As each file is hashed, a class implementing HashObserver is notified.
* Note: The callback on that class needs to be thread-safe.
*/
public class ModsScanThread extends Thread {
private static final Logger log = LogManager.getLogger(ModsScanThread.class);
private List<File> fileList = new ArrayList<File>();
private ModDB newDB;
private ModsScanObserver scanObserver;
public ModsScanThread( File[] files, ModDB knownDB, ModsScanObserver scanObserver ) {
this.fileList.addAll( Arrays.asList(files) );
this.newDB = new ModDB( knownDB );
this.scanObserver = scanObserver;
}
public void run() {
Map<File,String> hashMap = new HashMap<File,String>();
for ( File f : fileList ) {
String hash = calcFileMD5( f );
if ( hash != null ) {
hashMap.put( f, hash );
scanObserver.hashCalculated( f, hash );
}
}
log.info( "Background hashing finished." );
// Cache info about new files.
for ( File f : fileList ) {
String fileHash = hashMap.get( f );
if ( fileHash != null && newDB.getModInfo( fileHash ) == null ) {
ModInfo modInfo = JDOMModMetadataReader.parseModFile( f );
if ( modInfo != null ) {
modInfo.setFileHash( fileHash );
newDB.addMod( modInfo );
}
}
}
// Prune info about absent files.
for ( Iterator<ModInfo> it = newDB.getCatalog().iterator(); it.hasNext(); ) {
ModInfo modInfo = it.next();
if ( !hashMap.containsValue( modInfo.getFileHash() ) )
it.remove();
}
scanObserver.localModDBUpdated( new ModDB( newDB ) );
log.info( "Background metadata caching finished." );
scanObserver.modsScanEnded();
}
private String calcFileMD5( File f ) {
String result = null;
try {
result = FTLDat.calcFileMD5( f );
}
catch ( Exception e ) {
log.error( "Error while calculating hash for file: "+ f.getPath(), e );
}
return result;
}
}

View file

@ -19,6 +19,15 @@ public class SlipstreamConfig {
this.configFile = configFile;
}
/**
* Returns a copy of an existing SlipstreamConfig object.
*/
public SlipstreamConfig( SlipstreamConfig srcConfig ) {
this.configFile = srcConfig.getConfigFile();
this.config = new Properties();
this.config.putAll( srcConfig.getConfig() );
}
public Properties getConfig() { return config; }

View file

@ -0,0 +1,71 @@
package net.vhati.modmanager.json;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import net.vhati.modmanager.core.ModsInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class JacksonCatalogWriter {
/**
* Writes collated catalog entries to a file, as condensed json.
*/
public static void write( List<ModsInfo> modsInfoList, File dstFile ) throws IOException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode rootNode = mapper.createObjectNode();
ObjectNode catalogsNode = rootNode.objectNode();
rootNode.put( "catalog_versions", catalogsNode );
ArrayNode catalogNode = rootNode.arrayNode();
catalogsNode.put( "1", catalogNode );
for ( ModsInfo modsInfo : modsInfoList ) {
ObjectNode infoNode = rootNode.objectNode();
catalogNode.add( infoNode );
infoNode.put( "title", modsInfo.getTitle() );
infoNode.put( "author", modsInfo.getAuthor() );
infoNode.put( "desc", modsInfo.getDescription() );
infoNode.put( "url", modsInfo.getThreadURL() );
infoNode.put( "thread_hash", modsInfo.threadHash );
ArrayNode versionsNode = rootNode.arrayNode();
infoNode.put( "versions", versionsNode );
for ( Map.Entry<String,String> entry : modsInfo.getVersionsMap().entrySet() ) {
String versionFileHash = entry.getKey();
String versionString = entry.getValue();
ObjectNode versionNode = rootNode.objectNode();
versionNode.put( "hash", versionFileHash );
versionNode.put( "version", versionString );
versionsNode.add( versionNode );
}
}
OutputStream os = null;
try {
os = new FileOutputStream( dstFile );
OutputStreamWriter writer = new OutputStreamWriter( os, Charset.forName("US-ASCII") );
mapper.writeValue( writer, rootNode );
}
finally {
try {if ( os != null ) os.close();}
catch ( IOException e ) {}
}
}
}

View file

@ -2,8 +2,6 @@ package net.vhati.modmanager.json;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.ModInfo;
@ -63,7 +61,7 @@ public class JacksonGrognakCatalogReader {
exception = e;
}
if ( exception != null ) {
log.error( exception );
log.error( String.format( "While processing \"%s\", json parsing failed: %s", jsonFile.getName(), exception.getMessage() ), exception );
return null;
}

View file

@ -37,7 +37,9 @@ import java.util.regex.Pattern;
import net.vhati.ftldat.FTLDat;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.ModInfo;
import net.vhati.modmanager.core.ModsInfo;
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
import net.vhati.modmanager.json.JacksonCatalogWriter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -186,15 +188,15 @@ public class ForumScraper {
log.info( "Dumping json..." );
File dstFile = new File( cmdline.getOptionValue( "dump-json" ) );
List<ModsInfo> data = getCollatedModInfo( modDB );
if ( data.size() > 0 ) writeJSON( data, dstFile );
List<ModsInfo> data = modDB.getCollatedModInfo();
if ( data.size() > 0 ) JacksonCatalogWriter.write( data, dstFile );
}
if ( cmdline.hasOption( "dump-xml" ) ) {
log.info( "Dumping xml..." );
File dstFile = new File( cmdline.getOptionValue( "dump-xml" ) );
List<ModsInfo> data = getCollatedModInfo( modDB );
List<ModsInfo> data = modDB.getCollatedModInfo();
if ( data.size() > 0 ) writeXML( data, dstFile );
}
@ -218,43 +220,6 @@ public class ForumScraper {
}
/**
* Collects ModInfo objects that differ only in version, and creates ModsInfo objects.
*/
private static List<ModsInfo> getCollatedModInfo( ModDB modDB ) {
List<ModsInfo> results = new ArrayList<ModsInfo>();
List<ModInfo> seenList = new ArrayList<ModInfo>();
for ( ModInfo modInfo : modDB.getCatalog() ) {
if ( seenList.contains( modInfo ) ) continue;
seenList.add( modInfo );
ModsInfo modsInfo = new ModsInfo();
modsInfo.title = modInfo.getTitle();
modsInfo.author = modInfo.getAuthor();
modsInfo.threadURL = modInfo.getURL();
modsInfo.description = modInfo.getDescription();
String threadHash = modDB.getThreadHash( modInfo.getURL() );
modsInfo.threadHash = ( threadHash != null ? threadHash : "???" );
modsInfo.putVersion( modInfo.getFileHash(), modInfo.getVersion() );
HashMap<String,List<ModInfo>> similarMods = modDB.getSimilarMods( modInfo );
for ( ModInfo altInfo : similarMods.get( ModDB.EXACT ) ) {
if ( seenList.contains( altInfo ) ) continue;
seenList.add( altInfo );
modsInfo.putVersion( altInfo.getFileHash(), altInfo.getVersion() );
}
results.add( modsInfo );
}
return results;
}
/**
* Scrapes the forum for changed posts and returns info from updated mods.
*/
@ -265,11 +230,11 @@ public class ForumScraper {
for ( ScrapeResult scrapedInfo : scrapeList ) {
ModsInfo modsInfo = new ModsInfo();
modsInfo.title = scrapedInfo.title;
modsInfo.author = scrapedInfo.author;
modsInfo.threadURL = scrapedInfo.threadURL;
modsInfo.threadHash = scrapedInfo.threadHash;
modsInfo.description = scrapedInfo.rawDesc;
modsInfo.setTitle( scrapedInfo.title );
modsInfo.setAuthor( scrapedInfo.author );
modsInfo.setThreadURL( scrapedInfo.threadURL );
modsInfo.setThreadHash( scrapedInfo.threadHash );
modsInfo.setDescription( scrapedInfo.rawDesc );
modsInfo.putVersion( "???", "???"+ (scrapedInfo.wip ? " WIP" : "") );
results.add( modsInfo );
}
@ -562,23 +527,26 @@ public class ForumScraper {
XMLOutputter xmlOut = new XMLOutputter( xmlFormat );
writeIndent( dst, indent, depth++ ).append( "<modsinfo>\n" );
writeIndent( dst, indent, depth ); dst.append("<title>").append( xmlOut.escapeElementEntities( modsInfo.title ) ).append( "</title>\n" );
writeIndent( dst, indent, depth ); dst.append("<author>").append( xmlOut.escapeElementEntities( modsInfo.author ) ).append( "</author>\n" );
writeIndent( dst, indent, depth ); dst.append("<threadUrl><![CDATA[ ").append( modsInfo.threadURL ).append( " ]]></threadUrl>\n" );
writeIndent( dst, indent, depth ); dst.append("<title>").append( xmlOut.escapeElementEntities( modsInfo.getTitle() ) ).append( "</title>\n" );
writeIndent( dst, indent, depth ); dst.append("<author>").append( xmlOut.escapeElementEntities( modsInfo.getAuthor() ) ).append( "</author>\n" );
writeIndent( dst, indent, depth ); dst.append("<threadUrl><![CDATA[ ").append( modsInfo.getThreadURL() ).append( " ]]></threadUrl>\n" );
writeIndent( dst, indent, depth++ ).append( "<versions>\n" );
for ( String[] entry : modsInfo.versions ) {
for ( Map.Entry<String,String> entry : modsInfo.getVersionsMap().entrySet() ) {
String versionFileHash = entry.getKey();
String versionString = entry.getValue();
writeIndent( dst, indent, depth );
dst.append( "<version hash=\"" ).append( xmlOut.escapeAttributeEntities( entry[0] ) ).append( "\">" );
dst.append( xmlOut.escapeElementEntities( entry[1] ) );
dst.append( "<version hash=\"" ).append( xmlOut.escapeAttributeEntities( versionFileHash ) ).append( "\">" );
dst.append( xmlOut.escapeElementEntities( versionString ) );
dst.append( "</version>" ).append( "\n" );
}
writeIndent( dst, indent, --depth ).append( "</versions>\n" );
writeIndent( dst, indent, depth ); dst.append("<threadHash>").append( modsInfo.threadHash ).append( "</threadHash>\n" );
writeIndent( dst, indent, depth ); dst.append("<threadHash>").append( modsInfo.getThreadHash() ).append( "</threadHash>\n" );
dst.append( "\n" );
writeIndent( dst, indent, depth ); dst.append( "<description>" ).append( "<![CDATA[" );
dst.append( modsInfo.description );
dst.append( modsInfo.getDescription() );
dst.append( "]]>\n" );
writeIndent( dst, indent, depth ); dst.append( "</description>\n" );
@ -641,55 +609,6 @@ public class ForumScraper {
}
/**
* Writes collated catalog entries to a file, as condensed json.
*/
private static void writeJSON( List<ModsInfo> data, File dstFile ) throws IOException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode rootNode = mapper.createObjectNode();
ObjectNode catalogsNode = rootNode.objectNode();
rootNode.put( "catalog_versions", catalogsNode );
ArrayNode catalogNode = rootNode.arrayNode();
catalogsNode.put( "1", catalogNode );
for ( ModsInfo modsInfo : data ) {
ObjectNode infoNode = rootNode.objectNode();
catalogNode.add( infoNode );
infoNode.put( "title", modsInfo.title );
infoNode.put( "author", modsInfo.author );
infoNode.put( "desc", modsInfo.description );
infoNode.put( "url", modsInfo.threadURL );
infoNode.put( "thread_hash", modsInfo.threadHash );
ArrayNode versionsNode = rootNode.arrayNode();
infoNode.put( "versions", versionsNode );
for ( String[] entry : modsInfo.versions ) {
ObjectNode versionNode = rootNode.objectNode();
versionNode.put( "hash", entry[0] );
versionNode.put( "version", entry[1] );
versionsNode.add( versionNode );
}
}
OutputStream os = null;
try {
os = new FileOutputStream( dstFile );
OutputStreamWriter writer = new OutputStreamWriter( os, Charset.forName("US-ASCII") );
mapper.writeValue( writer, rootNode );
}
finally {
try {if ( os != null ) os.close();}
catch ( IOException e ) {}
}
}
/** Information gleaned from scraping the forum. */
private static class ScrapeResult {
public String threadURL = null;
@ -699,18 +618,4 @@ public class ForumScraper {
public String rawDesc = null;
public String threadHash = null;
}
/** Combined information from several similar ModInfo objects of varying versions. */
private static class ModsInfo {
public String threadURL = null;
public String title = null;
public String author = null;
public String description = null;
public String threadHash = null;
public ArrayList<String[]> versions = new ArrayList<String[]>();
public void putVersion( String fileHash, String fileVersion ) {
versions.add( new String[] {fileHash, fileVersion} );
}
}
}

View file

@ -35,6 +35,9 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.BorderFactory;
@ -65,22 +68,24 @@ import net.vhati.ftldat.FTLDat;
import net.vhati.modmanager.core.AutoUpdateInfo;
import net.vhati.modmanager.core.ComparableVersion;
import net.vhati.modmanager.core.FTLUtilities;
import net.vhati.modmanager.core.HashObserver;
import net.vhati.modmanager.core.HashThread;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.ModFileInfo;
import net.vhati.modmanager.core.ModInfo;
import net.vhati.modmanager.core.ModPatchThread;
import net.vhati.modmanager.core.ModPatchThread.BackedUpDat;
import net.vhati.modmanager.core.ModsScanObserver;
import net.vhati.modmanager.core.ModsScanThread;
import net.vhati.modmanager.core.ModUtilities;
import net.vhati.modmanager.core.Report;
import net.vhati.modmanager.core.Report.ReportFormatter;
import net.vhati.modmanager.core.SlipstreamConfig;
import net.vhati.modmanager.json.JacksonAutoUpdateReader;
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
import net.vhati.modmanager.json.JacksonCatalogWriter;
import net.vhati.modmanager.json.URLFetcher;
import net.vhati.modmanager.ui.ChecklistTableModel;
import net.vhati.modmanager.ui.InertPanel;
import net.vhati.modmanager.ui.ManagerInitThread;
import net.vhati.modmanager.ui.ModInfoArea;
import net.vhati.modmanager.ui.ModPatchDialog;
import net.vhati.modmanager.ui.ModXMLSandbox;
@ -93,7 +98,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class ManagerFrame extends JFrame implements ActionListener, HashObserver, Nerfable, Statusbar {
public class ManagerFrame extends JFrame implements ActionListener, ModsScanObserver, Nerfable, Statusbar {
private static final Logger log = LogManager.getLogger(ManagerFrame.class);
@ -103,12 +108,20 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
private File backupDir = new File( "./backup/" );
private File modsDir = new File( "./mods/" );
private File modorderFile = new File( modsDir, "modorder.txt" );
private File metadataFile = new File( backupDir, "cached_metadata.json" );
private File catalogFile = new File( backupDir, "current_catalog.json" );
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 final Lock managerLock = new ReentrantLock();
private final Condition scanEndedCond = managerLock.newCondition();
private boolean scanning = false;
private SlipstreamConfig appConfig;
private String appName;
private ComparableVersion appVersion;
@ -116,7 +129,8 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
private String appAuthor;
private HashMap<File,String> modFileHashes = new HashMap<File,String>();
private ModDB modDB = new ModDB();
private ModDB catalogModDB = new ModDB();
private ModDB localModDB = new ModDB();
private AutoUpdateInfo appUpdateInfo = null;
private Color updateBtnDisabledColor = UIManager.getColor( "Button.foreground" );
@ -295,7 +309,14 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
appConfig.writeConfig();
}
catch ( IOException f ) {
log.error( String.format( "Error writing config to \"%s\".", appConfig.getConfigFile() ), f );
log.error( String.format( "Error writing config to \"%s\".", appConfig.getConfigFile().getName() ), f );
}
try {
JacksonCatalogWriter.write( localModDB.getCollatedModInfo(), metadataFile );
}
catch ( IOException f ) {
log.error( String.format( "Error writing metadata from local mods to \"%s\".", metadataFile.getName() ), f );
}
System.gc(); // Ward off an intermittent InterruptedException from exit()?
@ -441,133 +462,17 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
*/
public void init() {
List<String> preferredOrder = loadModOrder();
rescanMods( preferredOrder );
int catalogUpdateInterval = appConfig.getPropertyAsInt( "update_catalog", 0 );
boolean needNewCatalog = false;
if ( catalogFile.exists() ) {
// Load the catalog first, before updating.
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
if ( currentDB != null ) modDB = currentDB;
if ( catalogUpdateInterval > 0 ) {
// Check if the downloaded catalog is stale.
Date catalogDate = new Date( catalogFile.lastModified() );
Calendar cal = Calendar.getInstance();
cal.add( Calendar.DATE, catalogUpdateInterval * -1 );
if ( catalogDate.before( cal.getTime() ) ) {
log.debug( String.format( "Catalog is older than %d days.", catalogUpdateInterval ) );
needNewCatalog = true;
} else {
log.debug( "Catalog isn't stale yet." );
}
}
}
else {
// Catalog file doesn't exist.
needNewCatalog = true;
}
// Don't update if the user doesn't want to.
if ( catalogUpdateInterval <= 0 ) needNewCatalog = false;
if ( needNewCatalog ) {
Runnable fetchTask = new Runnable() {
@Override
public void run() {
boolean fetched = URLFetcher.refetchURL( CATALOG_URL, catalogFile, catalogETagFile );
if ( fetched ) reloadCatalog();
}
};
Thread fetchThread = new Thread( fetchTask );
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;
boolean isUpdateAvailable = ( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
updateBtn.setForeground( isUpdateAvailable ? updateBtnEnabledColor : updateBtnDisabledColor );
updateBtn.setEnabled( isUpdateAvailable );
}
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 replaces the downloaded ModDB catalog. (thread-safe)
*/
public void reloadCatalog() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if ( catalogFile.exists() ) {
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
if ( currentDB != null ) modDB = currentDB;
}
}
});
}
/**
* 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;
boolean isUpdateAvailable = ( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
updateBtn.setForeground( isUpdateAvailable ? updateBtnEnabledColor : updateBtnDisabledColor );
updateBtn.setEnabled( isUpdateAvailable );
}
}
}
});
ManagerInitThread initThread = new ManagerInitThread( this,
new SlipstreamConfig( appConfig ),
modorderFile,
metadataFile,
catalogFile,
catalogETagFile,
appUpdateFile,
appUpdateETagFile
);
initThread.setDaemon( true );
initThread.start();
}
@ -599,38 +504,11 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
return sortedMods;
}
/**
* Reads modorder.txt and returns a list of mod names in preferred order.
*/
private List<String> loadModOrder() {
List<String> result = new ArrayList<String>();
FileInputStream is = null;
try {
is = new FileInputStream( new File( modsDir, "modorder.txt" ) );
BufferedReader br = new BufferedReader(new InputStreamReader( is, Charset.forName("UTF-8") ));
String line;
while ( (line = br.readLine()) != null ) {
result.add( line );
}
}
catch ( FileNotFoundException e ) {
}
catch ( IOException e ) {
log.error( "Error reading modorder.txt.", e );
}
finally {
try {if (is != null) is.close();}
catch (Exception e) {}
}
return result;
}
private void saveModOrder( List<ModFileInfo> sortedMods ) {
FileOutputStream os = null;
try {
os = new FileOutputStream( new File( modsDir, "modorder.txt" ) );
os = new FileOutputStream( modorderFile );
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( os, Charset.forName("UTF-8") ));
for ( ModFileInfo modFileInfo : sortedMods ) {
@ -640,7 +518,7 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
bw.flush();
}
catch ( IOException e ) {
log.error( "Error writing modorder.txt.", e );
log.error( String.format( "Error writing \"%s\".", modorderFile.getName() ), e );
}
finally {
try {if (os != null) os.close();}
@ -648,12 +526,20 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
}
}
/**
* Clears and syncs the mods list with mods/ dir, then starts a new hash thread.
*/
private void rescanMods( List<String> preferredOrder ) {
if ( rescanMenuItem.isEnabled() == false ) return;
rescanMenuItem.setEnabled( false );
public void rescanMods( List<String> preferredOrder ) {
managerLock.lock();
try {
scanning = true;
if ( rescanMenuItem.isEnabled() == false ) return;
rescanMenuItem.setEnabled( false );
}
finally {
managerLock.unlock();
}
modFileHashes.clear();
localModsTableModel.removeAllItems();
@ -672,9 +558,9 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
localModsTableModel.addItem( modFileInfo );
}
HashThread hashThread = new HashThread( modFiles, this );
hashThread.setDaemon( true );
hashThread.start();
ModsScanThread scanThread = new ModsScanThread( modFiles, localModDB, this );
scanThread.setDaemon( true );
scanThread.start();
}
@ -739,11 +625,19 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
/**
* Shows info about a local mod in the text area.
*
* Priority is given to embedded metadata.xml, but when that's absent,
* the gatalog's info is used. If the catalog doesn't have the info,
* an 'info missing' notice is shown instead.
*/
public void showLocalModInfo( ModFileInfo modFileInfo ) {
String modHash = modFileHashes.get( modFileInfo.getFile() );
ModInfo modInfo = modDB.getModInfo( modHash );
ModInfo modInfo = localModDB.getModInfo( modHash );
if ( modInfo == null || modInfo.isBlank() ) {
modInfo = catalogModDB.getModInfo( modHash );
}
if ( modInfo != null ) {
infoArea.setDescription( modInfo.getTitle(), modInfo.getAuthor(), modInfo.getVersion(), modInfo.getURL(), modInfo.getDescription() );
}
@ -768,7 +662,7 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
body += "If it is stable and has been out for over a month,\n";
body += "please let the Slipstream devs know where you ";
body += "found it.\n";
body += "found it.\n\n";
body += "Include the mod's version, and this hash.\n";
body += "MD5: "+ modHash +"\n";
infoArea.setDescription( modFileInfo.getName(), body );
@ -954,22 +848,123 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
@Override
public void hashCalculated( final File f, final String hash ) {
SwingUtilities.invokeLater( new Runnable() {
Runnable r = new Runnable() {
@Override
public void run() {
modFileHashes.put( f, hash );
}
});
public void run() { modFileHashes.put( f, hash ); }
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
@Override
public void hashingEnded() {
SwingUtilities.invokeLater( new Runnable() {
public void localModDBUpdated( ModDB newDB ) {
setLocalModDB( newDB );
}
@Override
public void modsScanEnded() {
Runnable r = new Runnable() {
@Override
public void run() {
rescanMenuItem.setEnabled( true );
managerLock.lock();
try {
rescanMenuItem.setEnabled( true );
scanning = false;
scanEndedCond.signalAll();
}
finally {
managerLock.unlock();
}
}
});
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
/**
* Returns a lock for synchronizing thread operations.
*/
public Lock getLock() {
return managerLock;
}
/**
* Returns a condition that will signal when the "mods/" dir has been scanned.
*
* Call getLock().lock() first.
* Loop while isScanning() is true, calling this condition's await().
* Finally, call getLock().unlock().
*/
public Condition getScanEndedCondition() {
return scanEndedCond;
}
/**
* Returns true if the "mods/" folder is currently being scanned. (thread-safe)
*/
public boolean isScanning() {
managerLock.lock();
try {
return scanning;
}
finally {
managerLock.unlock();
}
}
@Override
public void setNerfed( boolean b ) {
Component glassPane = this.getGlassPane();
if (b) {
glassPane.setVisible(true);
glassPane.requestFocusInWindow();
} else {
glassPane.setVisible(false);
}
}
/**
* Sets the ModDB for local metadata. (thread-safe)
*/
public void setLocalModDB( final ModDB newDB ) {
Runnable r = new Runnable() {
@Override
public void run() { localModDB = newDB; }
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
/**
* Sets the ModDB for the catalog. (thread-safe)
*/
public void setCatalogModDB( final ModDB newDB ) {
Runnable r = new Runnable() {
@Override
public void run() { catalogModDB = newDB; }
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
/**
* Sets info about available app updates. (thread-safe)
*/
public void setAppUpdateInfo( final AutoUpdateInfo aui ) {
Runnable r = new Runnable() {
@Override
public void run() {
appUpdateInfo = aui;
boolean isUpdateAvailable = ( appVersion.compareTo(appUpdateInfo.getLatestVersion()) < 0 );
updateBtn.setForeground( isUpdateAvailable ? updateBtnEnabledColor : updateBtnDisabledColor );
updateBtn.setEnabled( isUpdateAvailable );
}
};
if ( SwingUtilities.isEventDispatchThread() ) r.run();
else SwingUtilities.invokeLater( r );
}
@ -999,18 +994,6 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver
}
@Override
public void setNerfed( boolean b ) {
Component glassPane = this.getGlassPane();
if (b) {
glassPane.setVisible(true);
glassPane.requestFocusInWindow();
} else {
glassPane.setVisible(false);
}
}
/**
* Toggles a main window's nerfed state as popups are opened/disposed.

View file

@ -0,0 +1,216 @@
package net.vhati.modmanager.ui;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.locks.Lock;
import javax.swing.SwingUtilities;
import net.vhati.modmanager.core.AutoUpdateInfo;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.SlipstreamConfig;
import net.vhati.modmanager.json.JacksonAutoUpdateReader;
import net.vhati.modmanager.json.JacksonGrognakCatalogReader;
import net.vhati.modmanager.json.URLFetcher;
import net.vhati.modmanager.ui.ManagerFrame;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Performs I/O-related setup for ManagerFrame in the background.
*
* Reads cached local metadata.
* Rescans the "mods/" folder.
* Reads saved catalog, and redownloads if stale.
* Reads saved info about app updates, and redownloads if stale.
*/
public class ManagerInitThread extends Thread {
private static final Logger log = LogManager.getLogger(ManagerInitThread.class);
private final ManagerFrame frame;
private final SlipstreamConfig appConfig;
private final File modorderFile;
private final File metadataFile;
private final File catalogFile;
private final File catalogETagFile;
private final File appUpdateFile;
private final File appUpdateETagFile;
public ManagerInitThread( ManagerFrame frame, SlipstreamConfig appConfig, File modorderFile, File metadataFile, File catalogFile, File catalogETagFile, File appUpdateFile, File appUpdateETagFile ) {
this.frame = frame;
this.appConfig = appConfig;
this.modorderFile = modorderFile;
this.metadataFile = metadataFile;
this.catalogFile = catalogFile;
this.catalogETagFile = catalogETagFile;
this.appUpdateFile = appUpdateFile;
this.appUpdateETagFile = appUpdateETagFile;
}
@Override
public void run() {
try {
init();
}
catch ( Exception e ) {
log.error( "Error during ManagerFrame init.", e );
}
}
private void init() throws InterruptedException {
if ( metadataFile.exists() ) {
// Load cached metadata first, before scanning for new info.
ModDB cachedDB = JacksonGrognakCatalogReader.parse( metadataFile );
if ( cachedDB != null ) frame.setLocalModDB( cachedDB );
}
final List<String> preferredOrder = loadModOrder();
Lock managerLock = frame.getLock();
managerLock.lock();
try {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() { frame.rescanMods( preferredOrder ); }
});
// Wait until notified that "mods/" has been scanned.
while ( frame.isScanning() ) {
frame.getScanEndedCondition().await();
}
}
finally {
managerLock.unlock();
}
int catalogUpdateInterval = appConfig.getPropertyAsInt( "update_catalog", 0 );
boolean needNewCatalog = false;
if ( catalogFile.exists() ) {
// Load the catalog first, before updating.
reloadCatalog();
if ( catalogUpdateInterval > 0 ) {
// Check if the downloaded catalog is stale.
if ( isFileStale( catalogFile, catalogUpdateInterval ) ) {
log.debug( String.format( "Catalog is older than %d days.", catalogUpdateInterval ) );
needNewCatalog = true;
} else {
log.debug( "Catalog isn't stale yet." );
}
}
}
else {
// Catalog file doesn't exist.
needNewCatalog = true;
}
// Don't update if the user doesn't want to.
if ( catalogUpdateInterval <= 0 ) needNewCatalog = false;
if ( needNewCatalog ) {
boolean fetched = URLFetcher.refetchURL( ManagerFrame.CATALOG_URL, catalogFile, catalogETagFile );
if ( fetched && catalogFile.exists() ) {
reloadCatalog();
}
}
int appUpdateInterval = appConfig.getPropertyAsInt( "update_app", 0 );
boolean needAppUpdate = false;
if ( appUpdateFile.exists() ) {
// Load the info first, before downloading.
reloadAppUpdateInfo();
if ( appUpdateInterval > 0 ) {
// Check if the app update info is stale.
if ( isFileStale( appUpdateFile, catalogUpdateInterval ) ) {
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 ) {
boolean fetched = URLFetcher.refetchURL( ManagerFrame.APP_UPDATE_URL, appUpdateFile, appUpdateETagFile );
if ( fetched && appUpdateFile.exists() ) {
reloadAppUpdateInfo();
}
}
}
/**
* Reads modorder.txt and returns a list of mod names in preferred order.
*/
private List<String> loadModOrder() {
List<String> result = new ArrayList<String>();
FileInputStream is = null;
try {
is = new FileInputStream( modorderFile );
BufferedReader br = new BufferedReader(new InputStreamReader( is, Charset.forName("UTF-8") ));
String line;
while ( (line = br.readLine()) != null ) {
result.add( line );
}
}
catch ( FileNotFoundException e ) {
}
catch ( IOException e ) {
log.error( String.format( "Error reading \"%s\".", modorderFile.getName() ), e );
}
finally {
try {if ( is != null ) is.close();}
catch ( Exception e ) {}
}
return result;
}
private void reloadCatalog() {
ModDB currentDB = JacksonGrognakCatalogReader.parse( catalogFile );
if ( currentDB != null ) frame.setCatalogModDB( currentDB );
}
private void reloadAppUpdateInfo() {
AutoUpdateInfo aui = JacksonAutoUpdateReader.parse( appUpdateFile );
if ( aui != null ) frame.setAppUpdateInfo( aui );
}
/**
* Returns true if a file is older than N days.
*/
private boolean isFileStale( File f, int maxDays ) {
Date modifiedDate = new Date( f.lastModified() );
Calendar cal = Calendar.getInstance();
cal.add( Calendar.DATE, maxDays * -1 );
return modifiedDate.before( cal.getTime() );
}
}

View file

@ -0,0 +1,132 @@
package net.vhati.modmanager.xml;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import net.vhati.modmanager.core.ModDB;
import net.vhati.modmanager.core.ModInfo;
import net.vhati.modmanager.core.ModUtilities;
import net.vhati.modmanager.core.ModUtilities.DecodeResult;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class JDOMModMetadataReader {
private static final Logger log = LogManager.getLogger(JDOMModMetadataReader.class);
public static final String METADATA_INNERPATH = "mod-appendix/metadata.xml";
/**
* Reads metadata.xml from a mod file and returns a ModInfo object.
*
* @return the read metadata, a blank ModInfo, or null if an error occurred
*/
public static ModInfo parseModFile( File modFile ) {
ModInfo modInfo = null;
InputStream fis = null;
ZipInputStream zis = null;
Exception exception = null;
try {
fis = new FileInputStream( modFile );
zis = new ZipInputStream( new BufferedInputStream( fis ) );
ZipEntry item;
while ( (item = zis.getNextEntry()) != null ) {
if ( item.isDirectory() ) {
zis.closeEntry();
continue;
}
String innerPath = item.getName();
innerPath = innerPath.replace( '\\', '/' ); // Non-standard zips.
if ( innerPath.equals( METADATA_INNERPATH ) ) {
String metadataText = ModUtilities.decodeText( zis, modFile.getName()+":"+METADATA_INNERPATH ).text;
modInfo = parse( metadataText );
zis.closeEntry();
break;
}
zis.closeEntry();
}
}
catch ( JDOMException e ) {
exception = e;
}
catch ( IOException e ) {
exception = e;
}
finally {
try {if ( zis != null ) zis.close();}
catch ( IOException e ) {}
try {if ( fis != null ) fis.close();}
catch ( IOException e ) {}
}
if ( exception != null ) {
log.error( String.format( "While processing \"%s:%s\", strict parsing failed: %s", modFile.getName(), METADATA_INNERPATH, exception.getMessage() ), exception );
return null;
}
if ( modInfo == null ) modInfo = new ModInfo();
return modInfo;
}
/**
* Reads a mod's metadata.xml and returns a ModInfo object.
*/
public static ModInfo parse( String metadataText ) throws IOException, JDOMException {
ModInfo modInfo = new ModInfo();
SAXBuilder strictParser = new SAXBuilder();
Document doc = strictParser.build( new StringReader( metadataText ) );
Element root = doc.getRootElement();
String modTitle = root.getChildTextTrim( "title" );
if ( modTitle != null && modTitle.length() > 0 )
modInfo.setTitle( modTitle );
else
throw new JDOMException( "Missing title." );
String modURL = root.getChildTextTrim( "threadUrl" );
if ( modURL != null && modURL.length() > 0 )
modInfo.setURL( modURL );
else
throw new JDOMException( "Missing threadUrl." );
String modAuthor = root.getChildTextTrim( "author" );
if ( modAuthor != null && modAuthor.length() > 0 )
modInfo.setAuthor( modAuthor );
else
throw new JDOMException( "Missing author." );
String modVersion = root.getChildTextTrim( "version" );
if ( modVersion != null && modVersion.length() > 0 )
modInfo.setVersion( modVersion );
else
throw new JDOMException( "Missing version." );
String modDesc = root.getChildTextTrim( "description" );
if ( modDesc != null && modDesc.length() > 0 )
modInfo.setDescription( modDesc );
else
throw new JDOMException( "Missing description." );
return modInfo;
}
}