package com.revolsys.spring.resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import com.revolsys.io.FileUtil;
import com.revolsys.util.Base64;
import com.revolsys.util.Exceptions;
import com.revolsys.util.Property;
import com.revolsys.util.UrlUtil;
public class UrlResource extends AbstractResource {
/**
* Cleaned URL (with normalized path), used for comparisons.
*/
private final URL cleanedUrl;
/**
* Original URI, if available; used for URI and File access.
*/
private final URI uri;
/**
* Original URL, used for actual access.
*/
private URL url;
private String username;
private String password;
/**
* Construct a new new UrlResource based on a URL path.
* <p>Note: The given path needs to be pre-encoded if necessary.
* @param path a URL path
* @throws MalformedURLException if the given URL path is not valid
* @see java.net.URL#URL(String)
*/
public UrlResource(final CharSequence path) {
Assert.notNull(path, "Path must not be null");
try {
this.uri = null;
final String urlString = path.toString();
initUrl(new URL(urlString));
this.cleanedUrl = getCleanedUrl(this.url, urlString);
} catch (final Throwable ex) {
throw Exceptions.wrap(ex);
}
}
public UrlResource(final CharSequence path, final String username, final String password) {
this(path);
this.username = username;
this.password = password;
}
/**
* Construct a new new UrlResource based on the given URI object.
* @param uri a URI
* @throws MalformedURLException if the given URL path is not valid
*/
public UrlResource(final URI uri) {
Assert.notNull(uri, "URI must not be null");
try {
this.uri = uri;
initUrl(uri.toURL());
this.cleanedUrl = getCleanedUrl(this.url, uri.toString());
} catch (final Throwable ex) {
throw Exceptions.wrap(ex);
}
}
/**
* Construct a new new UrlResource based on the given URL object.
* @param url a URL
*/
public UrlResource(final URL url) {
Assert.notNull(url, "URL must not be null");
initUrl(url);
this.cleanedUrl = getCleanedUrl(this.url, url.toString());
this.uri = null;
}
public UrlResource(final URL url, final String username, final String password) {
this(url);
this.username = username;
this.password = password;
}
@Override
public long contentLength() throws IOException {
final URL url = getURL();
if (ResourceUtils.isFileURL(url)) {
// Proceed with file system resolution...
return getFile().length();
} else {
// Try a URL connection content-length header...
final URLConnection con = url.openConnection();
customizeConnection(con);
return con.getContentLength();
}
}
/**
* This implementation creates a UrlResource, applying the given path
* relative to the path of the underlying URL of this resource descriptor.
* @see java.net.URL#URL(java.net.URL, String)
*/
@Override
public UrlResource createRelative(String relativePath) {
try {
if (relativePath.startsWith("/")) {
relativePath = relativePath.substring(1);
}
final URL url = getURL();
final URL relativeUrl = UrlUtil.getUrl(url, relativePath);
final UrlResource relativeResource = new UrlResource(relativeUrl.toString(), this.username,
this.password);
return relativeResource;
} catch (final Throwable e) {
throw new IllegalArgumentException(
"Unable to create relative URL " + this + " " + relativePath, e);
}
}
/**
* Customize the given {@link HttpURLConnection}, obtained in the course of an
* {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call.
* <p>Sets request method "HEAD" by default. Can be overridden in subclasses.
* @param con the HttpURLConnection to customize
* @throws IOException if thrown from HttpURLConnection methods
*/
protected void customizeConnection(final HttpURLConnection con) throws IOException {
con.setRequestMethod("HEAD");
}
/**
* Customize the given {@link URLConnection}, obtained in the course of an
* {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call.
* <p>Calls {@link ResourceUtils#useCachesIfNecessary(URLConnection)} and
* delegates to {@link #customizeConnection(HttpURLConnection)} if possible.
* Can be overridden in subclasses.
* @param con the URLConnection to customize
* @throws IOException if thrown from URLConnection methods
*/
protected void customizeConnection(final URLConnection con) throws IOException {
if (con instanceof HttpURLConnection) {
customizeConnection((HttpURLConnection)con);
}
}
/**
* This implementation compares the underlying URL references.
*/
@Override
public boolean equals(final Object obj) {
return obj == this
|| obj instanceof UrlResource && this.cleanedUrl.equals(((UrlResource)obj).cleanedUrl);
}
@Override
public boolean exists() {
if (isFolderConnection()) {
try {
final File file = getFile();
if (file == null) {
return false;
} else {
return file.exists();
}
} catch (final Throwable e) {
return false;
}
} else {
try {
final URL url = getURL();
if (ResourceUtils.isFileURL(url)) {
// Proceed with file system resolution...
return getFile().exists();
} else {
// Try a URL connection content-length header...
final URLConnection con = url.openConnection();
customizeConnection(con);
final HttpURLConnection httpCon = con instanceof HttpURLConnection
? (HttpURLConnection)con : null;
if (httpCon != null) {
final int code = httpCon.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
return true;
} else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
}
if (con.getContentLength() >= 0) {
return true;
}
if (httpCon != null) {
// no HTTP OK status, and no content-length header: give up
httpCon.disconnect();
return false;
} else {
// Fall back to stream existence: can we open the stream?
try (
final InputStream is = getInputStream()) {
} catch (final Throwable e) {
return false;
}
return true;
}
}
} catch (final IOException ex) {
return false;
}
}
}
/**
* Determine a cleaned URL for the given original URL.
* @param originalUrl the original URL
* @param originalPath the original URL path
* @return the cleaned URL
* @see org.springframework.util.StringUtils#cleanPath
*/
private URL getCleanedUrl(final URL originalUrl, final String originalPath) {
try {
return new URL(StringUtils.cleanPath(originalPath));
} catch (final MalformedURLException ex) {
// Cleaned URL path cannot be converted to URL
// -> take original URL.
return originalUrl;
}
}
/**
* This implementation returns a description that includes the URL.
*/
@Override
public String getDescription() {
return this.url.toString();
}
/**
* This implementation returns a File reference for the underlying URL/URI,
* provided that it refers to a file in the file system.
* @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String)
*/
@Override
public File getFile() {
try {
final URL url = getURL();
if (isFolderConnection()) {
return FileUtil.getFile(url);
} else {
if (!"file".equals(url.getProtocol())) {
throw new FileNotFoundException(getDescription() + " is not a file URL: " + url);
}
try {
final String filePath = ResourceUtils.toURI(url).getSchemeSpecificPart();
final int queryIndex = filePath.indexOf('?');
if (queryIndex == -1) {
return new File(filePath);
} else {
final String filePart = filePath.substring(0, queryIndex);
return new File(filePart);
}
} catch (final URISyntaxException ex) {
// Fallback for URLs that are not valid URIs (should hardly ever
// happen).
return new File(url.getFile());
}
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* This implementation returns a File reference for the underlying class path
* resource, provided that it refers to a file in the file system.
* @see org.springframework.util.ResourceUtils#getFile(java.net.URI, String)
*/
protected File getFile(final URI uri) throws IOException {
return ResourceUtils.getFile(uri, getDescription());
}
/**
* This implementation determines the underlying File
* (or jar file, in case of a resource in a jar/zip).
*/
@Override
protected File getFileForLastModifiedCheck() throws IOException {
final URL url = getURL();
if (ResourceUtils.isJarURL(url)) {
final URL actualUrl = ResourceUtils.extractJarFileURL(url);
return ResourceUtils.getFile(actualUrl, "Jar URL");
} else {
return getFile();
}
}
/**
* This implementation returns the name of the file that this URL refers to.
*/
@Override
public String getFilename() {
return UrlUtil.getFileName(this.url);
}
/**
* This implementation opens an InputStream for the given URL.
* It sets the "UseCaches" flag to {@code false},
* mainly to avoid jar file locking on Windows.
* @see java.net.URL#openConnection()
* @see java.net.URLConnection#setUseCaches(boolean)
* @see java.net.URLConnection#getInputStream()
*/
@Override
public InputStream getInputStream() {
try {
if (isFolderConnection()) {
final File file = getFile();
return new FileInputStream(file);
} else {
final URLConnection con = this.url.openConnection();
con.addRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110613 Firefox/6.0a2");
if (con instanceof HttpURLConnection) {
final HttpURLConnection httpUrlConnection = (HttpURLConnection)con;
setAuthorization(this.url, httpUrlConnection);
}
// ResourceUtils.useCachesIfNecessary(con);
try {
return con.getInputStream();
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Error opening file: " + toString(), e);
} catch (final IOException e) {
// Close the HTTP connection (if applicable).
if (con instanceof HttpURLConnection) {
final HttpURLConnection httpUrlConnection = (HttpURLConnection)con;
httpUrlConnection.disconnect();
}
throw Exceptions.wrap(e);
}
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
@Override
public Resource getParent() {
final URL url = getURL();
final String parentUrl = UrlUtil.getParentString(url);
return new UrlResource(parentUrl, this.username, this.password);
}
public String getPassword() {
return this.password;
}
/**
* This implementation returns the underlying URI directly,
* if possible.
*/
@Override
public URI getURI() throws IOException {
if (this.uri != null) {
return this.uri;
} else {
return super.getURI();
}
}
/**
* This implementation returns the underlying URL reference.
*/
@Override
public URL getURL() {
return this.url;
}
public String getUsername() {
return this.username;
}
/**
* This implementation returns the hash code of the underlying URL reference.
*/
@Override
public int hashCode() {
return this.cleanedUrl.hashCode();
}
protected void initUrl(final URL url) {
this.url = url;
final String userInfo = url.getUserInfo();
if (userInfo != null) {
final int colonIndex = userInfo.indexOf(':');
if (colonIndex == -1) {
this.username = userInfo;
} else {
this.username = userInfo.substring(0, colonIndex);
this.password = userInfo.substring(colonIndex + 1);
}
}
}
public boolean isFolderConnection() {
final URL url = getURL();
final String protocol = url.getProtocol();
return "folderConnection".equalsIgnoreCase(protocol);
}
@Override
public boolean isReadable() {
try {
final URL url = getURL();
if (ResourceUtils.isFileURL(url)) {
// Proceed with file system resolution...
final File file = getFile();
return file.canRead() && !file.isDirectory();
} else {
return true;
}
} catch (final Throwable ex) {
return false;
}
}
@Override
public long lastModified() throws IOException {
final URL url = getURL();
if (ResourceUtils.isFileURL(url) || ResourceUtils.isJarURL(url)) {
// Proceed with file system resolution...
return super.lastModified();
} else {
// Try a URL connection last-modified header...
final URLConnection con = url.openConnection();
customizeConnection(con);
return con.getLastModified();
}
}
@Override
public UrlResource newChildResource(final CharSequence childPath) {
return createRelative(childPath.toString());
}
public UrlResource newUrlResource(final Map<String, ? extends Object> parameters) {
final URL url = getURL();
final String urlString = UrlUtil.getUrl(url, parameters);
return new UrlResource(urlString, this.username, this.password);
}
public UrlResource newUrlResourceAuthorization(final String username, final String password) {
return new UrlResource(getURL(), username, password);
}
private void setAuthorization(final URL url, final HttpURLConnection connection) {
if (Property.hasValue(this.username)) {
final String userpass;
if (this.password == null) {
userpass = this.username + ":";
} else {
userpass = this.username + ":" + this.password;
}
final String basicAuth = "Basic " + new String(Base64.encode(userpass));
connection.setRequestProperty("Authorization", basicAuth);
} else {
final String userInfo = url.getUserInfo();
if (userInfo != null) {
connection.setRequestProperty("Authorization", "Basic " + Base64.encode(userInfo));
}
}
}
}