Skip to content

Commit

Permalink
Make license text encoding configurable
Browse files Browse the repository at this point in the history
The license text encoding is optional per standard, but was hardcoded as base64.
This implementation makes it optional and is extensible if the standard introduces additional choices for the encoding.
Old behaviour stays as default for backwards compatibility.
  • Loading branch information
synaos-bwi committed Nov 14, 2022
1 parent b1d4490 commit a5ea36b
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 17 deletions.
4 changes: 4 additions & 0 deletions src/main/java/org/cyclonedx/model/AttachmentText.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@
package org.cyclonedx.model;

import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;

@SuppressWarnings("unused")
@JsonPropertyOrder({
"content-type",
"encoding"})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AttachmentText {

@JacksonXmlProperty(isAttribute = true)
Expand Down
105 changes: 88 additions & 17 deletions src/main/java/org/cyclonedx/util/LicenseResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private LicenseResolver() {
* @return a LicenseChoice object if resolution was successful, or null if unresolved
*/
public static LicenseChoice resolve(final String licenseString) {
return resolve(licenseString, true);
return resolve(licenseString, new LicenseTextSettings(true, LicenseEncoding.BASE64));
}

/**
Expand All @@ -55,17 +55,32 @@ public static LicenseChoice resolve(final String licenseString) {
* @return a LicenseChoice object if resolution was successful, or null if unresolved
*/
public static LicenseChoice resolve(final String licenseString, final boolean includeLicenseText) {
return resolve(licenseString, new LicenseTextSettings( includeLicenseText, LicenseEncoding.BASE64));
}

static LicenseChoice resolve(final String licenseString, final boolean includeLicenseText, final ObjectMapper mapper) {
return resolve(licenseString, new LicenseTextSettings( includeLicenseText, LicenseEncoding.BASE64), mapper);
}

/**
* Attempts to resolve the specified license string via SPDX license identifier and expression
* parsing first. If SPDX resolution is not successful, the method will attempt fuzzy matching.
* @param licenseString the license string to resolve
* @param licenseTextSettings specifies settings regarding the entire text of the resolved license
* @return a LicenseChoice object if resolution was successful, or null if unresolved
*/
public static LicenseChoice resolve(final String licenseString, final LicenseTextSettings licenseTextSettings) {
final ObjectMapper mapper = new ObjectMapper();

return resolve(licenseString, includeLicenseText, mapper);
return resolve(licenseString, licenseTextSettings, mapper);
}

static LicenseChoice resolve(final String licenseString, final boolean includeLicenseText, final ObjectMapper mapper) {
static LicenseChoice resolve(final String licenseString, final LicenseTextSettings licenseTextSettings, final ObjectMapper mapper) {
try {
LicenseChoice licenseChoice = resolveLicenseString(licenseString, includeLicenseText, mapper);
LicenseChoice licenseChoice = resolveLicenseString(licenseString, licenseTextSettings, mapper);

if (licenseChoice == null) {
licenseChoice = resolveFuzzyMatching(licenseString, includeLicenseText, mapper);
licenseChoice = resolveFuzzyMatching(licenseString, licenseTextSettings, mapper);
}
return licenseChoice;
} catch (IOException ex) {
Expand All @@ -77,12 +92,12 @@ static LicenseChoice resolve(final String licenseString, final boolean includeLi
* Given an SPDX license ID or expression, this method will resolve the license(s) and
* return a LicenseChoice object.
* @param licenseString the license string to resolve
* @param includeLicenseText specifies is the resolved license will include the entire text of the license
* @param licenseTextSettings specifies settings regarding the entire text of the resolved license
* @param mapper is to provide a Jackson ObjectMapper
* @return a LicenseChoice object if resolved, or null
* @throws IOException an exception while parsing the license string
*/
private static LicenseChoice resolveLicenseString(String licenseString, boolean includeLicenseText, final ObjectMapper mapper)
private static LicenseChoice resolveLicenseString(String licenseString, LicenseTextSettings licenseTextSettings, final ObjectMapper mapper)
throws IOException
{
final InputStream is = LicenseResolver.class.getResourceAsStream("/licenses/licenses.json");
Expand All @@ -95,9 +110,9 @@ private static LicenseChoice resolveLicenseString(String licenseString, boolean
final String primaryLicenseUrl = (licenseDetail.seeAlso != null && !licenseDetail.seeAlso.isEmpty()) ? licenseDetail.seeAlso.get(0) : null;

if (licenseString.trim().equalsIgnoreCase(licenseDetail.licenseId)) {
return createLicenseChoice(licenseDetail.licenseId, primaryLicenseUrl, licenseDetail.isDeprecatedLicenseId, includeLicenseText);
return createLicenseChoice(licenseDetail.licenseId, primaryLicenseUrl, licenseDetail.isDeprecatedLicenseId, licenseTextSettings);
} else if (licenseString.trim().equalsIgnoreCase(licenseDetail.name)) {
return createLicenseChoice(licenseDetail.licenseId, primaryLicenseUrl, licenseDetail.isDeprecatedLicenseId, includeLicenseText);
return createLicenseChoice(licenseDetail.licenseId, primaryLicenseUrl, licenseDetail.isDeprecatedLicenseId, licenseTextSettings);
} else {

if (licenseDetail.isDeprecatedLicenseId) {
Expand All @@ -110,7 +125,7 @@ private static LicenseChoice resolveLicenseString(String licenseString, boolean
final String licenseStringModified = urlNormalize(licenseString);

if (licenseStringModified.equalsIgnoreCase(urlNormalize(url))) {
return createLicenseChoice(licenseDetail.licenseId, url, licenseDetail.isDeprecatedLicenseId, includeLicenseText);
return createLicenseChoice(licenseDetail.licenseId, url, licenseDetail.isDeprecatedLicenseId, licenseTextSettings);
}
}
}
Expand All @@ -125,11 +140,11 @@ private static LicenseChoice resolveLicenseString(String licenseString, boolean
/**
* Attempts to perform high-confidence license resolution with unstructured text as input.
* @param licenseString the license string (not the actual license text)
* @param includeLicenseText specifies is the resolved license will include the entire text of the license
* @param licenseTextSettings specifies settings regarding the entire text of the resolved license
* @param mapper is to provide a Jackson ObjectMapper
* @return a LicenseChoice object if resolved, otherwise null
*/
private static LicenseChoice resolveFuzzyMatching(final String licenseString, final boolean includeLicenseText, final ObjectMapper mapper) throws IOException {
private static LicenseChoice resolveFuzzyMatching(final String licenseString, final LicenseTextSettings licenseTextSettings, final ObjectMapper mapper) throws IOException {
if (licenseString == null) {
return null;
}
Expand All @@ -148,7 +163,7 @@ private static LicenseChoice resolveFuzzyMatching(final String licenseString, fi
lc.setExpression(licenseMapping.exp);
return lc;
} else {
return createLicenseChoice(licenseMapping.exp, null, false, includeLicenseText);
return createLicenseChoice(licenseMapping.exp, null, false, licenseTextSettings);
}
}
}
Expand All @@ -167,26 +182,82 @@ private static String urlNormalize(String input) {
.replace("http://", "");
}

private static LicenseChoice createLicenseChoice(String licenseId, String primaryLicenseUrl, boolean isDeprecatedLicenseId, boolean includeLicenseText) throws IOException {
private static LicenseChoice createLicenseChoice(String licenseId, String primaryLicenseUrl, boolean isDeprecatedLicenseId, LicenseTextSettings licenseTextSettings ) throws IOException {
final LicenseChoice choice = new LicenseChoice();
final License license = new License();
license.setId(licenseId);
license.setUrl(primaryLicenseUrl);
if (!isDeprecatedLicenseId && includeLicenseText) {
if (!isDeprecatedLicenseId && licenseTextSettings.isTextIncluded()) {
final InputStream is = LicenseResolver.class.getResourceAsStream("/licenses/" + licenseId + ".txt");
if (is != null) {
final String text = IOUtils.toString(is, StandardCharsets.UTF_8);
final AttachmentText attachment = new AttachmentText();
attachment.setContentType("plain/text");
attachment.setEncoding("base64");
attachment.setText(Base64.getEncoder().encodeToString(text.getBytes()));
switch(licenseTextSettings.getEncoding()){
case NONE:
attachment.setEncoding(null);
attachment.setText(text);
break;
case BASE64:
attachment.setEncoding(licenseTextSettings.getEncoding().toString());
attachment.setText(Base64.getEncoder().encodeToString(text.getBytes()));
break;
default:
throw new IllegalArgumentException("Unhandled License Encoding:" + licenseTextSettings.getEncoding().toString() );
}
license.setLicenseText(attachment);
}
}
choice.addLicense(license);
return choice;
}

/**
* Lists possible choices for license text encoding
*/
public enum LicenseEncoding{
BASE64("base64"),
NONE("none");

private String encodingName;

/**
* Constructor with a string representation of the enum value
* @param encodingName The string representation of the enum value
*/
LicenseEncoding(String encodingName) {
this.encodingName = encodingName;
}
public String toString() {
return encodingName;
}
}

/**
* Data class aggregating settings for license text output
*/
public static class LicenseTextSettings {
public boolean isTextIncluded;
public LicenseEncoding encoding;

public LicenseTextSettings(boolean includeLicenseText, LicenseEncoding encoding) {
this.isTextIncluded = includeLicenseText;
this.encoding = encoding;
}
public boolean isTextIncluded() {
return isTextIncluded;
}
public void setTextIncluded(boolean include) {
this.isTextIncluded = include;
}
public LicenseEncoding getEncoding() {
return encoding;
}
public void setEncoding(LicenseEncoding encoding) {
this.encoding = encoding;
}
}

private static class LicenseDetail {
public String reference;
public boolean isDeprecatedLicenseId;
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/org/cyclonedx/util/LicenseResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

public class LicenseResolverTest {

Expand All @@ -49,6 +50,24 @@ public void resolveTestSingleLicense() {
assertNotNull(c1.getLicenses().get(0).getAttachmentText().getText());
assertEquals("plain/text", c1.getLicenses().get(0).getAttachmentText().getContentType());
assertEquals("base64", c1.getLicenses().get(0).getAttachmentText().getEncoding());

LicenseResolver.LicenseTextSettings textSettings = new LicenseResolver.LicenseTextSettings( true, LicenseResolver.LicenseEncoding.NONE);
LicenseChoice c2 = LicenseResolver.resolve("GPL-3.0-only", textSettings);
assertEquals(1, c2.getLicenses().size());
assertEquals("GPL-3.0-only", c2.getLicenses().get(0).getId());
assertEquals("https://www.gnu.org/licenses/gpl-3.0-standalone.html", c2.getLicenses().get(0).getUrl());
assertNotNull(c2.getLicenses().get(0).getAttachmentText().getText());
assertEquals("plain/text", c2.getLicenses().get(0).getAttachmentText().getContentType());
assertNull(c2.getLicenses().get(0).getAttachmentText().getEncoding());

textSettings = new LicenseResolver.LicenseTextSettings( true, LicenseResolver.LicenseEncoding.BASE64);
LicenseChoice c3 = LicenseResolver.resolve("GPL-3.0-only", textSettings);
assertEquals(1, c3.getLicenses().size());
assertEquals("GPL-3.0-only", c3.getLicenses().get(0).getId());
assertEquals("https://www.gnu.org/licenses/gpl-3.0-standalone.html", c3.getLicenses().get(0).getUrl());
assertNotNull(c3.getLicenses().get(0).getAttachmentText().getText());
assertEquals("plain/text", c3.getLicenses().get(0).getAttachmentText().getContentType());
assertEquals("base64", c3.getLicenses().get(0).getAttachmentText().getEncoding());
}

@Test
Expand Down

0 comments on commit a5ea36b

Please sign in to comment.