package com.limegroup.gnutella.version; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.Log; import java.util.Collections; import java.util.List; import java.util.LinkedList; import java.util.Iterator; import java.io.StringReader; import java.io.IOException; import java.net.URLEncoder; import org.apache.xerces.parsers.DOMParser; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.NamedNodeMap; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.xml.LimeXMLUtils; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.settings.ApplicationSettings; /** * An abstraction for the update XML. * Contains the ID & timestamp of the message, as well as the list * of UpdateData information for individual messages. */ class UpdateCollection { private static final Log LOG = LogFactory.getLog(UpdateCollection.class); /** * The id of this UpdateCollection. */ private int collectionId = Integer.MIN_VALUE; /** * The timestamp of this collection. */ private long collectionTimestamp = -1; /** * The list of UpdateData's in this collection. */ private List updateDataList = new LinkedList(); /** * The list of DownloadDatas in this collection. */ private List downloadDataList = new LinkedList(); /** * Ensure that this is only created by using the factory constructor. */ private UpdateCollection() {} /** * A string rep of the collection. */ public String toString() { return "Update Collection, id: " + collectionId + ", timestamp: " + collectionTimestamp + ", data: " + updateDataList; } /** * Gets the id of this UpdateCollection. */ int getId() { return collectionId; } /** * Gets the timestamp. */ long getTimestamp() { return collectionTimestamp; } /** * Gets the UpdateData objects. */ List getUpdateData() { return updateDataList; } /** * Gets all updates that have information so we can download them. */ List getUpdatesWithDownloadInformation() { return Collections.unmodifiableList(downloadDataList); } /** * Gets the UpdateData that is relevant to us. * Returns null if there is no relevant update. */ UpdateData getUpdateDataFor(Version currentV, String lang, boolean currentPro, int currentStyle, Version currentJava) { UpdateData englishMatch = null; UpdateData exactMatch = null; // Iterate through them till we find an acceptable version. // Remember for the 'English' and 'Exact' match -- // If we got an exact, use that. Otherwise, use English. for(Iterator i = updateDataList.iterator(); i.hasNext(); ) { UpdateData next = (UpdateData)i.next(); if(next.isAllowed(currentV, currentPro, currentStyle, currentJava)) { if(lang.equals(next.getLanguage())) { exactMatch = next; break; } else if("en".equals(next.getLanguage()) && englishMatch == null) { englishMatch = next; } } } if(exactMatch == null) return englishMatch; else return exactMatch; } /** * Constructs and returns a new UpdateCollection that corresponds * to the elements in the XML. */ static UpdateCollection create(String xml) { if(LOG.isTraceEnabled()) LOG.trace("Parsing Update XML: " + xml); UpdateCollection collection = new UpdateCollection(); collection.parse(xml); return collection; } /** * Parses the XML and fills in the data of this collection. */ private void parse(String xml) { DOMParser parser = new DOMParser(); InputSource is = new InputSource(new StringReader(xml)); try { parser.parse(is); } catch(IOException ioe) { LOG.error("Unable to parse: " + xml, ioe); return; } catch(SAXException sax) { LOG.error("Unable to parse: " + xml, sax); return; } parseDocumentElement(parser.getDocument().getDocumentElement()); } /** * Parses the document element. * * This requires that the element be "update" and has the attribute 'id'. * The 'timestamp' attribute is checked (but is optional), as are child 'msg' * elements. */ private void parseDocumentElement(Node doc) { // Ensure the document element is the 'update' element. if(!"update".equals(doc.getNodeName())) return; // Parse the 'id' & 'timestamp' attributes. NamedNodeMap attr = doc.getAttributes(); // we MUST have an id. String idText = getAttributeText(attr, "id"); if(idText == null) { LOG.error("No id attribute."); return; } try { collectionId = Integer.parseInt(idText); } catch(NumberFormatException nfe) { LOG.error("Couldn't get collection id from: " + idText, nfe); return; } // Parse the optional 'timestamp' attribute. String timestampText = getAttributeText(attr, "timestamp"); if(timestampText != null) { try { collectionTimestamp = Long.parseLong(timestampText); } catch(NumberFormatException nfe) { LOG.warn("Couldn't get timestamp from: " + timestampText, nfe); } } NodeList children = doc.getChildNodes(); for(int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if("msg".equals(child.getNodeName())) parseMsgItem(child); } } /** * Parses a single msg item. * * The elements this parses are: * from -- OPTIONAL (defaults to 0.0.0) * to -- OPTIONAL (defaults to the 'for' value) * for -- REQUIRED * pro -- OPTIONAL (see free) * free -- OPTIONAL (if both pro & free are missing, both default to true. otherwise they defaults to false. any non-null value == true) * url -- REQUIRED * style -- REQUIRED (accepts a number only.) * javafrom -- OPTIONAL (see javato) * javato -- OPTIONAL (if both are missing, all ranges are valid. if one is missing, defaults to above or below that.) * os -- OPTIONAL (defaults to '*' -- accepts a comma delimited list.) * * The below elements are necessary for downloading the update in the network. * urn -- The BITPRINT of the download * ucommand -- The command to run to invoke the update. * uname -- The filename on disk the update should have. * size -- The size of the update when completed. * * If any values exist but error while parsing, the entire block is considered * invalid and ignored. */ private void parseMsgItem(Node msg) { UpdateData data = new UpdateData(); NamedNodeMap attr = msg.getAttributes(); String fromV = getAttributeText(attr, "from"); String toV = getAttributeText(attr, "to"); String forV = getAttributeText(attr, "for"); String pro = getAttributeText(attr, "pro"); String free = getAttributeText(attr, "free"); String url = getAttributeText(attr, "url"); String style = getAttributeText(attr, "style"); String javaFrom = getAttributeText(attr, "javafrom"); String javaTo = getAttributeText(attr, "javato"); String os = getAttributeText(attr, "os"); String updateURN = getAttributeText(attr, "urn"); String updateCommand = getAttributeText(attr, "ucommand"); String updateName = getAttributeText(attr, "uname"); String fileSize = getAttributeText(attr, "size"); if(updateURN != null) { try { URN urn = URN.createSHA1Urn(updateURN); String tt = URN.getTigerTreeRoot(updateURN); data.setUpdateURN(urn); data.setUpdateTTRoot(tt); } catch(IOException ignored) { LOG.warn("Invalid bitprint urn: " + updateURN, ignored); } } data.setUpdateCommand(updateCommand); data.setUpdateFileName(updateName); if(fileSize != null) { try { data.setUpdateSize(Integer.parseInt(fileSize)); } catch(NumberFormatException nfe) { LOG.warn("Invalid size: " + fileSize); } } // if this has enough information for downloading, add it to the list of potentials. if(data.getUpdateURN() != null && data.getUpdateFileName() != null && data.getSize() != 0) { if (LOG.isDebugEnabled()) LOG.debug("Adding new download data item: " + data); downloadDataList.add(data); } if(forV == null || url == null || style == null) { LOG.error("Missing required for, url, or style."); return; } if(fromV == null) fromV = "0.0.0"; if(toV == null) toV = forV; try { data.setFromVersion(new Version(fromV)); data.setToVersion(new Version(toV)); data.setForVersion(new Version(forV)); if(javaFrom != null) data.setFromJava(new Version(javaFrom)); if(javaTo != null) data.setToJava(new Version(javaTo)); } catch(VersionFormatException vfe) { LOG.error("Invalid version", vfe); return; } if(pro == null && free == null) { data.setPro(true); data.setFree(true); } else { data.setPro(pro != null); data.setFree(free != null); } // Update the URL to contain the correct pro & language. if(url.indexOf('?') == -1) url += "?"; else url += "&"; url += "pro=" + CommonUtils.isPro() + "&lang=" + encode(ApplicationSettings.getLanguage()) + "&lv=" + encode(CommonUtils.getLimeWireVersion()) + "&jv=" + encode(CommonUtils.getJavaVersion()) + "&os=" + encode(CommonUtils.getOS()) + "&osv=" + encode(CommonUtils.getOSVersion()); data.setUpdateURL(url); try { data.setStyle(Integer.parseInt(style)); } catch(NumberFormatException nfe) { LOG.error("Invalid style", nfe); return; } if(os == null) os = "*"; data.setOSList(OS.createFromList(os)); NodeList children = msg.getChildNodes(); for(int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if("lang".equals(child.getNodeName())) parseLangItem((UpdateData)data.clone(), child); } } /** * Parses a single lang item. * * Accepts attributes 'id', 'button1', and 'button2'. * 'id' is REQUIRD. others are optional. * REQUIRES a text content inside. */ private void parseLangItem(UpdateData data, Node lang) { // Parse the id & url & current attributes -- all MUST exist. NamedNodeMap attr = lang.getAttributes(); String id = getAttributeText(attr, "id"); String button1 = getAttributeText(attr, "button1"); String button2 = getAttributeText(attr, "button2"); String title = getAttributeText(attr, "title"); String msg = LimeXMLUtils.getTextContent(lang); if(id == null || msg == null || msg.equals("")) { LOG.error("Missing id or message."); return; } data.setLanguage(id); data.setButton1Text(button1); data.setButton2Text(button2); data.setUpdateText(msg); data.setUpdateTitle(title); // A-Okay -- we've got a good UpdateData. updateDataList.add(data); } /** * Gets the text from an attribute map. */ private String getAttributeText(NamedNodeMap map, String attr) { Node node = map.getNamedItem(attr); if(node != null) return node.getNodeValue(); else return null; } /** * Converts a string into url encoding. */ private String encode(String unencoded) { return URLEncoder.encode(unencoded); } }