All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.metaeffekt.artifact.analysis.workbench.PackageLicenseTransformer Maven / Gradle / Ivy

There is a newer version: 0.126.0
Show newest version
/*
 * 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