package net.sf.fmj.media.protocol;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.MediaLocator;
import javax.media.Time;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.PullDataSource;
import javax.media.protocol.PullSourceStream;
import javax.media.protocol.SourceCloneable;
import com.lti.utils.PathUtils;
import net.sf.fmj.media.MimeManager;
import net.sf.fmj.utility.LoggerSingleton;
import net.sf.fmj.utility.URLUtils;
/**
* URL DataSource. Used by default when there is no more specific protocol handler available.
* @author Ken Larson
*
*/
public class URLDataSource extends PullDataSource implements SourceCloneable
{
private static final Logger logger = LoggerSingleton.logger;
protected URLConnection conn;
protected boolean connected = false;
private String contentTypeStr;
private ContentDescriptor contentType;
protected URLSourceStream[] sources;
protected URLDataSource()
{ super();
}
public URLDataSource(URL url)
{ setLocator(new MediaLocator(url));
}
public javax.media.protocol.DataSource createClone()
{
final URLDataSource d;
try
{
d = new URLDataSource(getLocator().getURL());
} catch (MalformedURLException e)
{
logger.log(Level.WARNING, "" + e, e);
return null; // according to the API, return null on failure.
}
if (connected)
{
try
{
d.connect();
}
catch (IOException e)
{
logger.log(Level.WARNING, "" + e, e);
return null; // according to the API, return null on failure.
}
}
return d;
}
@Override
public PullSourceStream[] getStreams()
{
if (!connected)
throw new Error("Unconnected source.");
return sources;
}
/** Strips trailing ; and anything after it. Is generally only used for multipart content. */
private String stripTrailer(String contentType)
{ final int index = contentType.indexOf(";");
if (index < 0)
return contentType;
final String result = contentType.substring(0, index);
return result;
}
@Override
public void connect() throws IOException
{
// we allow a re-connection even if we are connected, due to an oddity in the way Manager works. See comments there
// in createPlayer(MediaLocator sourceLocator).
// if (connected) // TODO: FMJ tends to call this twice. Check with JMF to see if that is normal.
// return;
URL url = getLocator().getURL();
// TODO:
// it seems that FMJ's use of relative file URLs may be nonstandard.
// see http://en.wikipedia.org/wiki/File_URI_scheme and the things it links to.
// workaround because URL.openConnection does not like relative file URLs.
if (url.getProtocol().equals("file"))
{ final String newUrlStr = URLUtils.createAbsoluteFileUrl(url.toExternalForm());
if (newUrlStr != null)
{
if (!newUrlStr.toString().equals(url.toExternalForm()))
{
logger.warning("Changing file URL to absolute for URL.openConnection, from " + url.toExternalForm() + " to " + newUrlStr);
url = new URL(newUrlStr);
}
}
}
conn = url.openConnection();
// TODO: why does Java do this with relative file URLs, like file://samplemedia/safexmas.mov
// it turns this into ftp://samplemedia/safexmas.mov
// very strange.
// typically sun.net.www.protocol.ftp.FtpURLConnection
// this shouldn't happen anymore because of the relative file URL transformation above.
if (!url.getProtocol().equals("ftp") && conn.getURL().getProtocol().equals("ftp"))
{ logger.warning("URL.openConnection() morphed " + url + " to " + conn.getURL());
throw new IOException("URL.openConnection() returned an FTP connection for a non-ftp url: " + url);
}
if (conn instanceof HttpURLConnection)
{ // TODO: this is probably why JMF has explicit HTTP and FTP data sources - so we can check things explicitly.
final HttpURLConnection huc = (HttpURLConnection) conn;
huc.connect();
final int code = huc.getResponseCode();
if (!(code >= 200 && code < 300))
{ huc.disconnect();
throw new IOException("HTTP response code: " + code);
}
// some web servers will give the wrong content type. It is hard to say whether we should just
// always ignore the web server's content type, and just check the extension, or whether we should
// only do it for clearly wrong types. For now, we'll just do it for clearly wrong types.
logger.finer("URL: " + url);
logger.finer("Response code: " + code);
logger.finer("Full content type: " + conn.getContentType());
boolean contentTypeSet = false;
if (stripTrailer(conn.getContentType()).equals("text/plain"))
{
// see if the URL has an extension, and guess based on that.
final String ext = PathUtils.extractExtension(url.getPath()); // this is a bit of a hack, PathUtils.extractExtension was designed for file paths, but it works here.
if (ext != null)
{
// TODO: what if ext is null?
final String result = MimeManager.getMimeType(ext);
if (result != null)
{ contentTypeStr = ContentDescriptor.mimeTypeToPackageName(result);
contentTypeSet = true;
logger.fine("Received content type " + conn.getContentType() + "; overriding based on extension, to: " + result);
}
}
}
// TODO: what is the right place to apply ContentDescriptor.mimeTypeToPackageName?
if (!contentTypeSet)
contentTypeStr = ContentDescriptor.mimeTypeToPackageName(stripTrailer(conn.getContentType()));
}
else
{
conn.connect();
// TODO: what is the right place to apply ContentDescriptor.mimeTypeToPackageName?
contentTypeStr = ContentDescriptor.mimeTypeToPackageName(conn.getContentType());
}
contentType = new ContentDescriptor(contentTypeStr);
sources = new URLSourceStream[1];
sources[0] = new URLSourceStream();
connected = true;
}
@Override
public String getContentType()
{ return contentTypeStr;
}
@Override
public void disconnect()
{ if (!connected)
return;
if (conn != null)
{
if (conn instanceof HttpURLConnection)
{
final HttpURLConnection huc = (HttpURLConnection) conn;
huc.disconnect();
}
// TODO: others
}
connected = false;
}
@Override
public void start() throws java.io.IOException
{
//throw new UnsupportedOperationException(); // TODO - what to do?
}
@Override
public void stop() throws java.io.IOException
{ //throw new UnsupportedOperationException(); // TODO - what to do?
}
@Override
public Time getDuration()
{ return Time.TIME_UNKNOWN; // TODO: any case where we know the duration?
}
@Override
public Object[] getControls()
{ return new Object[0];
}
@Override
public Object getControl(String controlName)
{ return null;
}
class URLSourceStream implements PullSourceStream
{
private boolean endOfStream = false;
public boolean endOfStream()
{
return endOfStream;
}
public ContentDescriptor getContentDescriptor()
{
return contentType;
}
public long getContentLength()
{
return conn.getContentLength(); // returns -1 if unknown, which is the same as LENGTH_UNKNOWN
}
public int read(byte[] buffer, int offset, int length) throws IOException
{
final int result = conn.getInputStream().read(buffer, offset, length); // TODO: does this handle the requirement of not returning 0 unless passed in 0?
if (result == -1) // end of stream
endOfStream = true;
return result;
}
public boolean willReadBlock()
{
try
{
return conn.getInputStream().available() <= 0;
} catch (IOException e)
{
return true;
}
}
public Object getControl(String controlType)
{
return null;
}
public Object[] getControls()
{
return new Object[0];
}
}
}