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

com.metaeffekt.artifact.terms.model.ScanResultPart Maven / Gradle / Ivy

The 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.terms.model;

import org.metaeffekt.core.inventory.InventoryUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Manages the scan result of a single scan target.
 */
public class ScanResultPart {

    /**
     * Matched {@link TermsMetaData} instances.
     */
    private List matches = new ArrayList<>();

    private List nameMatches = new ArrayList<>();

    private List licenseMatches = new ArrayList<>();

    /**
     * Partially matched {@link TermsMetaData} instances.
     */
    private List partialMatches = new ArrayList<>();

    /**
     * {@link TermsMetaData} instances that were explicitly excluded because of an excluded match.
     */
    private List excludedMatches = new ArrayList<>();

    /**
     * All match items of the scan.
     */
    private List matchItems = new ArrayList<>();

    /**
     * Filters the licenseMetaDataList. If a category is covered by a specific {@link TermsMetaData} instance all
     * unspecific results are removed.
     */
    public void filterUnspecific() {
        Set coveredCategories = matches.stream().
                filter(lmd -> !lmd.isUnspecific()).
                map(TermsMetaData::getCategory).
                collect(Collectors.toSet());

        Set toBeRemoved = matches.stream().
                filter(lmd -> lmd.isUnspecific()).
                filter(lmd -> coveredCategories.contains(lmd.getCategory())).
                collect(Collectors.toSet());

        matches.removeAll(toBeRemoved);
    }

    /**
     * Filters the licenseMetaDataList. If a license allows later versions of a given license, the version specific licenses
     * are removed.
     */
    public void filterSpecific() {
        Set coveredCategories = matches.stream().
                filter(lmd -> lmd.allowLaterVersions()).
                map(TermsMetaData::getCanonicalName).
                collect(Collectors.toSet());

        Set toBeRemoved = matches.stream().
                filter(lmd -> !lmd.allowLaterVersions()).
                filter(lmd -> coveredCategories.contains(lmd.getCanonicalName() + " (or any later version)")).
                collect(Collectors.toSet());

        matches.removeAll(toBeRemoved);
    }

    /**
     * Filters the licenseMetaDataList. Retains licenses with a specified name.
     */
    public void filterUnnamed() {
        Set coveredLicenses = matches.stream().
                map(lmd -> lmd.getNamedEquivalence()).
                filter(Objects::nonNull).
                collect(Collectors.toSet());

        Set toBeRemoved = matches.stream().
                filter(lmd -> coveredLicenses.contains(lmd.getCanonicalName())).
                collect(Collectors.toSet());

        matches.removeAll(toBeRemoved);
    }

    public void filterConflicts(String category) {
        Boolean licenseMatchCategory = false;

        for (TermsMetaData licenseMatch : licenseMatches) {
            if (licenseMatch.getCategory().equals(category)) licenseMatchCategory = true;
        }
        if (licenseMatchCategory == true) {
            for (TermsMetaData nameMatch : nameMatches) {
                if (nameMatch.getCategory().equals(category) && !(licenseMatches.contains(nameMatch))) {
                    matches.remove(nameMatch);

                }
            }
        }
    }

    public List getMatchedTerms() {
        return matches.stream().filter(lmd -> !excludedMatches.contains(lmd)).
                map(TermsMetaData::getCanonicalName).collect(Collectors.toList());
    }

    public List getNameMatchedTerms() {
        return nameMatches.stream().filter(lmd -> !excludedMatches.contains(lmd)).
                map(TermsMetaData::getCanonicalName).collect(Collectors.toList());
    }

    public List getTextMatchedTerms() {
        return licenseMatches.stream().filter(lmd -> !excludedMatches.contains(lmd)).
                map(TermsMetaData::getCanonicalName).collect(Collectors.toList());
    }

    public List getPartialMatchedTerms() {
        return partialMatches.stream().distinct().
                map(TermsMetaData::getCanonicalName).sorted(String::compareTo).collect(Collectors.toList());
    }

    public List getExcludeMatchedLicenses() {
        return excludedMatches.stream().distinct().
                map(TermsMetaData::getCanonicalName).sorted(String::compareTo).collect(Collectors.toList());
    }

    public void cleanExcludedMatches() {
        excludedMatches.clear();
    }

    public void cleanLicenseMatches() {
        licenseMatches.clear();
    }

    private void addMatch(TermsMetaData licenseMetaData) {
        if (!matches.contains(licenseMetaData)) {
            matches.add(licenseMetaData);
        }
    }

    public void addNameMatch(TermsMetaData licenseMetaData) {
        if (!matches.contains(licenseMetaData)) {
            matches.add(licenseMetaData);
        }
        if (!nameMatches.contains(licenseMetaData)) {
            nameMatches.add(licenseMetaData);
        }
    }

    public void addLicenseMatch(TermsMetaData licenseMetaData) {
        if (!matches.contains(licenseMetaData)) {
            matches.add(licenseMetaData);
        }
        if (!licenseMatches.contains(licenseMetaData)) {
            licenseMatches.add(licenseMetaData);
        }
    }

    public synchronized void merge(ScanResultPart other) {
        for (TermsMetaData licenseMetaData : other.matches) {
            addMatch(licenseMetaData);
        }
        for (TermsMetaData licenseMetaData : other.licenseMatches) {
            addLicenseMatch(licenseMetaData);
        }
        for (TermsMetaData licenseMetaData : other.nameMatches) {
            addNameMatch(licenseMetaData);
        }
        for (TermsMetaData licenseMetaData : other.partialMatches) {
            addPartialMatch(licenseMetaData);
        }
        for (TermsMetaData licenseMetaData : other.excludedMatches) {
            addExcludedMatch(licenseMetaData);
        }
        this.matchItems.addAll(other.getMatchItems());
    }

    public void filterIgnored() {
        Set toBeRemoved = matches.stream().
                filter(lmd -> lmd.ignore()).
                collect(Collectors.toSet());
        matches.removeAll(toBeRemoved);
    }

    public void process(NormalizationMetaData normalizationMetaData, boolean enableIgnore, boolean enableCombine) {
        final Set toBeRemovedLmds = new HashSet<>();

        //filter Name Match / EvidenceMatch Conflict
        String[] categories = {"BSD"};
        for (String cat : categories) {
            filterConflicts(cat);
        }


        if (enableCombine) {
            combine(normalizationMetaData, toBeRemovedLmds);
        }

        // filter those that are not an actual license
        if (enableIgnore) {
            filterIgnored();
        }

        if (enableCombine) {
            combine(normalizationMetaData, toBeRemovedLmds);
        }

        // filter unnamed where named equivalent licenses are used
        filterUnnamed();

        if (enableCombine) {
            combine(normalizationMetaData, toBeRemovedLmds);
        }

        // filter those covered by 'or any later version'
        filterSpecific();

        // filter those with unspecified version if a version was otherwise identified
        filterUnspecific();

        // FIXME: we may need to have as many passes as required to work down the chain
        if (enableCombine) {
            combine(normalizationMetaData, toBeRemovedLmds);
            combine(normalizationMetaData, toBeRemovedLmds);
            combine(normalizationMetaData, toBeRemovedLmds);
        }

        matches.removeAll(toBeRemovedLmds);

        if (enableCombine) {
            Map> map = buildCategoryMap(normalizationMetaData);
            filterUnspecificFromCategoryMap(normalizationMetaData, map);
            resolveSpecificInMultiLicenseExpressions(normalizationMetaData, map, false);

            // in the second pass we also resolve the ANY license if it is unique
            resolveSpecificInMultiLicenseExpressions(normalizationMetaData, map, true);
        }
    }

