com.metaeffekt.artifact.analysis.workbench.PackageLicenseTransformer Maven / Gradle / Ivy
/*
* Copyright 2021-2024 the original author or authors.
*
* 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 com.metaeffekt.artifact.analysis.workbench;
import com.metaeffekt.artifact.analysis.utils.InventoryUtils;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.terms.model.NormalizationMetaData;
import com.metaeffekt.artifact.terms.model.TermsMetaData;
import org.metaeffekt.core.inventory.processor.model.Artifact;
import org.metaeffekt.core.inventory.processor.model.Inventory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
public class PackageLicenseTransformer {
private static final Logger LOG = LoggerFactory.getLogger(PackageLicenseTransformer.class);
public static final String PACKAGE_SPECIFIED_LICENSE = "Package Specified License";
public static final String PACKAGE_SPECIFIED_LICENSE_MAPPED = "Package Specified License (mapped)";
public static final String PACKAGE_SPECIFIED_LICENSE_UNMAPPED = "Package Specified License (unknown)";
public static final String COMPONENT_SPECIFIED_LICENSE = "Component Specified License";
public static final String COMPONENT_SPECIFIED_LICENSE_MAPPED = "Component Specified License (mapped)";
public static final String COMPONENT_SPECIFIED_LICENSE_UNMAPPED = "Component Specified License (unknown)";
private Set unmapped = new HashSet<>();
// FIXME: move to inventory utils
public static String joinDisjunctiveLicenses(Collection licenses) {
return licenses.stream().collect(Collectors.joining(" + "));
}
public Inventory transformPackageLicenses(Inventory inventory) {
if (inventory != null) {
transform(inventory);
}
return inventory;
}
private Inventory transform(Inventory inventory) {
for (Artifact artifact : inventory.getArtifacts()) {
transformPackageLicenses(artifact);
}
return inventory;
}
public void transformPackageLicenses(Artifact artifact) {
transform(artifact, PACKAGE_SPECIFIED_LICENSE, PACKAGE_SPECIFIED_LICENSE_MAPPED, PACKAGE_SPECIFIED_LICENSE_UNMAPPED);
}
public void transformComponentLicenses(Artifact artifact) {
transform(artifact, COMPONENT_SPECIFIED_LICENSE, COMPONENT_SPECIFIED_LICENSE_MAPPED, COMPONENT_SPECIFIED_LICENSE_UNMAPPED);
}
private void transform(Artifact artifact, String attributeKey, String attributeKeyMapped, String attributeKeyUnmapped) {
List artifactUnmappedLicenses = new ArrayList<>();
String specifiedPackageLicenses = artifact.get(attributeKey);
if (specifiedPackageLicenses == null) {
// FIXME: needs consolidation; node modules use a different attribute key
specifiedPackageLicenses = artifact.get("Package Specified Licenses");
}
if (specifiedPackageLicenses != null) {
String preprocessLicenseExpression = preprocessLicenseExpression(specifiedPackageLicenses);
String licenseExpression = transformLicenseExpression(preprocessLicenseExpression, artifactUnmappedLicenses);
licenseExpression = postprocessLicenseExpression(licenseExpression);
isValid(licenseExpression, artifact);
artifact.set(attributeKeyMapped, licenseExpression);
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("%-60s --> (%s)", specifiedPackageLicenses, preprocessLicenseExpression));
LOG.debug(String.format("%-60s --> %s", "(" + preprocessLicenseExpression + ")", licenseExpression));
}
// log unmapped to warn to make prominent
if (!artifactUnmappedLicenses.isEmpty()) {
String unmappedLicense = InventoryUtils.joinLicenses(artifactUnmappedLicenses);
artifact.set(attributeKeyUnmapped, unmappedLicense);
LOG.info(String.format("%-60s --> (%s)", specifiedPackageLicenses, preprocessLicenseExpression));
LOG.info(String.format("%-60s --> %s", "(" + preprocessLicenseExpression + ")", licenseExpression));
LOG.warn(String.format("%64s %s", "unmapped licenses:", unmappedLicense));
unmapped.addAll(artifactUnmappedLicenses);
}
}
}
private String postprocessLicenseExpression(String license) {
license = license.replace("XX;;XX", ",");
license = license.replace("ANY + ", "");
return license;
}
public String preprocessLicenseExpression(String specifiedPackageLicenses) {
if (StringUtils.isEmpty(specifiedPackageLicenses))
return specifiedPackageLicenses;
// FIXME: resolve source of strange license patterns; remove here, when case is clear
if ("LGPL-3.0-or-later, or, GPL-2.0-or-later, or, (LGPL-3.0-or-later, GPL-2.0-or-later)".equals(specifiedPackageLicenses)) {
// the secondary part is intrinsically wrong or requires a decomposition of the software; since the first
// part already allows a selection of either licenses. Therefore, this is not regarded a fault; rather an
// option less.
specifiedPackageLicenses = "LGPL-3.0-or-later, or, GPL-2.0-or-later";
}
if ("BSD-3-Clause GPL-2.0-or-later".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "BSD-3-Clause, GPL-2.0-or-later";
}
if ("BSD or Apache License, Version 2.0".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "BSD + Apache-2.0";
}
if ("GPL-2.0-or-later LGPL-2.1-or-later".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "GPL-2.0-or-later, LGPL-2.1-or-later";
}
if ("MIT BSD GPL2+".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MIT, BSD, GPL2+";
}
if ("MPL-2.0 GPL-2.0-or-later".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MPL-2.0, GPL-2.0-or-later";
}
if ("LGPLv2.1/MIT".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "LGPLv2.1, MIT";
}
if ("[\"AFLv2.1\",\"BSD\"]".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "AFLv2.1, BSD";
}
if ("GPL-3.0+, with, exception".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "GNU General Public License 3.0 (or any later version, with exceptions)";
}
if ("ISC, AND, (, BSD-3-Clause, OR, MIT, )".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "ISC, BSD-3-Clause or MIT";
}
if ("[\"AFLv2.1\",\"BSD\"]".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "AFLv2.1, BSD";
}
if ("Public, Domain".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "Public Domain";
}
if ("GPL-2.0-only, WITH, Classpath-exception-2.0".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "GNU General Public License 2.0 (with classpath exception)";
}
if ("(IBM and GPLv2+) or (EPL-2.0 and GPLv2+)".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "IBM or EPL-2.0, GPLv2+";
}
if ("2-clause, BSD-like, license".equals(specifiedPackageLicenses)) {
specifiedPackageLicenses = "BSD 2-Clause License, BSD alike";
}
if ("GPLv2+ BSD LGPL-2.1+ LGPL-2.0+".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "GPLv2+ and BSD and LGPL-2.1+ and LGPL-2.0+";
}
if ("GPLv2+, LGPLv2+, MIT".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "GPLv2+ and LGPLv2+ and MIT";
}
if ("GPLv3+, GFDL".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "GPLv3+ and GFDL";
}
if ("LGPLv2+ Public Domain".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "LGPLv2+ and Public Domain";
}
if ("(MIT and BSD and BSD with advertising) and GPLv2".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MIT, BSD, BSD with advertising, GPLv2";
}
if ("(MIT or AFL), (MIT or GPLv2)".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MIT or AFL, MIT or GPLv2";
}
if ("(MIT or AFL), (MIT or GPLv2)".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MIT or AFL, MIT or GPLv2";
}
if ("(MIT and BSD and BSD with advertising) and GPLv2".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MIT, BSD, BSD with advertising, GPLv2";
}
if ("(MIT or AFL) and (MIT or GPLv2)".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MIT or AFL, MIT or GPLv2";
}
if ("(MIT or AFL) and (MIT or GPLv2)".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "MIT or AFL, MIT or GPLv2";
}
if ("BSD-3-Clause, AND, BSD-2-Clause, AND, ISC, AND, Beerware, AND, Public, Domain".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "BSD 3-Clause, BSD-2-Clause, ISC, Beerware, Public Domain";
}
if ("Public, Domain".equalsIgnoreCase(specifiedPackageLicenses)) {
specifiedPackageLicenses = "Public Domain";
}
specifiedPackageLicenses = specifiedPackageLicenses.replace("Redistributable, no modification permitted", "Redistributable (no modification permitted)");
specifiedPackageLicenses = specifiedPackageLicenses.replace("or later", "XX+XX");
specifiedPackageLicenses = specifiedPackageLicenses.replace("or any later", "XX++XX");
specifiedPackageLicenses = removeOutterBrackets(specifiedPackageLicenses);
specifiedPackageLicenses = specifiedPackageLicenses.replace("Notice and License", "NoticeXXANDXXLicense");
specifiedPackageLicenses = specifiedPackageLicenses.replace(", OR, ", " + ");
specifiedPackageLicenses = specifiedPackageLicenses.replace(" OR ", " + ");
specifiedPackageLicenses = specifiedPackageLicenses.replace(", AND, ", ", ");
specifiedPackageLicenses = specifiedPackageLicenses.replace(" AND ", ", ");
specifiedPackageLicenses = specifiedPackageLicenses.replace(", or, ", " + ");
specifiedPackageLicenses = specifiedPackageLicenses.replace(" or ", " + ");
specifiedPackageLicenses = specifiedPackageLicenses.replace(", and, ", ", ");
specifiedPackageLicenses = specifiedPackageLicenses.replace(" and ", ", ");
specifiedPackageLicenses = specifiedPackageLicenses.replace("XX+XX", "or later");
specifiedPackageLicenses = specifiedPackageLicenses.replace("XX++XX", "or any later");
specifiedPackageLicenses = specifiedPackageLicenses.replace("XXANDXX", " and ");
// cut off dangling AND at the end - artifact
if (specifiedPackageLicenses.endsWith(", AND")) {
specifiedPackageLicenses = specifiedPackageLicenses.substring(0, 5);
}
return specifiedPackageLicenses;
}
public String transformLicenseExpression(String license, List artifactUnmappedLicenses) {
List rawLicenses = InventoryUtils.tokenizeLicense(license, false, true);
List mappedLicenses = mapLicenses(rawLicenses, artifactUnmappedLicenses);
return InventoryUtils.joinLicenses(mappedLicenses);
}
public void dumpUnmapped() {
unmapped.forEach(l -> LOG.warn("Unmapped license {}", l));
}
public List mapLicenses(Collection licenses) {
if (licenses == null) return null;
return mapLicenses(licenses, new HashSet<>());
}
private List mapLicenses(Collection licenses, Collection unmapped) {
List mappedLicenses = new ArrayList<>();
for (String license : licenses) {
license = license.trim();
license = removeOutterBrackets(license);
final List licenseParts = InventoryUtils.tokenizeLicense(license, false, true);
for (String licensePart : licenseParts) {
final String[] atomicLicenses = licensePart.split(" \\+ ");
// transform atomic parts
final List multiLicense = new ArrayList<>();
for (String atomicLicense : atomicLicenses) {
multiLicense.add(applyMapping(atomicLicense, unmapped));
}
mappedLicenses.add(joinDisjunctiveLicenses(multiLicense));
}
}
return mappedLicenses;
}
private String removeOutterBrackets(String license) {
if (StringUtils.isEmpty(license)) return license;
if (license.startsWith("(") && license.endsWith(")") &&
org.springframework.util.StringUtils.countOccurrencesOf(license, "(") == 1 &&
org.springframework.util.StringUtils.countOccurrencesOf(license, ")") == 1) {
license = license.substring(1, license.length() - 1);
}
return license;
}
private String applyMapping(String license, Collection unmapped) {
NormalizationMetaData normalizationMetaData = InventoryUtils.getNormalizationMetaData();
// try resolve by canonical name
TermsMetaData tmd = normalizationMetaData.getTermsMetaData(license);
if (tmd != null) {
return tmd.getCanonicalName();
}
// use find (alternative names)
TermsMetaData tmdMapping = normalizationMetaData.findTermsMetaData(license);
if (tmdMapping != null) {
return tmdMapping.getCanonicalName();
}
// use specific find (local)
TermsMetaData tmdMappingAlternative = findMyShortNameSPDXorOtherId(license);
if (tmdMappingAlternative != null) {
return tmdMappingAlternative.getCanonicalName();
}
TermsMetaData tmdShortNameMapped = normalizationMetaData.findByShortName(license);
if (tmdShortNameMapped != null) {
return tmdShortNameMapped.getCanonicalName();
}
// track unmapped license
unmapped.add(postprocessLicenseExpression(license));
return license;
}
private TermsMetaData findMyShortNameSPDXorOtherId(String name) {
NormalizationMetaData normalizationMetaData = InventoryUtils.getNormalizationMetaData();
final String shortName = name.replace(" ", "-");
for (TermsMetaData termsMetaData : normalizationMetaData.getLicenseMetaDataMap().values()) {
if (name.equalsIgnoreCase(termsMetaData.getSpdxIdentifier())) {
return termsMetaData;
}
}
for (TermsMetaData termsMetaData : normalizationMetaData.getLicenseMetaDataMap().values()) {
if (shortName.equalsIgnoreCase(termsMetaData.getShortName())) {
return termsMetaData;
}
}
// FIXME: capture by addition name list in terms metadata yaml files
if (name.equals("OFL")) return findMyShortNameSPDXorOtherId("OFL-?");
if (name.equals("LGPLv2")) return findMyShortNameSPDXorOtherId("LGPL-2.0");
if (name.equals("GPL+")) return findMyShortNameSPDXorOtherId("GPL-?+");
if (name.equals("GPLv2")) return findMyShortNameSPDXorOtherId("GPL-2.0");
if (name.equals("GPLv2 with exception")) return findMyShortNameSPDXorOtherId("GPL-2.0-exceptions");
if (name.equals("GPLv2+ with exception")) return findMyShortNameSPDXorOtherId("GPL-2.0+-exceptions");
if (name.equals("GPLv2 with exceptions")) return findMyShortNameSPDXorOtherId("GPL-2.0-exceptions");
if (name.equals("GPLv2+ with exceptions")) return findMyShortNameSPDXorOtherId("GPL-2.0+-exceptions");
if (name.equals("LGPLv2+ with exceptions")) return findMyShortNameSPDXorOtherId("LGPL-2.0+-exceptions");
if (name.equals("LGPLv2.1+ with exceptions")) return findMyShortNameSPDXorOtherId("LGPL-2.1+-exceptions");
if (name.equals("GPLv3 with exception")) return findMyShortNameSPDXorOtherId("GPL-3.0-exceptions");
if (name.equals("GPLv3+ with exception")) return findMyShortNameSPDXorOtherId("GPL-3.0+-exceptions");
if (name.equals("GPLv3 with exceptions")) return findMyShortNameSPDXorOtherId("GPL-3.0-exceptions");
if (name.equals("GPLv3+ with exceptions")) return findMyShortNameSPDXorOtherId("GPL-3.0+-exceptions");
if (name.equals("LGPLv3+ with exceptions")) return findMyShortNameSPDXorOtherId("LGPL-3.0+-exceptions");
if (name.equals("BSD")) return findMyShortNameSPDXorOtherId("BSD-?");
if (name.equals("BSD with advertising")) return findMyShortNameSPDXorOtherId("BSD-?"); // despite there is more information available we map to BSD (undefined)
if (name.equals("Lucida")) return findMyShortNameSPDXorOtherId("Lucida Legal Notice");
if (name.equals("JasPer")) return findMyShortNameSPDXorOtherId("JasPer-?");
if (name.equals("OpenLDAP")) return findMyShortNameSPDXorOtherId("OLDAP-?");
if (name.equals("Boost")) return findMyShortNameSPDXorOtherId("BSL-1.0");
if (name.equals("UCD")) return findMyShortNameSPDXorOtherId("UCD-TOU");
if (!name.contains("-?")) {
TermsMetaData resolveUnspecific = findMyShortNameSPDXorOtherId(shortName + "-?");
if (resolveUnspecific != null) {
return resolveUnspecific;
}
}
return null;
}
public void isValid(String licenseExpression, Artifact artifact) {
if (!(org.apache.commons.lang3.StringUtils.countMatches(licenseExpression, "(") == org.apache.commons.lang3.StringUtils.countMatches(licenseExpression, ")"))) {
LOG.error("Odd number of brackets found in {}", artifact.getId());
}
if (licenseExpression.matches(".*\\(\\).*")) {
LOG.warn("Empty brackets found in {}", artifact.getId());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy