// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: ResumingSupport.java,v 1.19 2007/04/13 12:56:43 spyromus Exp $
//
package com.salas.bb.utils.net;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Resuming support module. Returns connection to a given URL. When <code>resume()</code>
* method is called the module tries to use protocol-specific means to reach the given
* position in stream faster. If resuming isn't supported (by local file system or old
* HTTP servers) then it makes a try to read out everything by that position.
* <p/>
* Errors handling isn't performed in this module. So, if there will be an error during
* connection or skipping of already read content the error will be passed to the caller.
*/
public final class ResumingSupport
{
private static final Logger LOG = Logger.getLogger(ResumingSupport.class.getName());
/**
* It may come that we have a cyclic redirections happening. We need some place to
* record all redirections we had in current connection session to break the loop
* once we detect that we are being redirected to the place we have already been to.
* This thread local object will contain the list of URL's we were trying to connect
* (as <code>String</code>'s). The first URL to be put there is our initial request URL.
*/
private static ThreadLocal visitedURLs = new ThreadLocal();
/**
* Hidden utility class constructor.
*/
private ResumingSupport()
{
}
/**
* Connects to a given URL.
*
* @param url URL to connect to.
*
* @return connection object.
*
* @throws IOException in case of any IO error.
*/
public static URLConnectionHolder connect(URL url)
throws IOException
{
return resume(url, 0);
}
/**
* Connects to a given URL at specified position.
*
* @param url URL to connecto.
* @param position position in the stream to start reading from.
*
* @return connection object.
*
* @throws IOException in case of any IO error.
*/
public static URLConnectionHolder resume(URL url, long position)
throws IOException
{
return resume(url, position, -1);
}
/**
* Connects to a given URL at specified position.
*
* @param url URL to connecto.
* @param position position in the stream to start reading from.
* @param lastFetchTime time of last successful fetching.
*
* @return connection object.
*
* @throws IOException in case of any IO error.
*/
public static URLConnectionHolder resume(URL url, long position, long lastFetchTime)
throws IOException
{
return resume(url, position, lastFetchTime, null, null, null);
}
/**
* Connects to a given URL at specified position.
*
* @param url URL to connecto.
* @param position position in the stream to start reading from.
* @param lastFetchTime time of last successful fetching.
* @param userAgent HTTP user agent.
* @param username Basic Authentication user name.
* @param password Basic Authenticaiton user password.
*
* @return connection object.
*
* @throws IOException in case of any IO error.
*/
public static URLConnectionHolder resume(URL url, long position, long lastFetchTime, String userAgent,
String username, String password)
throws IOException
{
if (url == null) throw new IllegalArgumentException(Strings.error("unspecified.url"));
clearVisitedURLs();
URLConnectionHolder holder;
if (url.getProtocol().equalsIgnoreCase("file"))
{
holder = fileResume(url, position, lastFetchTime);
} else
{
holder = remoteResume(url, position, lastFetchTime, userAgent, username, password);
}
return holder;
}
private static URLConnectionHolder remoteResume(URL url, long position, long lastFetchTime, String userAgent,
String username, String password)
throws IOException
{
URLConnection con = url.openConnection();
URLConnectionHolder holder = new URLConnectionHolder(con, null);
return remoteResume(holder, position, lastFetchTime, userAgent, username, password);
}
private static URLConnectionHolder remoteResume(URLConnectionHolder aHolder, long position,
long lastFetchTime, String userAgent, String username,
String password) throws IOException
{
URL url = aHolder.getConnection().getURL();
// Verify that we haven't already been at this location and register it for further checks
if (isURLVisited(url)) throw new CyclicRedirectionException(getVisitedURLsList());
registerVisitedURL(url);
if (aHolder.getConnection() instanceof HttpURLConnection)
{
return httpResume(aHolder, position, lastFetchTime, userAgent, username, password);
} else
{
return otherResume(aHolder, position);
}
}
private static URLConnectionHolder httpResume(URLConnectionHolder holder, long aPosition,
long aLastFetchTime, String userAgent, String username,
String password) throws IOException, CyclicRedirectionException
{
HttpURLConnection httpCon = (HttpURLConnection)holder.getConnection();
if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password))
{
httpCon.setRequestProperty("Authorization", StringUtils.createBasicAuthToken(username, password));
}
httpCon.setInstanceFollowRedirects(false);
httpCon.setAllowUserInteraction(false);
httpCon.setRequestProperty("Accept-Encoding", "gzip");
if (userAgent == null) userAgent = System.getProperty("http.agent");
if (userAgent != null) httpCon.setRequestProperty("User-Agent", userAgent);
if (aPosition > 0)
{
httpCon.setRequestProperty("Range", "bytes=" + Long.toString(aPosition) + "-");
}
if (aLastFetchTime > 0) httpCon.setIfModifiedSince(aLastFetchTime);
httpCon.connect();
int responseCode = httpCon.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE ||
responseCode == HttpURLConnection.HTTP_MOVED_PERM ||
responseCode == HttpURLConnection.HTTP_MOVED_TEMP ||
responseCode == HttpURLConnection.HTTP_SEE_OTHER ||
responseCode == HttpURLConnection.HTTP_USE_PROXY ||
responseCode == 307)
{
// Redirected
boolean perm = responseCode == HttpURLConnection.HTTP_MOVED_PERM;
String location = httpCon.getHeaderField("Location");
if (location == null) throw new IOException(
MessageFormat.format(Strings.error("unspecified.redirection.location.0"),
new Object[] { httpCon.getURL() }));
URL newURL = new URL(httpCon.getURL(), location);
httpCon.disconnect();
// Log the redirection
if (LOG.isLoggable(Level.FINE))
{
String newLoc = newURL.toString();
String oldLoc = httpCon.getURL().toString();
LOG.fine("Redirected " + oldLoc + (perm ? " (permanently)" : "") +
" to: " + newLoc);
}
// Make another loop with new URL
holder = new URLConnectionHolder(newURL.openConnection(),
perm ? newURL : holder.getPermanentRedirectionURL());
holder = remoteResume(holder, aPosition, aLastFetchTime, userAgent, username, password);
} else if (responseCode == HttpURLConnection.HTTP_OK)
{
skipToPosition(httpCon, aPosition);
}
return holder;
}
private static URLConnectionHolder otherResume(URLConnectionHolder aHolder, long aPosition)
throws IOException
{
URLConnection con = aHolder.getConnection();
con.connect();
skipToPosition(con, aPosition);
return aHolder;
}
/**
* Performs (re)connection to local resource.
*
* @param url URL to explore.
* @param position position in stream.
* @param lastFetchTime time of last successful fetching.
*
* @return holder for connection data.
*
* @throws IOException in case of any I/O error.
*/
private static URLConnectionHolder fileResume(URL url, long position, long lastFetchTime)
throws IOException
{
URLConnection con = url.openConnection();
// For some reason JRE skipps this check
if (lastFetchTime > 0) con.setIfModifiedSince(lastFetchTime);
con.connect();
skipToPosition(con, position);
return new URLConnectionHolder(con, null);
}
/**
* Skips first bytes up to specified position.
*
* @param con connection.
* @param position position.
*
* @throws IOException in case of any I/O error.
*/
private static void skipToPosition(URLConnection con, long position)
throws IOException
{
if (position <= 0) return;
InputStream in = con.getInputStream();
while (position > 0)
{
long skipped = in.skip(position);
position = (skipped == 0) ? 0 : position - skipped;
}
}
/**
* Clears the list of visited URLs.
*/
private static void clearVisitedURLs()
{
getVisitedURLsList().clear();
}
/**
* Registers visited URL.
*
* @param url visited URL.
*/
private static void registerVisitedURL(URL url)
{
if (url != null)
{
String urlString = url.toString();
List listOfVisitedURLs = getVisitedURLsList();
if (!listOfVisitedURLs.contains(urlString)) listOfVisitedURLs.add(urlString);
}
}
/**
* Returns <code>TRUE</code> if the URL was visited before.
*
* @param url URL to check.
*
* @return <code>TRUE</code> if the URL was visited before.
*/
private static boolean isURLVisited(URL url)
{
String urlString = url.toString();
List listOfVisitedURLs = getVisitedURLsList();
return listOfVisitedURLs.contains(urlString);
}
/**
* Returns the list of visited URL's in current session.
*
* @return list of visited URL's.
*/
private static List getVisitedURLsList()
{
List listOfVisitedURLs;
synchronized (visitedURLs)
{
if (visitedURLs.get() == null) visitedURLs.set(new ArrayList());
listOfVisitedURLs = (ArrayList)visitedURLs.get();
}
return listOfVisitedURLs;
}
}