package com.limegroup.gnutella.browser;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.limewire.io.GUID;
import org.limewire.util.FileUtils;
import org.limewire.util.URIUtils;
import com.limegroup.gnutella.FileDetails;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.util.EncodingUtils;
import com.limegroup.gnutella.util.URLDecoder;
/**
* Contains information fields extracted from a magnet link.
*/
public class MagnetOptions implements Serializable {
private static final long serialVersionUID = 5612757489102667276L;
public static final String MAGNET = "magnet:?";
private static final String HTTP = "http://";
/**
* The string to prefix download files with in the rare case that we don't
* have a download name and can't calculate one from the URN.
*/
private static final String DOWNLOAD_PREFIX = "MAGNET download from ";
private final Map<Option, List<String>> optionsMap;
private enum Option {
/** eXact Source of the magnet, can be a url or a urn. */
XS,
/**
* eXact Topic of the magnet, should be urn that uniquely identifies the
* resource, e.g. a sha1 urn.
*/
XT,
/** Alternate Source of the magnet, a url or a urn. */
AS,
/** Display Name of the file. */
DN,
/**
* Keyword Topic, a short description or name for the file, can be used
* for searching.
*/
KT,
/** Length of the file denoted by the magnet. */
XL;
public static Option valueFor(String str) {
for (Option option : values()) {
if (str.toUpperCase(Locale.US).startsWith(option.toString()))
return option;
}
return null;
}
}
private transient String[] defaultURLs;
private transient String localizedErrorMessage;
private transient URN urn;
private transient String extractedFileName;
private transient Set<URN> guidUrns;
/**
* Cached field of file size of magnet, values meant the following:
*
* <pre>
* -1 - magnet doesn't have a size field
* -2 - magnet has not been parsed for size field yet
* >= 0 - size of magnet
* </pre>
*
* Volatile, to have thread safe assignments:
* <p>
* http://java.sun.com/docs/books/jvms/second_edition/html/Threads.doc.html#
* 22244
*/
private transient volatile long fileSize = -2;
/**
* Creates a MagnetOptions object from file details.
* <p>
* The resulting MagnetOptions might not be {@link #isDownloadable()
* downloadable}.
*/
public static MagnetOptions createMagnet(FileDetails fileDetails,
InetSocketAddress socketAddress, byte[] clientGuid) {
Map<Option, List<String>> map = new EnumMap<Option, List<String>>(Option.class);
map.put(Option.DN, Collections.singletonList(fileDetails.getFileName()));
URN urn = fileDetails.getSHA1Urn();
if (urn != null) {
addAppend(map, Option.XT, urn.httpStringValue());
}
String url = null;
if (socketAddress != null && urn != null) {
StringBuilder addr = new StringBuilder("http://");
addr.append(socketAddress.getAddress().getHostAddress()).append(':').append(
socketAddress.getPort()).append("/uri-res/N2R?");
addr.append(urn.httpStringValue());
url = addr.toString();
addAppend(map, Option.XS, url);
}
URN guidUrn = null;
if (clientGuid != null) {
guidUrn = URN.createGUIDUrn(new GUID(clientGuid));
addAppend(map, Option.XS, guidUrn.httpStringValue());
}
long fileSize = fileDetails.getSize();
if (fileSize >= 0) {
addAppend(map, Option.XL, Long.toString(fileSize));
}
MagnetOptions magnet = new MagnetOptions(map);
// set already known values
magnet.urn = urn;
if (url != null) {
magnet.defaultURLs = new String[] { url };
}
if (guidUrn != null) {
magnet.guidUrns = Collections.singleton(guidUrn);
}
magnet.fileSize = fileSize >= 0 ? fileSize : -1;
return magnet;
}
/**
* Creates a MagnetOptions object from a several parameters.
* <p>
* The resulting MagnetOptions might not be {@link #isDownloadable()
* downloadable}.
*
* @param keywordTopics can be <code>null</code>
* @param fileName can be <code>null</code>
* @param urn can be <code>null</code>
* @param defaultURLs can be <code>null</code>
*/
public static MagnetOptions createMagnet(String keywordTopics, String fileName, URN urn,
String[] defaultURLs) {
return createMagnet(keywordTopics, fileName, urn, defaultURLs, null);
}
public static MagnetOptions createMagnet(String keywordTopics, String fileName, URN urn,
String[] defaultURLs, Set<? extends URN> guidUrns) {
Map<Option, List<String>> map = new HashMap<Option, List<String>>();
List<String> kt = new ArrayList<String>(1);
kt.add(keywordTopics);
map.put(Option.KT, kt);
List<String> dn = new ArrayList<String>(1);
dn.add(fileName);
map.put(Option.DN, dn);
if (urn != null) {
addAppend(map, Option.XT, urn.httpStringValue());
}
if (defaultURLs != null) {
for (int i = 0; i < defaultURLs.length; i++) {
addAppend(map, Option.AS, defaultURLs[i]);
}
}
if (guidUrns != null) {
for (URN guidUrn : guidUrns) {
if (!guidUrn.isGUID()) {
throw new IllegalArgumentException("Not a GUID urn: " + guidUrn);
}
addAppend(map, Option.XS, guidUrn.httpStringValue());
}
}
MagnetOptions magnet = new MagnetOptions(map);
magnet.urn = urn;
if (defaultURLs != null) {
// copy array to protect against outside changes
magnet.defaultURLs = new String[defaultURLs.length];
System.arraycopy(defaultURLs, 0, magnet.defaultURLs, 0, magnet.defaultURLs.length);
} else {
magnet.defaultURLs = new String[0];
}
if (guidUrns != null) {
magnet.guidUrns = Collections.unmodifiableSet(new HashSet<URN>(guidUrns));
}
return magnet;
}
/**
* Allows multi line parsing of magnet links.
*
* @return array may be empty, but is never <code>null</code>
*/
public static MagnetOptions[] parseMagnets(String magnets) {
List<MagnetOptions> list = new ArrayList<MagnetOptions>();
StringTokenizer tokens = new StringTokenizer(magnets, System.getProperty("line.separator"));
while (tokens.hasMoreTokens()) {
String next = tokens.nextToken();
MagnetOptions[] options = MagnetOptions.parseMagnet(next);
if (options.length > 0) {
list.addAll(Arrays.asList(options));
}
}
return list.toArray(new MagnetOptions[0]);
}
/**
* Returns an empty array if the string could not be parsed.
*
* @param arg a string like
* "magnet:?xt.1=urn:sha1:49584DFD03&xt.2=urn:sha1:495345k"
* @return array may be empty, but is never <code>null</code>
*/
public static MagnetOptions[] parseMagnet(String arg) {
Map<Integer, Map<Option, List<String>>> options = new HashMap<Integer, Map<Option, List<String>>>();
// Strip out any single quotes added to escape the string
if (arg.startsWith("'"))
arg = arg.substring(1);
if (arg.endsWith("'"))
arg = arg.substring(0, arg.length() - 1);
// Parse query
if (!arg.toLowerCase(Locale.US).startsWith(MagnetOptions.MAGNET))
return new MagnetOptions[0];
// Parse and assemble magnet options together.
//
arg = arg.substring(8);
StringTokenizer st = new StringTokenizer(arg, "&");
String keystr;
String cmdstr;
int start;
int index;
Integer iIndex;
int periodLoc;
// Process each key=value pair
while (st.hasMoreTokens()) {
Map<Option, List<String>> curOptions;
keystr = st.nextToken();
keystr = keystr.trim();
start = keystr.indexOf("=") + 1;
if (start == 0)
continue; // no '=', ignore.
cmdstr = keystr.substring(start);
keystr = keystr.substring(0, start - 1);
try {
cmdstr = URLDecoder.decode(cmdstr);
} catch (IOException e1) {
continue;
}
// Process any numerical list of cmds
if ((periodLoc = keystr.indexOf(".")) > 0) {
try {
index = Integer.parseInt(keystr.substring(periodLoc + 1));
} catch (NumberFormatException e) {
continue;
}
} else {
index = 0;
}
// Add to any existing options
iIndex = new Integer(index);
curOptions = options.get(iIndex);
if (curOptions == null) {
curOptions = new HashMap<Option, List<String>>();
options.put(iIndex, curOptions);
}
Option option = Option.valueFor(keystr);
if (option != null)
addAppend(curOptions, option, cmdstr);
}
MagnetOptions[] ret = new MagnetOptions[options.size()];
int i = 0;
for (Map<Option, List<String>> current : options.values())
ret[i++] = new MagnetOptions(current);
return ret;
}
private static void addAppend(Map<Option, List<String>> map, Option key, String value) {
List<String> l = map.get(key);
if (l == null) {
l = new ArrayList<String>(1);
map.put(key, l);
}
l.add(value);
}
private MagnetOptions(Map<Option, List<String>> options) {
optionsMap = Collections.unmodifiableMap(options);
}
@Override
public String toString() {
return toExternalForm();
}
/**
* Returns the magnet uri representation as it can be used in an html link.
* <p>
* Display name and keyword topic are url encoded.
*
*/
public String toExternalForm() {
StringBuilder ret = new StringBuilder(MAGNET);
for (String xt : getExactTopics())
ret.append("&xt=").append(xt);
if (getDisplayName() != null)
ret.append("&dn=").append(EncodingUtils.encode(getDisplayName()));
if (getKeywordTopic() != null)
ret.append("&kt=").append(EncodingUtils.encode(getKeywordTopic()));
for (String xs : getXS())
ret.append("&xs=").append(xs);
for (String as : getAS())
ret.append("&as=").append(as);
for (String xl : getXL())
ret.append("&xl=").append(xl);
return ret.toString();
}
/**
* Returns the sha1 urn of this magnet uri if it has one.
* <p>
* It looks in the exactly topics, the exact sources and then in the
* alternate sources for it.
*/
public URN getSHA1Urn() {
if (urn == null) {
urn = extractSHA1URNFromList(getExactTopics());
if (urn == null)
urn = extractSHA1URNFromList(getXS());
if (urn == null)
urn = extractSHA1URNFromList(getAS());
if (urn == null)
urn = extractSHA1URNFromURLS(getDefaultURLs());
if (urn == null)
urn = URN.INVALID;
}
if (urn == URN.INVALID)
return null;
return urn;
}
private URN extractSHA1URNFromURLS(String[] defaultURLs) {
for (int i = 0; i < defaultURLs.length; i++) {
try {
URI uri = URIUtils.toURI(defaultURLs[i]);
String query = uri.getQuery();
if (query != null) {
return URN.createSHA1Urn(uri.getQuery());
}
} catch (URISyntaxException e) {
} catch (IOException e) {
}
}
return null;
}
/**
* Returns true if there are enough pieces of information to start a
* download from it.
* <p>
* At any rate there has to be at least one default url or a sha1 and a non
* empty keyword topic/display name.
*/
public boolean isDownloadable() {
if (getDefaultURLs().length > 0) {
return true;
}
if (getSHA1Urn() != null) {
if (getQueryString() != null) {
return true;
}
}
return false;
}
/**
* Returns whether the magnet has no other fields set than the hash.
* <p>
* If this is the case the user has to kick of a search manually.
*/
public boolean isHashOnly() {
String kt = getKeywordTopic();
String dn = getDisplayName();
return (kt == null || kt.length() > 0) && (dn == null || dn.length() > 0)
&& getAS().isEmpty() && getXS().isEmpty() && !getExactTopics().isEmpty();
}
/**
* Returns a query string or <code>null</code> if there is none.
*/
public String getQueryString() {
String kt = getKeywordTopic();
if (kt != null && kt.length() > 0) {
return kt;
}
String dn = getDisplayName();
if (dn != null && dn.length() > 0) {
return dn;
}
return null;
}
/**
* Returns true if only the keyword topic is specified.
*/
public boolean isKeywordTopicOnly() {
String kt = getKeywordTopic();
String dn = getDisplayName();
return kt != null && kt.length() > 0 && (dn == null || dn.length() > 0)
&& getAS().isEmpty() && getXS().isEmpty() && getExactTopics().isEmpty();
}
private URN extractSHA1URNFromList(List<String> strings) {
for (String str : strings) {
try {
return URN.createSHA1Urn(str);
} catch (IOException ignored) {
}
}
return null;
}
private List<String> getPotentialURLs() {
List<String> urls = new ArrayList<String>();
urls.addAll(getPotentialURLs(getExactTopics()));
urls.addAll(getPotentialURLs(getXS()));
urls.addAll(getPotentialURLs(getAS()));
return urls;
}
private List<String> getPotentialURNs() {
List<String> urls = new ArrayList<String>();
urls.addAll(getPotentialURNs(getExactTopics()));
urls.addAll(getPotentialURNs(getXS()));
urls.addAll(getPotentialURNs(getAS()));
return urls;
}
private List<String> getPotentialURNs(List<String> strings) {
List<String> ret = new ArrayList<String>();
for (String str : strings) {
if (str.toLowerCase(Locale.US).startsWith(URN.Type.URN_NAMESPACE_ID))
ret.add(str);
}
return ret;
}
private List<String> getPotentialURLs(List<String> strings) {
List<String> ret = new ArrayList<String>();
for (String str : strings) {
if (str.toLowerCase(Locale.US).startsWith(HTTP))
ret.add(str);
}
return ret;
}
/**
* Returns all valid urls that can be tried for downloading.
*/
public String[] getDefaultURLs() {
if (defaultURLs == null) {
List<String> urls = getPotentialURLs();
for (Iterator<String> it = urls.iterator(); it.hasNext();) {
try {
String nextURL = it.next();
URIUtils.toURI(nextURL); // is it a valid URI?
} catch (URISyntaxException e) {
it.remove(); // if not, remove it from the list.
localizedErrorMessage = e.getLocalizedMessage();
}
}
defaultURLs = urls.toArray(new String[urls.size()]);
}
return defaultURLs;
}
/**
* Returns immutable set of all valid GUID urns than can be tried for
* downloading.
* <p>
* GUID urns denote possibly firewalled hosts in the network that can be
* looked up as possible download sources for this magnet link.
*/
public Set<URN> getGUIDUrns() {
if (guidUrns != null) {
return guidUrns;
}
Set<URN> urns = null;
List<String> potentialUrns = getPotentialURNs();
for (String candidate : potentialUrns) {
try {
URN urn = URN.createGUIDUrn(candidate);
if (urns == null) {
urns = new HashSet<URN>(2);
}
urns.add(urn);
} catch (IOException ie) {
// ignore, just not a valid guid urn
}
}
if (urns == null) {
urns = Collections.emptySet();
} else {
urns = Collections.unmodifiableSet(urns);
}
// only set after list is full to avoid race condition where some other
// thread sees the partial list
guidUrns = urns;
return urns;
}
/**
* Returns the display name, i.e. filename or <code>null</code>.
*/
public String getDisplayName() {
List<String> list = optionsMap.get(Option.DN);
if (list == null || list.isEmpty())
return null;
else
return list.get(0);
}
/**
* Returns a file name that can be used for saving for a downloadable
* magnet.
* <p>
* Guaranteed to return a non-null value
*/
public String getFileNameForSaving() {
if (extractedFileName != null)
return extractedFileName;
String name = getRawNameForSaving();
// remove any leading slashes or dots
while (name.startsWith(".") || name.startsWith("\\") || name.startsWith("/"))
name = name.substring(1);
extractedFileName = name;
return extractedFileName;
}
private String getRawNameForSaving() {
String tempFileName = getDisplayName();
if (tempFileName != null && tempFileName.length() > 0) {
return tempFileName;
}
tempFileName = getKeywordTopic();
if (tempFileName != null && tempFileName.length() > 0) {
return tempFileName;
}
URN urn = getSHA1Urn();
if (urn != null) {
tempFileName = urn.toString();
return tempFileName;
}
String[] urls = getDefaultURLs();
if (urls.length > 0) {
try {
URI uri = URIUtils.toURI(urls[0]);
tempFileName = extractFileName(uri);
if (tempFileName != null && tempFileName.length() > 0) {
return tempFileName;
}
} catch (URISyntaxException e) {
}
}
try {
File file = FileUtils.createTempFile("magnet", "");
file.deleteOnExit();
tempFileName = file.getName();
return tempFileName;
} catch (IOException ie) {
}
tempFileName = DOWNLOAD_PREFIX;
return tempFileName;
}
/**
* Returns the keyword topic if there is one, otherwise <code>null</code>.
*/
public String getKeywordTopic() {
List<String> list = optionsMap.get(Option.KT);
if (list == null || list.isEmpty())
return null;
else
return list.get(0);
}
/**
* Returns the file size of the file of this magnet.
*
* @return -1 if file size is not specified for this magnet
*/
public long getFileSize() {
if (fileSize == -2) {
List<String> lengthValues = getList(Option.XL);
long size = -1;
for (String value : lengthValues) {
try {
size = Long.parseLong(value);
} catch (NumberFormatException nfe) {
// ignore
}
}
fileSize = size >= 0 ? size : -1;
}
return fileSize;
}
/**
* Returns a list of exact topic strings, they can be url or urn string.
*/
public List<String> getExactTopics() {
return getList(Option.XT);
}
/**
* Returns the list of exact source strings, they should be urls.
*/
public List<String> getXS() {
return getList(Option.XS);
}
/**
* Returns the list of exact source strings, they should be a singleton
* list.
*/
public List<String> getXL() {
return getList(Option.XL);
}
/**
* Returns the list of alternate source string, they should be urls.
*/
public List<String> getAS() {
return getList(Option.AS);
}
private List<String> getList(Option key) {
List<String> l = optionsMap.get(key);
if (l == null)
return Collections.emptyList();
else
return l;
}
/**
* Returns a localized error message if of the last invalid url that was
* parsed.
*
* @return null if there was no error
*/
public String getErrorMessage() {
return localizedErrorMessage;
}
/**
* Returns the filename to use for the download, guessed if necessary.
*
* @param uri the URL for the resource, which must not be <code>null</code>
*/
public static String extractFileName(URI uri) {
// If the URL has a filename, return that. Remember that URL.getFile()
// may include directory information, e.g., "/path/file.txt" or
// "/path/".
// It also returns "" if no file part.
String path = null;
String host = null;
path = uri.getPath();
host = uri.getHost();
if (path != null && path.length() > 0) {
int i = path.lastIndexOf('/');
if (i < 0)
return path; // e.g., "file.txt"
if (i >= 0 && i < (path.length() - 1))
return path.substring(i + 1); // e.g., "/path/to/file"
}
// In the rare case of no filename ("http://www.limewire.com" or
// "http://www.limewire.com/path/"), just make something up.
if (host != null) {
return DOWNLOAD_PREFIX + host;
} else {
return DOWNLOAD_PREFIX;
}
}
}