/**
* Copyright (c) 2011 Source Auditor Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.spdx.rdfparser;
import java.util.ArrayList;
import java.util.HashSet;
import org.spdx.spdxspreadsheet.InvalidLicenseStringException;
import com.hp.hpl.jena.graph.Node;
import com.hp.hpl.jena.graph.Triple;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;
/**
* Factory for creating SPDXLicenseInfo objects from a Jena model
* @author Gary O'Neall
*
*/
public class SPDXLicenseInfoFactory {
static final String[] STANDARD_LICENSE_IDS = new String[] {
"AFL-3","AFL-1.1","AFL-1.2","AFL-2","AFL-2.1","APL-1",
"Apache-1","Apache-1.1","Apache-2","APSL-1","APSL-1.1",
"APSL-1.2","APSL-2","Artistic-1","Artistic-2","AAL",
"BSL-1","BSD-2-Clause","BSD-3-Clause","BSD-4-Clause",
"CECILL-1","CECILL-2","CECILL-B","CECILL-C","ClArtistic",
"CDDL-1","CPAL-1","CPL-1","CATOSL-1.1","CC-BY-1","CC-BY-2",
"CC-BY-2.5","CC-BY-3","CC-BY-ND-1","CC-BY-ND-2","CC-BY-ND-2.5",
"CC-BY-ND-3","CC-BY-NC-1","CC-BY-NC-2","CC-BY-NC-2.5","CC-BY-NC-3",
"CC-BY-NC-ND-1","CC-BY-NC-ND-2","CC-BY-NC-ND-2.5","CC-BY-NC-ND-3",
"CC-BY-NC-SA-1","CC-BY-NC-SA-2","CC-BY-NC-SA-2.5","CC-BY-NC-SA-3",
"CC-BY-SA-1","CC-BY-SA-2","CC-BY-SA-2.5","CC-BY-SA-3","CUA-OPL-1",
"EPL-1","eCos-2","ECL-1","ECL-2","EFL-1","EFL-2","Entessa",
"ErlPL-1.1","EUDatagrid","EUPL-1","EUPL-1.1","Fair","Frameworx-1",
"AGPL-3","GFDL-1.2","GFDL-1.2","GFDL-1.3","GPL-1","GPL-1+",
"GPL-2","GPL-2+","GPL-2-with-autoconf-exception","GPL-2-with-bison-exception",
"GPL-2-with-classpath-exception","GPL-2-with-GCC-exception",
"GPL-2-with-font-exception","GPL-3","GPL-3+","GPL-3-with-autoconf-exception",
"GPL-3-with-GCC-exception","LGPL-2.1","LGPL-2.1+","LGPL-3",
"LGPL-3+","LGPL-2","LGPL-2+","LGPL+","gSOAP-1.3b","HPND",
"IPL-1","IPA","ISC","LPPL-1","LPPL-1.1","LPPL-1.2","LPPL-1.3c",
"Libpng","LPL-1.02","MS-PL","MS-RL","MirOS","MIT","Motosoto",
"MPL-1","MPL-1.1","Multics","NASA-1.3","Nauman","NGPL","Nokia",
"NPOSL-3","NTP","OCLC-2","OGTSL","OSL-1","OSL-2","OSL-3","OLDAP-2.8",
"OpenSSL","PHP-3","PostgreSQL","Python-CNRI","Python","QPL-1",
"RPSL-1","RPL-1.5","RHeCos-1.1","RSCPL","Ruby","OFL-1.1","Simple-2",
"Sleepycat","SugarCRM-1.1.3","SPL","Watcom-1","NCSA","VSL-1",
"W3C","WXwindows","Xnet","XFree86-1.1","YPL-1.1","Zimbra-1.3",
"Zlib","ZPL-1.1","ZPL-2","ZPL-2.1"
};
static final HashSet<String> STANDARD_LICENSE_ID_SET = new HashSet<String>();
static {
for (int i = 0; i < STANDARD_LICENSE_IDS.length; i++) {
STANDARD_LICENSE_ID_SET.add(STANDARD_LICENSE_IDS[i]);
}
}
/**
* Create the appropriate SPDXLicenseInfo from the model and node provided.
* The appropriate SPDXLicenseInfo subclass object will be chosen based on
* the class (rdf type) of the node
* @param model
* @param node
* @return
*/
static SPDXLicenseInfo getLicenseInfoFromModel(Model model, Node node) throws InvalidSPDXAnalysisException {
if (!node.isURI() && !node.isBlank()) {
throw(new InvalidSPDXAnalysisException("Can not create a LicenseInfo from a literal node"));
}
// find the subclass
Node rdfTypePredicate = model.getProperty(SPDXAnalysis.RDF_NAMESPACE,
SPDXAnalysis.RDF_PROP_TYPE).asNode();
Triple m = Triple.createMatch(node, rdfTypePredicate, null);
ExtendedIterator<Triple> tripleIter = model.getGraph().find(m); // find the type(s)
if (!tripleIter.hasNext()) {
throw(new InvalidSPDXAnalysisException("There is no type associated with a licenseInfo"));
}
Triple triple = tripleIter.next();
if (tripleIter.hasNext()) {
throw(new InvalidSPDXAnalysisException("More than one type associated with a licenseInfo"));
}
Node typeNode = triple.getObject();
if (!typeNode.isURI()) {
throw(new InvalidSPDXAnalysisException("Invalid type for licenseInfo - not a URI"));
}
// need to parse the URI
String typeUri = typeNode.getURI();
if (!typeUri.startsWith(SPDXAnalysis.SPDX_NAMESPACE)) {
throw(new InvalidSPDXAnalysisException("Invalid type for licenseInfo - not an SPDX type"));
}
String type = typeUri.substring(SPDXAnalysis.SPDX_NAMESPACE.length());
if (type.equals(SPDXAnalysis.CLASS_SPDX_CONJUNCTIVE_LICENSE_SET)) {
return new SPDXConjunctiveLicenseSet(model, node);
} else if (type.equals(SPDXAnalysis.CLASS_SPDX_DISJUNCTIVE_LICENSE_SET)) {
return new SPDXDisjunctiveLicenseSet(model, node);
}else if (type.equals(SPDXAnalysis.CLASS_SPDX_NON_STANDARD_LICENSE)) {
return new SPDXNonStandardLicense(model, node);
}else if (type.equals(SPDXAnalysis.CLASS_SPDX_STANDARD_LICENSE)) {
return new SPDXStandardLicense(model, node);
} else {
throw(new InvalidSPDXAnalysisException("Invalid type for licenseInfo '"+type+"'"));
}
}
/**
* Parses a license string and converts it into a SPDXLicenseInfo object
* Syntax - A license set must start and end with a parenthesis "("
* A conjunctive license set will have and AND after the first
* licenseInfo term
* A disjunctive license set will have an OR after the first
* licenseInfo term
* If there is no And or Or, then it is converted to a simple
* license type (standard or non-standard)
* A space or tab must be used between license ID's and the
* keywords AND and OR
* A licenseID must NOT be "AND" or "OR"
* @param licenseString String conforming to the syntax
* @return an SPDXLicenseInfo created from the string
* @throws InvalidLicenseStringException
*/
public static SPDXLicenseInfo parseSPDXLicenseString(String licenseString) throws InvalidLicenseStringException {
String parseString = licenseString.trim();
if (parseString.startsWith("(")) {
if (!parseString.endsWith(")")) {
throw(new InvalidLicenseStringException("Missing end ')'"));
}
// this will be treated some form of License Set
parseString = parseString.substring(1, parseString.length()-1).trim();
return parseLicenseSet(parseString);
} else {
// this is either a standard license or a non-standard license
int startOfIDPos = skipWhiteSpace(parseString, 0);
int endOfIDPos = skipNonWhiteSpace(parseString, startOfIDPos);
String licenseID = parseString.substring(startOfIDPos, endOfIDPos);
if (isStandardLicenseID(licenseID)) {
return new SPDXStandardLicense(licenseID, null, null, null, null, null, null);
} else {
return new SPDXNonStandardLicense(licenseID, null);
}
}
}
/**
* Parses a license set which consists of a list of LicenseInfo strings
* @param parseString
* @return
* @throws InvalidLicenseStringException
*/
private static SPDXLicenseInfo parseLicenseSet(String parseString) throws InvalidLicenseStringException {
boolean isConjunctive = false;
boolean isDisjunctive = false;
ArrayList<SPDXLicenseInfo> licenseInfoList = new ArrayList<SPDXLicenseInfo>();
int pos = 0; // character position
while (pos < parseString.length()) {
// skip white space
pos = skipWhiteSpace(parseString, pos);
if (pos >= parseString.length()) {
break; // we are done
}
// collect the license information
if (parseString.charAt(pos) == '(') {
int startOfSet = pos + 1;
pos = findEndOfSet(parseString, pos);
if (pos > parseString.length() || parseString.charAt(pos) != ')') {
throw(new InvalidLicenseStringException("Missing end ')'"));
}
licenseInfoList.add(parseLicenseSet(parseString.substring(startOfSet, pos)));
pos++;
} else {
// a license ID
int startOfID = pos;
pos = skipNonWhiteSpace(parseString, pos);
String licenseID = parseString.substring(startOfID, pos);
if (isStandardLicenseID(licenseID)) {
licenseInfoList.add(new SPDXStandardLicense(null, licenseID, null, null, null, null, null));
} else {
licenseInfoList.add(new SPDXNonStandardLicense(licenseID, null));
}
}
if (pos >= parseString.length()) {
break; // done
}
// consume the AND or the OR
// skip more whitespace
pos = skipWhiteSpace(parseString, pos);
if (parseString.charAt(pos) == 'A' || parseString.charAt(pos) == 'a') {
// And
if (pos + 4 >= parseString.length() ||
!parseString.substring(pos, pos+4).toUpperCase().equals("AND ")) {
throw(new InvalidLicenseStringException("Expecting an AND"));
}
isConjunctive = true;
pos = pos + 4;
} else if (parseString.charAt(pos) == 'O' || parseString.charAt(pos) == 'o') {
// or
if (pos + 3 >= parseString.length() ||
!parseString.substring(pos, pos+3).toUpperCase().equals("OR ")) {
throw(new InvalidLicenseStringException("Expecting an OR"));
}
isDisjunctive = true;
pos = pos + 3;
} else {
throw(new InvalidLicenseStringException("Expecting an AND or an OR"));
}
}
if (isConjunctive && isDisjunctive) {
throw(new InvalidLicenseStringException("Can not have both AND's and OR's inside the same set of parenthesis"));
}
SPDXLicenseInfo[] licenseInfos = new SPDXLicenseInfo[licenseInfoList.size()];
licenseInfos = licenseInfoList.toArray(licenseInfos);
if (isConjunctive) {
return new SPDXConjunctiveLicenseSet(licenseInfos);
} else if (isDisjunctive) {
return new SPDXDisjunctiveLicenseSet(licenseInfos);
} else {
throw(new InvalidLicenseStringException("Missing AND or OR inside parenthesis"));
}
}
/**
* @param parseString
* @return
* @throws InvalidLicenseStringException
*/
private static int findEndOfSet(String parseString, int pos) throws InvalidLicenseStringException {
if (parseString.charAt(pos) != '(') {
throw(new InvalidLicenseStringException("Expecting '('"));
}
int retval = pos;
retval++;
while (retval < parseString.length() && parseString.charAt(retval) != ')') {
if (parseString.charAt(retval) == '(') {
retval = findEndOfSet(parseString, retval) + 1;
} else {
retval++;
}
}
return retval;
}
/**
* @param parseString
* @param pos
* @return
*/
private static int skipWhiteSpace(String parseString, int pos) {
int retval = pos;
char c = parseString.charAt(retval);
while (retval < parseString.length() &&
(c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
retval++;
if (retval < parseString.length()) {
c = parseString.charAt(retval);
}
}
return retval;
}
/**
* @param parseString
* @param pos
* @return
*/
private static int skipNonWhiteSpace(String parseString, int pos) {
int retval = pos;
char c = parseString.charAt(retval);
while (retval < parseString.length() &&
c != ' ' && c != '\t' && c != '\r' && c != '\n') {
retval++;
if (retval < parseString.length()) {
c = parseString.charAt(retval);
}
}
return retval;
}
/**
* @param licenseID
* @return true if the licenseID belongs to a standard license
*/
public static boolean isStandardLicenseID(String licenseID) {
// TODO Replace with a lookup of the website
return STANDARD_LICENSE_ID_SET.contains(licenseID);
}
}