    protected void resolveSpecificInMultiLicenseExpressions(NormalizationMetaData normalizationMetaData, Map> map, boolean resolveAny) {
        final Set toBeRemovedLmds = new HashSet<>();
        final Set toBeAddedLmds = new HashSet<>();

        final Set noMarkerMatches = matches.stream().filter(lmd -> !lmd.isMarker()).collect(Collectors.toSet());

        for (TermsMetaData lmd : matches) {
            boolean replacement = false;
            String canonicalName = lmd.getCanonicalName();

            if (canonicalName.contains(" + ")) {
                final List parts = InventoryUtils.tokenizeLicense(lmd.getCanonicalName(), true, false);
                final List partLicenseMetaData = new ArrayList<>();
                for (String part : parts) {
                    final TermsMetaData licenseMetaData = normalizationMetaData.getTermsMetaData(part);
                    if (licenseMetaData != null && licenseMetaData.isUnspecific()) {
                        final Set representatives = map.get(licenseMetaData.getCategory());
                        if (representatives != null && representatives.size() == 1) {
                            String representative = representatives.iterator().next();
                            canonicalName = canonicalName.replace(part + " +", representative + " +");
                            canonicalName = canonicalName.replace("+ " + part, "+ " + representative);
                            replacement = true;

                            // also add the representative (BSD alike --> BSD 3-Clause) to the list of parts covered
                            // by the (resulting) expression
                            TermsMetaData representativeLmd = normalizationMetaData.getTermsMetaData(representative);
                            if (representativeLmd != null) {
                                partLicenseMetaData.add(representativeLmd);
                            }
                        }
                    } else if (licenseMetaData == null && "ANY".equals(part)) {
                        if (noMarkerMatches.size() == 2) {
                            HashSet representativeSet = new HashSet<>(noMarkerMatches);
                            representativeSet.remove(lmd);
                            TermsMetaData representativeLmd = representativeSet.iterator().next();
                            String representative = representativeLmd.getCanonicalName();
                            canonicalName = canonicalName.replace(part + " +", representative + " +");
                            canonicalName = canonicalName.replace("+ " + part, "+ " + representative);

                            replacement = true;
                            partLicenseMetaData.remove(representativeLmd);
                        }
                    }

                    if (licenseMetaData != null) {
                        partLicenseMetaData.add(licenseMetaData);
                    }
                }

                if (replacement) {
                    toBeRemovedLmds.add(lmd);
                    toBeAddedLmds.add(findOrCreateLicenseMetaData(normalizationMetaData, true, canonicalName));
                }

                // remove those parts that are covered by the expression
                toBeRemovedLmds.addAll(partLicenseMetaData);
            }
        }

        matches.removeAll(toBeRemovedLmds);
        matches.addAll(toBeAddedLmds);

        removeLicenseOptionIfUnique(matches, normalizationMetaData);
    }

    private void removeLicenseOptionIfUnique(List licenseMetaDataList, NormalizationMetaData normalizationMetaData) {
        // since the list may include duplicates we first filter unique names
        final Set names = licenseMetaDataList.stream().filter(lmd -> !lmd.isMarker()).map(l -> l.getCanonicalName()).collect(Collectors.toSet());

        // count the lmds with ' + ' (in the lack of another attribute that defines a license option)
        long optionCount = names.stream().filter(n -> n.contains(" + ")).count();

        // in case there is only one option, we remove the "License Option" lmd as it is redundant.
        if (optionCount == 1) {
            licenseMetaDataList.remove(normalizationMetaData.getTermsMetaData("Licensing Option"));
        }
    }

    protected void filterUnspecificFromCategoryMap(NormalizationMetaData normalizationMetaData, Map> map) {
        for (Set representatives : map.values()) {
            if (representatives.size() > 1) {
                for (String cn : representatives) {
                    TermsMetaData licenseMetaData = normalizationMetaData.getTermsMetaData(cn);
                    if (licenseMetaData.isUnspecific()) {
                        representatives.remove(cn);
                        break;
                    }
                }
            }
        }
    }

