/**
* ThisTime 2.0 (2008-03-30)
* Copyright 2007 Zach Scrivena
* zachscrivena@gmail.com
* http://thistime.sourceforge.net/
*
* Simple clock and timer program.
*
* TERMS AND CONDITIONS:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package thistime;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Locale;
import java.util.Observable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Facilitate the downloading of remote resources.
*/
class WebGet
extends Observable
{
/** download buffer size in number of bytes (1 kb) */
private static final int BUFFER_SIZE = 1024;
/** has the download operation been cancelled? */
private volatile boolean cancelled = false;
/**
* Get the content of the specified source URL and put it into the given
* target object.
*
* @param sourceUrl
* URL of the source (e.g. webpage, remote file) to be downloaded
* @param sourceDescription
* String that describes the source (e.g. "webpage", "remote file")
* @param target
* Target object to be populated (e.g. StringBuilder, File)
* @return
* true if the content of the source URL is downloaded successfully;
* false otherwise
*/
synchronized boolean get(
final URL sourceUrl,
final String sourceDescription,
final Object target)
{
/* buffered reader for webpage */
BufferedReader br = null;
/* buffered input stream for remote file */
BufferedInputStream bis = null;
/* buffered output stream for local file */
BufferedOutputStream bos = null;
try
{
/* reset cancelled state */
cancelled = false;
/* check target type */
TargetType targetType;
if (target instanceof StringBuilder)
{
targetType = TargetType.STRINGBUILDER;
}
else if (target instanceof File)
{
targetType = TargetType.LOCAL_FILE;
}
else
{
throw new UnsupportedOperationException(
"Unsupported target type " + target.getClass());
}
/* open HTTP connection */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT,
"Opening HTTP connection to " + sourceDescription));
HttpURLConnection http = null;
try
{
http = (HttpURLConnection) sourceUrl.openConnection();
http.connect();
}
catch (Exception e)
{
setChanged();
notifyObservers(new Observation(
ObservationType.ERROR,
"Cannot open HTTP connection to " + sourceDescription));
return false;
}
if (cancelled)
{
return false;
}
/* get URL content length */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT,
"Getting size of " + sourceDescription));
/* get size of webpage in bytes; -1 if unknown */
final int size = http.getContentLength();
setChanged();
notifyObservers(new Observation(
ObservationType.SIZE,
size));
if (cancelled)
{
return false;
}
/* open input stream */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT,
"Opening input stream for " + sourceDescription));
try
{
switch (targetType)
{
case STRINGBUILDER:
/* open buffered reader for webpage */
final String contentType = http.getContentType().toLowerCase(Locale.ENGLISH);
/* look for charset, if specified */
String charset = null;
final Matcher m = Pattern.compile(".*charset[\\s]*=([^;]++).*").matcher(contentType);
if (m.find())
{
charset = m.group(1).trim();
}
if ((charset != null) && !charset.isEmpty())
{
try
{
br = new BufferedReader(new InputStreamReader(http.getInputStream(), charset));
}
catch (Exception e)
{
br = null;
}
}
if (br == null)
{
br = new BufferedReader(new InputStreamReader(http.getInputStream()));
}
break;
case LOCAL_FILE:
/* open input stream for remote file */
bis = new BufferedInputStream(http.getInputStream());
break;
}
}
catch (Exception e)
{
setChanged();
notifyObservers(new Observation(
ObservationType.ERROR,
"Cannot open input stream for " + sourceDescription));
return false;
}
if (targetType == TargetType.LOCAL_FILE)
{
/* open output stream for local file */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT,
"Opening output stream for local file"));
try
{
final File f = (File) target;
/* create parent directories, if necessary */
final File parent = f.getParentFile();
if ((parent != null) && !parent.exists())
parent.mkdirs();
bos = new BufferedOutputStream(new FileOutputStream(f));
}
catch (Exception e)
{
setChanged();
notifyObservers(new Observation(
ObservationType.ERROR,
"Cannot open output stream for local file"));
return false;
}
}
/* download content of source URL iteratively */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT,
"Downloading " + sourceDescription));
/* number of bytes downloaded so far */
int downloaded = 0;
try
{
switch (targetType)
{
case STRINGBUILDER:
final char[] charBuffer = new char[BUFFER_SIZE];
final StringBuilder sb = (StringBuilder) target;
while (true)
{
if (cancelled)
{
return false;
}
final int charCount = br.read(charBuffer, 0, BUFFER_SIZE);
/* check for end-of-stream */
if (charCount == -1)
break;
sb.append(charBuffer, 0, charCount);
downloaded += charCount; /* may not be accurate because byte != char */
if (size > 0)
{
int percent = (int) (100.0 * downloaded / size);
if (percent < 0)
{
percent = 0;
}
else if (percent > 100)
{
percent = 100;
}
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT_PERCENT,
new Object[] {"Downloading " + sourceDescription + ": " + percent + "%", percent}));
}
}
break;
case LOCAL_FILE:
final byte[] byteBuffer = new byte[BUFFER_SIZE];
while (true)
{
if (cancelled)
{
return false;
}
final int byteCount = bis.read(byteBuffer, 0, BUFFER_SIZE);
/* check for end-of-stream */
if (byteCount == -1)
break;
bos.write(byteBuffer, 0, byteCount);
downloaded += byteCount;
/* update progress bound property */
if (size > 0)
{
int percent = (int) (100.0 * downloaded / size);
if (percent < 0)
{
percent = 0;
}
else if (percent > 100)
{
percent = 100;
}
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT_PERCENT,
new Object[] {percent + "%", percent}));
}
}
break;
}
}
catch (Exception e)
{
setChanged();
notifyObservers(new Observation(
ObservationType.ERROR,
"Cannot download " + sourceDescription));
return false;
}
/* downloading completed */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT_PERCENT,
new Object[] {"Downloading " + sourceDescription, 100}));
if (cancelled)
{
return false;
}
/* close input stream */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT,
"Closing input stream for " + sourceDescription));
try
{
switch (targetType)
{
case STRINGBUILDER:
br.close();
br = null;
break;
case LOCAL_FILE:
bis.close();
bis = null;
break;
}
}
catch (Exception e)
{
setChanged();
notifyObservers(new Observation(
ObservationType.WARNING,
"Cannot close input stream for " + sourceDescription));
}
if (targetType == TargetType.LOCAL_FILE)
{
/* close output stream for local file */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT,
"Closing output stream for local file"));
try
{
bos.close();
bos = null;
}
catch (Exception e)
{
setChanged();
notifyObservers(new Observation(
ObservationType.WARNING,
"Cannot close output stream for local file"));
}
}
/* task completed successfully */
setChanged();
notifyObservers(new Observation(
ObservationType.PROGRESS_TEXT_PERCENT,
new Object[] {"Completed", 100}));
setChanged();
notifyObservers(new Observation(
ObservationType.COMPLETED,
null));
return true;
}
finally
{
/* close buffered reader for webpage */
if (br != null)
{
try
{
br.close();
}
catch (Exception e)
{
/* ignore */
}
}
/* close buffered input stream for remote file */
if (bis != null)
{
try
{
bis.close();
}
catch (Exception e)
{
/* ignore */
}
}
/* close buffered output stream for local file */
if (bos != null)
{
try
{
bos.close();
}
catch (Exception e)
{
/* ignore */
}
}
}
}
/**
* Cancel ongoing operation.
*/
public void cancel()
{
cancelled = true;
}
/*****************
* INNER CLASSES *
*****************/
/**
* Represent an observation.
*/
static class Observation
{
/** observation type */
public ObservationType observationType;
/** value of the observation */
public Object value;
/**
* Constructor.
*
* @param observationType
* Observation type
* @param value
* Value of the observation
*/
Observation(
final ObservationType observationType,
final Object value)
{
this.observationType = observationType;
this.value = value;
}
}
/**
* Observation types.
*/
static enum ObservationType
{
/** progress update with text only */
PROGRESS_TEXT,
/** progress update with text and percentage */
PROGRESS_TEXT_PERCENT,
/** operation completed */
COMPLETED,
/** obtained size of remote resource */
SIZE,
/** fatal error has occurred; operation aborted */
ERROR,
/** warning issued; can still proceed with operation */
WARNING
};
/**
* Target types.
*/
static enum TargetType
{
/** StringBuilder object */
STRINGBUILDER,
/** local file */
LOCAL_FILE
};
}