diff --git a/pom.xml b/pom.xml
index fdd4309..a0ca0a2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,11 @@
jdom2
2.0.6
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.4
+
info.picocli
picocli
diff --git a/readme_developers.txt b/readme_developers.txt
index e615cc9..7a9c7b1 100644
--- a/readme_developers.txt
+++ b/readme_developers.txt
@@ -32,6 +32,9 @@ To build, run "mvn clean package" in this folder.
This project depends on the following libraries.
+- Apache HttpComponents
+ https://hc.apache.org/
+ (For JavaDocs, click HttpCore or HttpClient, then again under "Project reports".)
- Jackson JSON Processor 2.x
http://jackson.codehaus.org/Home
(For JavaDocs, look right.)
diff --git a/skel_common/backup/auto_update.json b/skel_common/backup/auto_update.json
index 99613ba..bda8914 100644
--- a/skel_common/backup/auto_update.json
+++ b/skel_common/backup/auto_update.json
@@ -23,6 +23,7 @@
"Disabled XML escaping when reencoding to ensure invalid chars cause an error",
"Changed logging framework to SLF4J/Logback",
"Changed command line parser to picocli",
+ "Migrated to the Apache HttpComponents library",
"Made launcher scripts on OSX find java the recommended way"
]
},
diff --git a/skel_common/readme_changelog.txt b/skel_common/readme_changelog.txt
index 356c110..a471fa3 100644
--- a/skel_common/readme_changelog.txt
+++ b/skel_common/readme_changelog.txt
@@ -9,6 +9,7 @@ Changelog
- Added a Validate warning about FTL 1.5.13 for chars outside windows-1252
- Changed logging framework to SLF4J/Logback
- Changed command line parser to picocli
+- Migrated to the Apache HttpComponents library
- Made launcher scripts on OSX find java the recommended way
1.9:
diff --git a/src/main/java/net/vhati/modmanager/core/AutoUpdateInfo.java b/src/main/java/net/vhati/modmanager/core/AutoUpdateInfo.java
index 1c3b1ca..adbcf28 100644
--- a/src/main/java/net/vhati/modmanager/core/AutoUpdateInfo.java
+++ b/src/main/java/net/vhati/modmanager/core/AutoUpdateInfo.java
@@ -13,9 +13,9 @@ import net.vhati.modmanager.core.ComparableVersion;
*/
public class AutoUpdateInfo {
private ComparableVersion latestVersion = null;
- private Map latestURLs = new TreeMap();
+ private Map latestURLs = new TreeMap();
private String notice = null;
- private Map> changelog = new TreeMap>( Collections.reverseOrder() );
+ private Map> changelog = new TreeMap>( Collections.reverseOrder() );
public void setLatestVersion( ComparableVersion version ) {
@@ -26,7 +26,6 @@ public class AutoUpdateInfo {
return latestVersion;
}
-
public void setNotice( String s ) {
notice = s;
}
@@ -35,7 +34,6 @@ public class AutoUpdateInfo {
return notice;
}
-
public void putLatestURL( String os, String url ) {
latestURLs.put( os, url );
}
@@ -44,13 +42,11 @@ public class AutoUpdateInfo {
changelog.put( version, changeList );
}
-
-
- public Map getLatestURLs() {
+ public Map getLatestURLs() {
return latestURLs;
}
- public Map> getChangelog() {
+ public Map> getChangelog() {
return changelog;
}
}
diff --git a/src/main/java/net/vhati/modmanager/core/ComparableVersion.java b/src/main/java/net/vhati/modmanager/core/ComparableVersion.java
index f09e980..15b371d 100644
--- a/src/main/java/net/vhati/modmanager/core/ComparableVersion.java
+++ b/src/main/java/net/vhati/modmanager/core/ComparableVersion.java
@@ -12,11 +12,9 @@ import java.util.regex.Pattern;
* It is composed of three parts:
* - A series of period-separated positive ints.
*
- * - The numbers may be immediately followed by a short
- * suffix string.
+ * - The numbers may be immediately followed by a short suffix string.
*
- * - Finally, a string comment, separated from the rest
- * by a space.
+ * - Finally, a string comment, separated from the rest by a space.
*
* The (numbers + suffix) or comment may appear alone.
*
@@ -38,7 +36,7 @@ public class ComparableVersion implements Comparable {
public ComparableVersion( int[] numbers, String suffix, String comment ) {
this.numbers = numbers;
- setSuffix( suffix );
+ setSuffix( suffix );
setComment( comment );
}
@@ -148,16 +146,14 @@ public class ComparableVersion implements Comparable {
Matcher m = suffixPtn.matcher( s );
if ( m.matches() ) {
- suffix = s;
-
// Matched groups 1 and 2... or 3.
- if ( m.group(1) != null ) {
+ if ( m.group( 1 ) != null ) {
suffixDivider = m.group( 1 );
}
- if ( m.group(2) != null ) {
- suffixNum = Integer.parseInt( m.group(2) );
+ if ( m.group( 2 ) != null ) {
+ suffixNum = Integer.parseInt( m.group( 2 ) );
} else {
suffixNum = -1;
}
diff --git a/src/main/java/net/vhati/modmanager/json/JacksonAutoUpdateReader.java b/src/main/java/net/vhati/modmanager/json/JacksonAutoUpdateReader.java
index 17667d7..51b9b84 100644
--- a/src/main/java/net/vhati/modmanager/json/JacksonAutoUpdateReader.java
+++ b/src/main/java/net/vhati/modmanager/json/JacksonAutoUpdateReader.java
@@ -3,7 +3,6 @@ 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;
@@ -28,10 +27,11 @@ public class JacksonAutoUpdateReader {
public static AutoUpdateInfo parse( File jsonFile ) {
- AutoUpdateInfo aui = new AutoUpdateInfo();
Exception exception = null;
try {
+ AutoUpdateInfo aui = new AutoUpdateInfo();
+
ObjectMapper mapper = new ObjectMapper();
mapper.configure( JsonParser.Feature.ALLOW_SINGLE_QUOTES, true );
mapper.setVisibility( PropertyAccessor.FIELD, Visibility.ANY );
@@ -43,9 +43,9 @@ public class JacksonAutoUpdateReader {
JsonNode latestNode = historyNode.get( "latest" );
aui.setLatestVersion( new ComparableVersion( latestNode.get( "version" ).textValue() ) );
- Iterator> fieldIt = latestNode.get( "urls" ).fields();
+ Iterator> fieldIt = latestNode.get( "urls" ).fields();
while ( fieldIt.hasNext() ) {
- Map.Entry entry = fieldIt.next();
+ Map.Entry entry = fieldIt.next();
aui.putLatestURL( entry.getKey(), entry.getValue().textValue() );
}
@@ -67,6 +67,8 @@ public class JacksonAutoUpdateReader {
}
aui.putChanges( new ComparableVersion( releaseVersion ), changeList );
}
+
+ return aui;
}
catch ( JsonProcessingException e ) {
exception = e;
@@ -76,9 +78,8 @@ public class JacksonAutoUpdateReader {
}
if ( exception != null ) {
log.error( "Failed to parse info about available updates", exception );
- return null;
}
- return aui;
+ return null;
}
}
diff --git a/src/main/java/net/vhati/modmanager/json/JacksonCatalogReader.java b/src/main/java/net/vhati/modmanager/json/JacksonCatalogReader.java
index d1e3228..6da9c47 100644
--- a/src/main/java/net/vhati/modmanager/json/JacksonCatalogReader.java
+++ b/src/main/java/net/vhati/modmanager/json/JacksonCatalogReader.java
@@ -23,10 +23,11 @@ public class JacksonCatalogReader {
public static ModDB parse( File jsonFile ) {
- ModDB modDB = new ModDB();
Exception exception = null;
try {
+ ModDB modDB = new ModDB();
+
ObjectMapper mapper = new ObjectMapper();
mapper.configure( JsonParser.Feature.ALLOW_SINGLE_QUOTES, true );
mapper.setVisibility( PropertyAccessor.FIELD, Visibility.ANY );
@@ -53,6 +54,8 @@ public class JacksonCatalogReader {
modDB.addMod( modInfo );
}
}
+
+ return modDB;
}
catch ( JsonProcessingException e ) {
exception = e;
@@ -65,6 +68,6 @@ public class JacksonCatalogReader {
return null;
}
- return modDB;
+ return null;
}
}
diff --git a/src/main/java/net/vhati/modmanager/json/URLFetcher.java b/src/main/java/net/vhati/modmanager/json/URLFetcher.java
index b0d64ec..c0395ee 100644
--- a/src/main/java/net/vhati/modmanager/json/URLFetcher.java
+++ b/src/main/java/net/vhati/modmanager/json/URLFetcher.java
@@ -10,10 +10,6 @@ import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLConnection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -21,6 +17,14 @@ import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+
public class URLFetcher {
@@ -28,104 +32,129 @@ public class URLFetcher {
/**
- * Downloads content from a url into a file, if the remote content has changed.
+ * Downloads content from a url to one file, and its ETag to another.
+ *
+ * If the ETag files exists, it will be read to inform the GET request.
+ *
+ * If the content has not changed, the destination file's modified date
+ * will be reset to the present time.
+ *
+ * If the content has changed, it will be written to the file, as will the
+ * new ETag.
*
* @return true if successfully downloaded, false otherwise
*/
public static boolean refetchURL( String url, File localFile, File eTagFile ) {
String localETag = null;
- log.debug( String.format( "Attempting to download the latest \"%s\".", localFile.getName() ) );
+ log.debug( String.format( "Attempting to download the latest \"%s\"", localFile.getName() ) );
if ( eTagFile.exists() ) {
// Load the old eTag.
InputStream etagIn = null;
+ BufferedReader etagReader = null;
try {
etagIn = new FileInputStream( eTagFile );
- BufferedReader br = new BufferedReader( new InputStreamReader( etagIn, "UTF-8" ) );
- String line = br.readLine();
- if ( line.length() > 0 )
+ etagReader = new BufferedReader( new InputStreamReader( etagIn, "UTF-8" ) );
+ String line = etagReader.readLine();
+ if ( line.length() > 0 ) {
localETag = line;
+ }
}
catch ( IOException e ) {
// Not serious enough to be a real error.
- log.debug( String.format( "Error reading eTag from \"%s\".", eTagFile.getName() ), e );
+ log.debug( String.format( "Error reading eTag from \"%s\"", eTagFile.getName() ), e );
}
finally {
+ try {if ( etagReader != null ) etagReader.close();}
+ catch ( IOException e ) {}
+
try {if ( etagIn != null ) etagIn.close();}
catch ( IOException e ) {}
}
}
- String remoteETag = null;
- InputStream urlIn = null;
+ HttpGet request = null;
OutputStream localOut = null;
+ String remoteETag = null;
+
+ RequestConfig requestConfig = RequestConfig.custom()
+ .setConnectionRequestTimeout( 5000 )
+ .setConnectTimeout( 5000 )
+ .setSocketTimeout( 10000 )
+ .setRedirectsEnabled( true )
+ .build();
+
+ CloseableHttpClient httpClient = HttpClientBuilder.create()
+ .setDefaultRequestConfig( requestConfig )
+ .disableAuthCaching()
+ .disableAutomaticRetries()
+ .disableConnectionState()
+ .disableCookieManagement()
+ //.setUserAgent( "" )
+ .build();
+
try {
- URLConnection conn = new URL( url ).openConnection();
+ request = new HttpGet( url );
- if ( conn instanceof HttpURLConnection == false ) {
- log.error( String.format( "Non-Http(s) URL given for fetching: %s", url ) );
- return false;
+ HttpResponse response = httpClient.execute( request );
+
+ int status = response.getStatusLine().getStatusCode();
+ if ( status >= 200 && status < 300 ) {
+
+ HttpEntity entity = response.getEntity();
+ if ( entity != null ) {
+ localOut = new FileOutputStream( localFile );
+ entity.writeTo( localOut );
+ }
+
+ if ( response.containsHeader( "ETag" ) ) {
+ remoteETag = response.getLastHeader( "ETag" ).getValue();
+ }
}
- HttpURLConnection httpConn = (HttpURLConnection)conn;
-
- httpConn.setReadTimeout( 10000 );
- if ( localETag != null )
- httpConn.setRequestProperty( "If-None-Match", localETag );
- httpConn.connect();
-
- int responseCode = httpConn.getResponseCode();
-
- if ( responseCode == HttpURLConnection.HTTP_NOT_MODIFIED ) {
- log.debug( String.format( "No need to update \"%s\", the server's copy has not been modified since the previous check", localFile.getName() ) );
+ else if ( status == 304 ) { // Not modified.
+ log.debug( String.format( "No need to download \"%s\", the server's copy has not been modified", localFile.getName() ) );
// Update the local file's timestamp as if it had downloaded.
localFile.setLastModified( new Date().getTime() );
-
return false;
}
- else if ( responseCode == HttpURLConnection.HTTP_OK ) {
- Map> headerMap = httpConn.getHeaderFields();
- List eTagValues = headerMap.get( "ETag" );
- if ( eTagValues != null && eTagValues.size() > 0 )
- remoteETag = eTagValues.get( 0 );
-
- urlIn = httpConn.getInputStream();
- localOut = new FileOutputStream( localFile );
- byte[] buf = new byte[4096];
- int len;
- while ( (len = urlIn.read(buf)) >= 0 ) {
- localOut.write( buf, 0, len );
- }
- }
else {
- log.error( String.format( "Download request failed for \"%s\": HTTP Code %d (%s)", httpConn.getURL(), responseCode, httpConn.getResponseMessage() ) );
- return false;
+ throw new ClientProtocolException( "Unexpected response status: "+ status );
}
}
+ catch ( ClientProtocolException e ) {
+ log.error( "GET request failed for url: "+ request.getURI().toString(), e );
+ return false;
+ }
catch ( IOException e ) {
- log.error( String.format( "Error downloading the latest \"%s\"", localFile.getName() ), e );
+ log.error( "Download failed for url: "+ request.getURI().toString(), e );
+ return false;
}
finally {
- try {if ( urlIn != null ) urlIn.close();}
+ try {if ( localOut != null ) localOut.close();}
catch ( IOException e ) {}
- try {if ( localOut != null ) localOut.close();}
+ try {httpClient.close();}
catch ( IOException e ) {}
}
if ( remoteETag != null ) {
// Save the new eTag.
OutputStream etagOut = null;
+ BufferedWriter etagWriter = null;
try {
etagOut = new FileOutputStream( eTagFile );
- BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( etagOut, "UTF-8" ) );
- bw.append( remoteETag );
- bw.flush();
+ etagWriter = new BufferedWriter( new OutputStreamWriter( etagOut, "UTF-8" ) );
+ etagWriter.append( remoteETag );
+ etagWriter.flush();
}
catch ( IOException e ) {
log.error( String.format( "Error writing eTag to \"%s\"", eTagFile.getName() ), e );
}
finally {
+ try {if ( etagWriter != null ) etagWriter.close();}
+ catch ( IOException e ) {}
+
try {if ( etagOut != null ) etagOut.close();}
catch ( IOException e ) {}
}