diff --git a/src/main/java/common/HttpUtils.java b/src/main/java/common/HttpUtils.java new file mode 100644 index 0000000..646a940 --- /dev/null +++ b/src/main/java/common/HttpUtils.java @@ -0,0 +1,202 @@ +package common; + +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility methods to support HTTP network communication. Declared as a + * non-static class so that a mock implementation can be instantiated for testing. + */ +public class HttpUtils +{ + Logger LOGGER = Logger.getLogger( HttpUtils.class.getName() ); + + /** + * Sends an HTTP request. If postData is not null the request will be a POsT + * request otherwise it will be a GET request. + * + * @param urlString The URL to send the request to. + * @param postData Optional POST data. if not null, http request will be POST, otherwise it will be + * a GET + * @param contentType Optional content type for POST data. + * @param responseHeaders A map to store response headers. + * @param outputFilePath If not null, and the file exists, save the response to this file. + * @param resume Only applicable when outputFilePath is present. If false the + * target file should be truncated, true to try resume download and append + * data (works only in combination with a valid outputStream) + * @return The response from the server as a string. + */ + public String HTTPSendRequest( String urlString, String postData, String contentType, + Map responseHeaders, String outputFilePath, boolean resume ) + { + try + { + URL url = new URL( urlString ); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setUseCaches( false ); + connection.setDefaultUseCaches( false ); + if (postData != null && !postData.isEmpty()) + { + connection.setRequestMethod( "POST" ); + connection.setDoOutput( true ); + if (contentType != null && !contentType.isEmpty()) + { + connection.setRequestProperty( "Content-Type", contentType ); + } + // Copy POST data + try (OutputStream os = connection.getOutputStream()) + { + byte[] input = postData.getBytes( StandardCharsets.UTF_8 ); + os.write( input, 0, input.length ); + } + } + else + { + connection.setRequestMethod( "GET" ); + } + + if (resume && outputFilePath != null && Files.exists( Paths.get( outputFilePath ) )) + { + long existingSize = Files.size( Paths.get( outputFilePath ) ); + connection.setRequestProperty( "Range", "bytes=" + existingSize + "-" ); + LOGGER.info( "Resuming download from byte: " + existingSize ); + } + + // Get response + int responseCode = connection.getResponseCode(); + LOGGER.log( Level.FINER, "HTTP Response Code: " + responseCode ); + + // Populate response headers + if (responseHeaders != null) + { + connection.getHeaderFields().forEach( ( key, values ) -> + { + if (key != null && !values.isEmpty()) + { + responseHeaders.put( key, values.get( 0 ) ); // Take first value + } + } ); + } + + if (responseCode >= 200 && responseCode < 300) // || responseCode == 206 /* Partial Content for resume */) + { + InputStream is = connection.getInputStream(); + if (outputFilePath != null && !outputFilePath.isEmpty()) + { + try (FileOutputStream fos = new FileOutputStream( outputFilePath, resume )) + { // Append if resuming + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = is.read( buffer )) != -1) + { + fos.write( buffer, 0, bytesRead ); + } + } + return ""; // Return empty string if writing to file + } + else + { + try (BufferedReader in = new BufferedReader( new InputStreamReader( is, StandardCharsets.UTF_8 ) )) + { + StringBuilder response = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) + { + response.append( line ); + } + return response.toString(); + } + } + } + else + { + try (BufferedReader in = new BufferedReader( + new InputStreamReader( connection.getErrorStream(), StandardCharsets.UTF_8 ) )) + { + StringBuilder errorResponse = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) + { + errorResponse.append( line ); + } + // Attempt to parse error XML if available + if (errorResponse.length() > 0 && errorResponse.toString().startsWith( " responseHeaders ) + { + return HTTPSendRequest( urlString, null, contentType, + responseHeaders, null, false ); + } + + /** + * Sends an HTTP request. If postData is not null the request will be a POST + * request otherwise it will be a GET request. The response is assumed to be + * XML, and will be parsed into an XML Document. + * + * @param url The URL to send the request to. + * @param postData Optional POST data. if not null, http request will be POST, otherwise it will be a get + * @param contentType Optional content type for POST data. + * @param responseHeaders A map to store response headers. + * @param resume remains to be seen. + * + * @return The response from the server as an XML Document. + */ + public Document sendHTTPRequestForXML( String url, String postData, String contentType, + Map responseHeaders, boolean resume ) + throws IOException, SAXException, ParserConfigurationException + { + String response = HTTPSendRequest( url, postData, contentType, responseHeaders, null, resume ); + byte[] responseData = response.getBytes( StandardCharsets.UTF_8 ); + + DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance(); + fac.setNamespaceAware( true ); + DocumentBuilder db = fac.newDocumentBuilder(); + Document responseDoc = db.parse( new ByteArrayInputStream( responseData ) ); + responseDoc.getDocumentElement().normalize(); + return responseDoc; + } +}