package com.limegroup.gnutella.browser; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.InetSocketAddress; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import com.limegroup.gnutella.FileDetails; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.util.URLDecoder; /** * Contains information fields extracted from a magnet link. */ public class MagnetOptions implements Serializable { 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 optionsMap; private static final String XS = "XS"; private static final String XT = "XT"; private static final String AS = "AS"; private static final String DN = "DN"; private static final String KT = "KT"; private transient String [] defaultURLs; private transient String localizedErrorMessage; private transient URN urn; private transient String extractedFileName; /** * Creates a MagnetOptions object from file details. * <p> * The resulting MagnetOptions might not be * {@link #isDownloadable() downloadable}. * @param fileDetails * @return */ public static MagnetOptions createMagnet(FileDetails fileDetails) { HashMap map = new HashMap(); map.put(DN, fileDetails.getFileName()); URN urn = fileDetails.getSHA1Urn(); if (urn != null) { addAppend(map, XT, urn.httpStringValue()); } InetSocketAddress isa = fileDetails.getSocketAddress(); String url = null; if (isa != null && urn != null) { StringBuffer addr = new StringBuffer("http://"); addr.append(isa.getAddress().getHostAddress()).append(':') .append(isa.getPort()).append("/uri-res/N2R?"); addr.append(urn.httpStringValue()); url = addr.toString(); addAppend(map, XS, url); } MagnetOptions magnet = new MagnetOptions(map); // set already known values magnet.urn = urn; if (url != null) { magnet.defaultURLs = new String[] { url }; } 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> * @return */ public static MagnetOptions createMagnet(String keywordTopics, String fileName, URN urn, String[] defaultURLs) { HashMap map = new HashMap(); map.put(KT, keywordTopics); map.put(DN, fileName); if (urn != null) { addAppend(map, XT, urn.httpStringValue()); } if (defaultURLs != null) { for (int i = 0; i < defaultURLs.length; i++) { addAppend(map, AS, defaultURLs[i]); } } 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]; } return magnet; } /** * 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) { HashMap options = new HashMap(); // 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 - TODO: case sensitive? if ( !arg.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 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 = (Map) options.get(iIndex); if (curOptions == null) { curOptions = new HashMap(); options.put(iIndex,curOptions); } if ( keystr.startsWith("xt") ) { addAppend(curOptions, XT, cmdstr); } else if ( keystr.startsWith("dn") ) { curOptions.put(DN,cmdstr); } else if ( keystr.startsWith("kt") ) { curOptions.put(KT,cmdstr); } else if ( keystr.startsWith("xs") ) { addAppend(curOptions, XS, cmdstr ); } else if ( keystr.startsWith("as") ) { addAppend(curOptions, AS, cmdstr ); } } MagnetOptions[] ret = new MagnetOptions[options.size()]; int i = 0; for (Iterator iter = options.values().iterator(); iter.hasNext(); i++) { Map current = (Map)iter.next(); ret[i] = new MagnetOptions(current); } return ret; } private static void addAppend(Map map, String key, String value) { List l = (List) map.get(key); if (l == null) { l = new ArrayList(1); map.put(key,l); } l.add(value); } private MagnetOptions(Map options) { optionsMap = Collections.unmodifiableMap(options); } 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. * @return */ public String toExternalForm() { StringBuffer ret = new StringBuffer(MAGNET); for (Iterator iter = getExactTopics().iterator(); iter.hasNext();) { String xt = (String) iter.next(); ret.append("&xt=").append(xt); } if (getDisplayName() != null) ret.append("&dn=").append(URLEncoder.encode(getDisplayName())); if (getKeywordTopic() != null) ret.append("&kt=").append(URLEncoder.encode(getKeywordTopic())); for (Iterator iter = getXS().iterator(); iter.hasNext();) { String xs = (String) iter.next(); ret.append("&xs=").append(xs); } for (Iterator iter = getAS().iterator(); iter.hasNext();) { String as = (String) iter.next(); ret.append("&as=").append(as); } return ret.toString(); } /** * Returns the sha1 urn of this magnet uri if it has one. * <p> * It looks in the exacty topics, the exact sources and then in the alternate * sources for it. * @return */ 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 = new URI(defaultURLs[i].toCharArray()); String query = uri.getQuery(); if (query != null) { return URN.createSHA1Urn(uri.getQuery()); } } catch (URIException 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. * @return */ public boolean isDownloadable() { return getDefaultURLs().length > 0 || (getSHA1Urn() != null && getQueryString() != null); } /** * 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. * @return */ 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. * @return */ 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. * @return */ 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 strings) { for (Iterator iter = strings.iterator(); iter.hasNext(); ) { try { return URN.createSHA1Urn((String)iter.next()); } catch (IOException e) { } } return null; } private List getPotentialURLs() { List urls = new ArrayList(); urls.addAll(getPotentialURLs(getExactTopics())); urls.addAll(getPotentialURLs(getXS())); urls.addAll(getPotentialURLs(getAS())); return urls; } private List getPotentialURLs(List strings) { List ret = new ArrayList(); for (Iterator iter = strings.iterator(); iter.hasNext(); ) { String str = (String)iter.next(); if (str.startsWith(HTTP)) ret.add(str); } return ret; } /** * Returns all valid urls that can be tried for downloading. * @return */ public String[] getDefaultURLs() { if (defaultURLs == null) { List urls = getPotentialURLs(); for(Iterator it = urls.iterator(); it.hasNext(); ) { try { String nextURL = (String)it.next(); new URI(nextURL.toCharArray()); // is it a valid URI? } catch(URIException e) { it.remove(); // if not, remove it from the list. localizedErrorMessage = e.getLocalizedMessage(); } } defaultURLs = (String[])urls.toArray(new String[urls.size()]); } return defaultURLs; } /** * Returns the display name, i.e. filename or <code>null</code>. * @return */ public String getDisplayName() { return (String)optionsMap.get(DN); } /** * Returns a file name that can be used for saving for a downloadable magnet. * <p> * Guaranteed to return a non-null value * @return */ 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 = new URI(urls[0].toCharArray()); tempFileName = extractFileName(uri); if (tempFileName != null && tempFileName.length() > 0) { return tempFileName; } } catch (URIException e) { } } try { File file = File.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>. * @return */ public String getKeywordTopic() { return (String)optionsMap.get(KT); } /** * Returns a list of exact topic strings, they can be url or urn string. * @return */ public List getExactTopics() { return getList(XT); } /** * Returns the list of exact source strings, they should be urls. * @return */ public List getXS() { return getList(XS); } /** * Returns the list of alternate source string, they should be urls. * @return */ public List getAS() { return getList(AS); } private List getList(String key) { List l = (List) optionsMap.get(key); return l == null ? Collections.EMPTY_LIST : 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; try { path = uri.getPath(); host = uri.getHost(); } catch (URIException e) { } 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; } } }