package com.limegroup.gnutella.licenses;
import java.io.IOException;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import java.net.URL;
import java.net.MalformedURLException;
import org.apache.commons.httpclient.URI;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.URN;
/**
* A concrete implementation of a License, for Creative Commons licenses.
*/
class CCLicense extends AbstractLicense {
private static final Log LOG = LogFactory.getLog(CCLicense.class);
private static final long serialVersionUID = 8213994964631107858L;
/** The license string. */
private transient String license;
/** The license information for each Work. */
private Map /* URN -> Details */ allWorks;
/**
* Constructs a new CCLicense.
*/
CCLicense(String license, URI uri) {
super(uri);
this.license = license;
}
public String getLicense() {
return license;
}
/**
* Retrieves the license deed for the given URN.
*/
public URL getLicenseDeed(URN urn) {
Details details = getDetails(urn);
if(details == null || details.licenseURL == null)
return guessLicenseDeed();
else
return details.licenseURL;
}
/**
* Attempts to guess what the license URI is from the license text.
*/
private URL guessLicenseDeed() {
return CCConstants.guessLicenseDeed(license);
}
/**
* Determines if the CC License is valid with this URN.
*/
public boolean isValid(URN urn) {
return getDetails(urn) != null;
}
/**
* Returns a CCLicense exactly like this, except
* with a different license string.
*/
public License copy(String license, URI licenseURI) {
CCLicense newL = null;
try {
newL = (CCLicense)clone();
newL.license = license;
newL.licenseLocation = licenseURI;
} catch(CloneNotSupportedException error) {
ErrorService.error(error);
}
return newL;
}
/**
* Builds a description of this license based on what is permitted,
* probibited, and required.
*/
public String getLicenseDescription(URN urn) {
List permitted = Collections.EMPTY_LIST;
List prohibited = Collections.EMPTY_LIST;
List required = Collections.EMPTY_LIST;
Details details = getDetails(urn);
if(details != null) {
permitted = details.permitted;
prohibited = details.prohibited;
required = details.required;
}
StringBuffer sb = new StringBuffer();
if(permitted != null && !permitted.isEmpty()) {
sb.append("Permitted: ");
for(Iterator i = permitted.iterator(); i.hasNext(); ) {
sb.append(i.next().toString());
if(i.hasNext())
sb.append(", ");
}
}
if(prohibited != null && !prohibited.isEmpty()) {
if(sb.length() != 0)
sb.append("\n");
sb.append("Prohibited: ");
for(Iterator i = prohibited.iterator(); i.hasNext(); ) {
sb.append(i.next().toString());
if(i.hasNext())
sb.append(", ");
}
}
if(required != null && !required.isEmpty()) {
if(sb.length() != 0)
sb.append("\n");
sb.append("Required: ");
for(Iterator i = required.iterator(); i.hasNext(); ) {
sb.append(i.next().toString());
if(i.hasNext())
sb.append(", ");
}
}
if(sb.length() == 0)
sb.append("Permissions unknown.");
return sb.toString();
}
/**
* Erases all data associated with a verification.
*/
protected void clear() {
if(allWorks != null)
allWorks.clear();
}
/**
* Locates the RDF from the body of the URL.
*/
protected String getBody(String url) {
return locateRDF(super.getBody(url));
}
///// WORK & DETAILS CODE ///
/**
* Adds the given a work with the appropriate details to allWorks.
*/
private void addWork(URN urn, String licenseURL) {
URL url = null;
try {
url = new URL(licenseURL);
} catch(MalformedURLException murl) {
LOG.warn("Unable to make licenseURL out of: " + licenseURL, murl);
}
//See if we can refocus an existing licenseURL.
Details details = getDetails(urn);
if(details != null) {
if(LOG.isDebugEnabled())
LOG.debug("Found existing details item for URN: " + urn);
if(url != null) {
URL guessed = guessLicenseDeed();
if(guessed != null && guessed.equals(url)) {
if(LOG.isDebugEnabled())
LOG.debug("Updating license URL to be: " + url);
details.licenseURL = url;
}
}
// Otherwise, not much else we can do.
// We already have a Details for this URN and it has
// a licenseURL already.
return;
}
// There's no existing details for this item, so lets add one.
details = new Details(url);
if(LOG.isDebugEnabled())
LOG.debug("Adding new " + details + " for urn: " + urn);
if(allWorks == null)
allWorks = new HashMap(1); // assume it's small.
allWorks.put(urn, details); // it is fine if urn is null.
}
/**
* Locates a details for a given URN.
*/
private Details getDetails(URN urn) {
if(allWorks == null)
return null;
// First see if there's a details that matches exactly.
Details details = (Details)allWorks.get(urn);
if(details != null)
return details;
// Okay, nothing matched.
// If we want a specific URN, we can only give back the 'null' one.
if(urn != null)
return (Details)allWorks.get(null);
// We must have wanted the null one. Give back the first one we find.
return (Details)allWorks.values().iterator().next();
}
/**
* Locates all details that use the given License URL.
*/
private List getDetailsForLicenseURL(URL url) {
if(allWorks == null || url == null)
return Collections.EMPTY_LIST;
List details = new LinkedList();
for(Iterator i = allWorks.values().iterator(); i.hasNext(); ) {
Details detail = (Details)i.next();
if(detail.licenseURL != null && url.equals(detail.licenseURL))
details.add(detail);
}
return details;
}
/**
* A single details.
*/
private static class Details implements Serializable {
private static final long serialVersionUID = -1719502030054241350L;
URL licenseURL;
List required;
List permitted;
List prohibited;
// for de-serializing.
Details() { }
Details(URL url) {
licenseURL = url;
}
boolean isDescriptionAvailable() {
return required != null || permitted != null || prohibited != null;
}
public String toString() {
return "details:: license: " + licenseURL;
}
}
///// VERIFICATION CODE ///
/**
* Locates RDF from a big string of HTML.
*/
private String locateRDF(String body) {
if(body == null || body.trim().equals(""))
return null;
// look for two rdf:RDF's.
int startRDF = body.indexOf("<rdf:RDF");
if(startRDF >= body.length() - 1)
return null;
int endRDF = body.indexOf("rdf:RDF", startRDF+6);
if(startRDF == -1 || endRDF == -1)
return null;
// find the closing tag.
endRDF = body.indexOf('>', endRDF);
if(endRDF == -1)
return null;
// Alright, we got where the rdf is at!
return body.substring(startRDF, endRDF + 1);
}
/**
* Parses through the XML. If this is live data, we look for works.
* Otherwise (it isn't from the verifier), we only look for licenses.
*/
protected void parseDocumentNode(Node doc, boolean liveData) {
NodeList children = doc.getChildNodes();
// Do a first pass for Work elements.
if(liveData) {
for(int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if(child.getNodeName().equals("Work"))
parseWorkItem(child);
}
}
// And a second pass for License elements.
for(int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if(child.getNodeName().equals("License"))
parseLicenseItem(child);
}
// If this was from the verifier, see if we need to get any more
// license details.
if(liveData)
updateLicenseDetails();
return;
}
/**
* Parses the 'Work' item.
*/
protected void parseWorkItem(Node work) {
if(LOG.isTraceEnabled())
LOG.trace("Parsing work item.");
// Get the URN of this Work item.
NamedNodeMap attributes = work.getAttributes();
Node about = attributes.getNamedItem("rdf:about");
URN expectedURN = null;
if(about != null) {
// attempt to create a SHA1 urn out of it.
try {
expectedURN = URN.createSHA1Urn(about.getNodeValue());
} catch(IOException ioe) {}
}
// Get the license child element.
NodeList children = work.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if(child.getNodeName().equals("license")) {
attributes = child.getAttributes();
Node resource = attributes.getNamedItem("rdf:resource");
// if we found a resource, attempt to add the Work.
if(resource != null)
addWork(expectedURN, resource.getNodeValue());
}
}
// other than it existing, nothing else needs to happen.
return;
}
/**
* Parses the 'license' item.
*/
protected void parseLicenseItem(Node license) {
if(LOG.isTraceEnabled())
LOG.trace("Parsing license item.");
// Get the license URL.
NamedNodeMap attributes = license.getAttributes();
Node about = attributes.getNamedItem("rdf:about");
List details = Collections.EMPTY_LIST;
if(about != null) {
String value = about.getNodeValue();
try {
details = getDetailsForLicenseURL(new URL(value));
} catch(MalformedURLException murl) {
LOG.warn("Unable to create license URL for: " + value, murl);
}
}
// Optimization: If no details, exit early.
if(!details.iterator().hasNext())
return;
List required = null;
List prohibited = null;
List permitted = null;
// Get the 'permit', 'requires', and 'prohibits' values.
NodeList children = license.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
String name = child.getNodeName();
if(name.equalsIgnoreCase("requires")) {
if(required == null)
required = new LinkedList();
addPermission(required, child);
} else if(name.equalsIgnoreCase("permits")) {
if(permitted == null)
permitted = new LinkedList();
addPermission(permitted, child);
} else if(name.equalsIgnoreCase("prohibits")) {
if(prohibited == null)
prohibited = new LinkedList();
addPermission(prohibited, child);
}
}
// Okay, now iterate through each details and set the lists.
for(Iterator i = details.iterator(); i.hasNext(); ) {
Details detail = (Details)i.next();
if(LOG.isDebugEnabled())
LOG.debug("Setting license details for " + details);
detail.required = required;
detail.prohibited = prohibited;
detail.permitted = permitted;
}
return;
}
/**
* Adds a single permission to the list.
*/
private void addPermission(List permissions, Node node) {
NamedNodeMap attributes = node.getAttributes();
Node resource = attributes.getNamedItem("rdf:resource");
if(resource != null) {
String value = resource.getNodeValue();
int slash = value.lastIndexOf('/');
if(slash != -1 && slash != value.length()-1) {
String permission = value.substring(slash+1);
if(!permissions.contains(permission)) {
permissions.add(permission);
if(LOG.isDebugEnabled())
LOG.debug("Added permission: " + permission);
} else {
if(LOG.isWarnEnabled())
LOG.warn("Duplicate permission: " + permission + "!");
}
} else if (LOG.isWarnEnabled()) {
LOG.trace("Unable to find permission name: " + value);
}
} else if(LOG.isWarnEnabled()) {
LOG.warn("No resource item for permission.");
}
}
/**
* Updates the license details, potentially retrieving information
* from the licenseURL in each Details.
*/
private void updateLicenseDetails() {
if(allWorks == null)
return;
for(Iterator i = allWorks.values().iterator(); i.hasNext(); ) {
Details details = (Details)i.next();
if(!details.isDescriptionAvailable() && details.licenseURL != null) {
if(LOG.isDebugEnabled())
LOG.debug("Updating licenseURL for :" + details);
String url = details.licenseURL.toExternalForm();
// First see if we have cached details.
Object data = LicenseCache.instance().getData(url);
String body = null;
if(data != null && data instanceof String) {
if(LOG.isDebugEnabled())
LOG.debug("Using cached data for url: " + url);
body = locateRDF((String)data);
} else {
body = getBody(url);
if(body != null)
LicenseCache.instance().addData(url, body);
else
LOG.debug("Couldn't retrieve license details from url: " + url);
}
// parsing MUST NOT alter allWorks,
// otherwise a ConcurrentMod will happen
if(body != null)
parseXML(body, false);
}
}
}
}