    protected Map> buildCategoryMap(NormalizationMetaData normalizationMetaData) {
        Map> map = new HashMap<>();
        for (TermsMetaData lmd : matches) {
            List parts = InventoryUtils.tokenizeLicense(lmd.getCanonicalName(), true, false);
            for (String part : parts) {
                TermsMetaData licenseMetaData = normalizationMetaData.getTermsMetaData(part);
                if (licenseMetaData != null) {
                    map.computeIfAbsent(licenseMetaData.getCategory(), k -> new HashSet<>()).add(part);
                }
            }
        }
        return map;
    }

    public void combine(NormalizationMetaData normalizationMetaData, Set toBeRemoved) {
        List toBeAdded = new ArrayList<>();

        for (TermsMetaData lmd : matches) {
            if (lmd.getCombinedWith() != null) {
                for (Map.Entry combinerEntry : lmd.getCombinedWith().entrySet()) {
                    List combinerBaseLmds = mapLicenseMetaData(combinerEntry.getKey(), normalizationMetaData, false);
                    List combinerTargetLmds = mapLicenseMetaData(combinerEntry.getValue(), normalizationMetaData, true);

                    if (combinerBaseLmds.isEmpty()) {
                        throw new IllegalStateException("Cannot find base license meta data for combiner: " + combinerEntry);
                    }

                    if (matches.containsAll(combinerBaseLmds)) {
                        // mark the combiner base for removal; this entry is replaced by the target
                        toBeRemoved.addAll(combinerBaseLmds);

                        // remove this entry
                        toBeRemoved.add(lmd);

                        // add the target
                        toBeAdded.addAll(combinerTargetLmds);

                        // ensure the combiner target is no longer on toBeRemoved
                        toBeRemoved.removeAll(combinerTargetLmds);
                    }
                }
            }
        }

        // ensure that no duplicates are added, by filtering toBeAdded)
        toBeAdded.removeAll(matches);
        for (TermsMetaData toAdd : toBeAdded) {
            if (!matches.contains(toAdd)) {
                matches.add(toAdd);
            }
        }
    }

    protected List mapLicenseMetaData(String combiner, NormalizationMetaData normalizationMetaData, boolean allowCreate) {
        List combinerTargetLmds = new ArrayList<>();
        String[] targets = combiner.split(",");
        for (String combinerTargetName : targets) {
            TermsMetaData combinerTarget = findOrCreateLicenseMetaData(normalizationMetaData, allowCreate, combinerTargetName);
            if (combinerTarget != null) {
                combinerTargetLmds.add(combinerTarget);
            }
        }
        return combinerTargetLmds;
    }

    private TermsMetaData findOrCreateLicenseMetaData(NormalizationMetaData normalizationMetaData, boolean allowCreate, String combinerTargetName) {
        TermsMetaData combinerTarget = normalizationMetaData.getTermsMetaData(combinerTargetName.trim());
        if (combinerTarget == null && allowCreate) {
            combinerTarget = new TermsMetaData();
            combinerTarget.setCanonicalName(combinerTargetName);
            combinerTarget.setCategory(combinerTargetName);
        }
        return combinerTarget;
    }

    public void addPartialMatch(TermsMetaData licenseMetaData) {
        partialMatches.add(licenseMetaData);
    }

    public void addExcludedMatch(TermsMetaData licenseMetaData) {
        excludedMatches.add(licenseMetaData);
    }

    public void addMatchItem(MatchItem matchItem) {
        this.matchItems.add(matchItem);
    }

    public List getMatchItems() {
        return matchItems;
    }

    public void addMatchItems(List unmatchedMatchItems) {
        matchItems.addAll(unmatchedMatchItems);
    }

    public List getMatches() {
        return matches;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy