/*
* Created on Feb 9, 2009 by aunger
*
* Copyright (c) 2009 Concord Consortium. Created
* by Concord Consortium.
*
* This software is distributed under the GNU Lesser General Public License, v2.
*
* Permission is hereby granted, without written agreement and without license
* or royalty fees, to use, copy, modify, and distribute this software and its
* documentation for any purpose, provided that the above copyright notice and
* the following two paragraphs appear in all copies of this software.
*
* Concord Consortium SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE. THE SOFTWAREAND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
* HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
* MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
*
* IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
* SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
* REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.concord.otrunk.net;
import java.awt.BorderLayout;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import org.concord.framework.util.IResourceLoader;
/**
* @author aunger
*
*/
public class OTrunkResourceLoader implements IResourceLoader{
/**
* Logger for this class
*/
private static final Logger logger = Logger.getLogger(OTrunkResourceLoader.class.getName());
private URL url;
private long lastModified = 0;
private boolean promptRetryQuit;
private URLConnection connection;
private InputStream urlInputStream;
private int tryCount;
private int responseCode = -1;
private int contentLength = -1;
private static int ATTEMPTS_BEFORE_PROMPTING = 2;
// FIXME this will fail in a secure environment that can't access system properties
private static boolean silentMode = Boolean.getBoolean("sail.rloader.silent");
protected OTrunkResourceLoader(URL url, boolean promptRetryQuit)
{
this.url = url;
this.promptRetryQuit = promptRetryQuit;
}
public URL getURL()
{
return url;
}
/**
* Get an input stream from a URL. The simple way to do this is just:
* url.openConnection().getInputStream()
*
* This method does more than that. It sets some request properties indicating
* it wants an xml file, and it can handle gzip encoding.
*
* Then it checks the response code.
*
* Finally it checks if the returned contentEncoding is "gzip", in which case
* it uses a GZIPInputStream to uncompress the content.
*
* FIXME It currently throws an ResourceLoadError if there is a problem. That is a hold over
* from when this was a RequiredResourcedLoader. If the resource isn't required it should
* throw something nicer. Or the concept of required should be rethought a bit.
*
* @param resourceUrl
* @return
*/
public InputStream getInputStream() throws ResourceLoadException
{
logger.info("loading: " + url.toString());
tryCount = 0;
while (true) {
HttpURLConnection httpConnection = null;
try {
// This should not actually connect to the server yet
// that happens with the connect method.
connection = url.openConnection();
} catch (IOException e){
// CHECKME there isn't much point in retrying here. We haven't connected to the
// server so if there is an error at this point it will probably happen everytime.
// However it still might be better to inform the user before just shutting down the
// application.
throw new ResourceLoadException("Error opening connection", this, e, false);
}
connection.setRequestProperty("Accept", "application/xml");
connection.setRequestProperty("Accept-Encoding", "gzip");
try{
connection.connect();
} catch (IOException e) {
failed(e, "Error connecting", false);
continue;
}
if (connection instanceof HttpURLConnection) {
httpConnection = (HttpURLConnection) connection;
try {
// check if this is a valid response
responseCode = httpConnection.getResponseCode();
} catch (IOException e) {
failed(e, "Error getting response code", true);
continue;
}
if((responseCode / 100) != 2) {
failed(null, "Non 2XX response code: " + responseCode, true);
continue;
}
}
try {
urlInputStream = connection.getInputStream();
} catch (IOException e) {
failed(e, "Error opening input stream", true);
continue;
}
String encoding = connection.getContentEncoding();
if(encoding != null && encoding.toLowerCase().equals("gzip")){
try {
urlInputStream = new GZIPInputStream(urlInputStream);
} catch (IOException e) {
failed(e, "Error ungzipping", true);
continue;
}
}
lastModified = connection.getLastModified();
logger.fine("RequiredResourceLoader - Done.");
return urlInputStream;
}
}
private void failed(IOException e, String message, boolean connectionOpen) throws ResourceLoadException
{
tryCount++;
logger.info("Failed attempt: " + message +
". Count: " + tryCount);
if (tryCount >= ATTEMPTS_BEFORE_PROMPTING && !userRequestsRetry(e, message)) {
throw new ResourceLoadException(message, this, e, connectionOpen);
}
}
private boolean userRequestsRetry(final IOException e, final String message) {
if (silentMode || !promptRetryQuit) { return false; }
String[] options = new String[]{"Retry", "Quit", "Details"};
final JOptionPane optionsPane = new JOptionPane("There was an error downloading one or more required resources.\nPlease ensure you are connected to the Internet and retry,\n or select quit and launch the project again.", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
final JDialog dialog = new JDialog((JFrame)null,
"Download Error",
true);
dialog.setContentPane(optionsPane);
optionsPane.addPropertyChangeListener(
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e1) {
String prop = e1.getPropertyName();
if (dialog.isVisible()
&& (e1.getSource() == optionsPane)
&& (prop.equals(JOptionPane.VALUE_PROPERTY))) {
if (((String)e1.getNewValue()).equalsIgnoreCase("Details")){
showErrorDetails(e);
} else {
dialog.setVisible(false);
}
}
}
private void showErrorDetails(IOException e)
{
JTextArea textArea = new JTextArea(5, 10);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
String details = "Error downloading resource from "+url+"\n\n";
details += "================\n\n";
details += "Error:\n";
details += message+"\n";
if (e != null){
details += e.getLocalizedMessage();
}
textArea.setText(details);
JScrollPane scroll = new JScrollPane(textArea);
textArea.setCaretPosition(0);
dialog.getContentPane().add(scroll, BorderLayout.SOUTH);
dialog.pack();
}
});
dialog.pack();
dialog.setVisible(true);
String choice = (String) optionsPane.getValue();
// int choice = JOptionPane.showOptionDialog(null, "There was an error downloading one or more required resources.\nPlease ensure you are connected to the Internet and retry,\n or select quit and launch the project again.", "Download Error", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
return choice.equalsIgnoreCase("Retry");
}
public long getLastModified() {
return lastModified;
}
/**
* Returns -1 if there is no reponse code available.
* @return
*/
public int getHttpResponseCode() {
return responseCode;
}
public void writeResourceErrorDetails(PrintWriter writer, boolean printBody)
{
HttpURLConnection httpConnection = null;
if (connection instanceof HttpURLConnection) {
httpConnection = (HttpURLConnection) connection;
}
if (httpConnection != null) {
try {
writer.println("Response code: " + httpConnection.getResponseCode());
} catch (IOException ioe){
// can't get the response code for some reason
}
}
if (urlInputStream != null) {
try {
writer.println("available bytes in input stream: " + urlInputStream.available());
} catch (IOException e1){
// can't get the available bytes for some reason.
}
}
Map<String, List<String>> headerFields = connection.getHeaderFields();
if(headerFields != null){
for (Entry<String, List<String>> entry : headerFields.entrySet()) {
writer.println(entry.getKey() + ": " + entry.getValue());
}
}
if(!printBody){
return;
}
writer.println("==== error body ====");
String encoding = connection.getContentEncoding();
if(httpConnection != null){
InputStream errorStream = httpConnection.getErrorStream();
if(errorStream != null){
try {
StreamUtil.writeFromStream(writer, errorStream, encoding);
return;
} catch (IOException e) {
writer.println("Exception getting error body (errorStream): " + e);
}
}
}
if(urlInputStream == null){
// Open the connection stream if it hasn't been opened
try {
urlInputStream = connection.getInputStream();
StreamUtil.writeFromStream(writer, urlInputStream, encoding);
} catch (Exception e) {
// If we are here the caller already know there is a problem
// and if the getInputStream method throws an exception then the
// getErrorStream method should have returned some reason for the problem
// in which case the body would be printed already.
// So if an exception is thrown here it is unexpected. Put that in the
// print out so we can attempt to track this down later
writer.println("Exception getting error body (inputStream): " + e);
}
}
}
public int getContentLength()
{
return contentLength;
}
}