package com.limegroup.gnutella.version; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.util.Version; import org.limewire.util.VersionFormatException; import org.limewire.util.XMLUtils; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.limegroup.gnutella.ApplicationServices; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.util.LimeWireUtils; import com.limegroup.gnutella.xml.LimeXMLUtils; public class UpdateCollectionImpl implements UpdateCollection { private static final Log LOG = LogFactory.getLog(UpdateCollectionImpl.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 final List<UpdateData> updateDataList; /** * The list of DownloadDatas in this collection. */ private List<DownloadInformation> downloadDataList = new LinkedList<DownloadInformation>(); private final ApplicationServices applicationServices; /** * Ensure that this is only created by using the factory constructor. */ UpdateCollectionImpl(String xml, ApplicationServices applicationServices) { this.applicationServices = applicationServices; if(LOG.isTraceEnabled()) LOG.trace("Parsing Update XML: " + xml); List<UpdateData> updateData = new ArrayList<UpdateData>(); parse(xml, updateData); updateDataList = Collections.unmodifiableList(updateData); } /** * A string rep of the collection. */ @Override public String toString() { return "Update Collection, id: " + collectionId + ", timestamp: " + collectionTimestamp + ", data: " + updateDataList; } /* (non-Javadoc) * @see com.limegroup.gnutella.version.UpdateCollectionI#getId() */ public int getId() { return collectionId; } /* (non-Javadoc) * @see com.limegroup.gnutella.version.UpdateCollectionI#getTimestamp() */ public long getTimestamp() { return collectionTimestamp; } /* (non-Javadoc) * @see com.limegroup.gnutella.version.UpdateCollectionI#getUpdateData() */ public List<UpdateData> getUpdateData() { return updateDataList; } /* (non-Javadoc) * @see com.limegroup.gnutella.version.UpdateCollectionI#getUpdatesWithDownloadInformation() */ public List<DownloadInformation> getUpdatesWithDownloadInformation() { return Collections.unmodifiableList(downloadDataList); } /* (non-Javadoc) * @see com.limegroup.gnutella.version.UpdateCollectionI#getUpdateDataFor(org.limewire.util.Version, java.lang.String, boolean, int, org.limewire.util.Version) */ public 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(UpdateData next : updateDataList) { 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; } /** * Parses the XML and fills in the data of this collection. * @param updateDataList */ private void parse(String xml, List<UpdateData> updateDataList) { Document d; try { d = XMLUtils.getDocument(xml, LOG); } catch(IOException ioe) { LOG.error("Unable to parse: " + xml, ioe); return; } parseDocumentElement(d.getDocumentElement(), updateDataList); } /** * 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. * @param updateDataList */ private void parseDocumentElement(Node doc, List<UpdateData> updateDataList) { // 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, updateDataList); } } /** * 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. * @param updateDataList */ private void parseMsgItem(Node msg, List<UpdateData> updateDataList) { 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 osv = getAttributeText(attr, "osv"); 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. url = LimeWireUtils.addLWInfoToUrl(url, applicationServices.getMyGUID()); 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,osv)); 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, updateDataList); } } /** * Parses a single lang item. * * Accepts attributes 'id', 'button1', and 'button2'. * 'id' is REQUIRD. others are optional. * REQUIRES a text content inside. * @param updateDataList */ private void parseLangItem(UpdateData data, Node lang, List<UpdateData> updateDataList) { // 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; } }