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

com.metaeffekt.artifact.terms.model.TermsMetaData 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 com.metaeffekt.artifact.analysis.utils.FileUtils;
import com.metaeffekt.artifact.analysis.utils.InventoryUtils;
import com.metaeffekt.artifact.analysis.utils.PropertyUtils;
import com.metaeffekt.artifact.analysis.utils.SimpleIntPair;
import com.metaeffekt.artifact.analysis.utils.StringStats;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.representer.Representer;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import static com.metaeffekt.artifact.terms.model.MatchItem.MatchType.EVIDENCE_EXCLUDE;
import static com.metaeffekt.artifact.terms.model.MatchItem.MatchType.NOT_MATCHED;

/**
 * TermsMetaData captures metadata with respect to license terms, notices and exceptions.
 */
public class TermsMetaData implements Serializable {

    private static final long serialVersionUID = -1;

    private static final Logger LOG = LoggerFactory.getLogger(TermsMetaData.class);

    public static final String TYPE_MARKER = "marker";
    public static final String TYPE_EXCEPTION = "exception";
    public static final String TYPE_EXPRESSION = "expression";
    public static final String TYPE_REFERENCE = "reference";

    public static final String STATUS_NOT_APPROVED = "not approved";
    public static final String STATUS_APPROVED = "approved";

    public static final String STATUS_APPROVED_IMPLICIT = "(approved)";

    public static final Comparator COMPARATOR =
            (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.canonicalName, o2.canonicalName);

    /**
     * Canonical name as derived from the license text. Long form.
     */
    @Setter
    @Getter
    private String canonicalName;

    /**
     * License category. Used to group licenses together.
     */
    @Setter
    private String category;

    /**
     * Alternative name of the license. The names spdxIdentifier and shortName are implicitly
     * included.
     */
    @Setter
    private List alternativeNames;

    /**
     * Url to the license or license description.
     */
    @Setter
    @Getter
    private String url;

    /**
     * Metadata type. One of "marker", "exception", "license"; "terms" is the implicit default.
     */
    @Getter
    @Setter
    private String type;

    /**
     * The name of the license file. For programming convenience. The license file may
     * be placed with arbitrary name in the license folder. At parsing time the license
     * file (path) is derived from this location.
     */
    @Setter
    @Getter
    private transient String licenseFile;

    /**
     * The name of the readme file. For programming convenience. The readme file may
     * be places with arbitrary name in the readme folder. At parsing time the readme
     * file (path) is derived from this location.
     */
    @Setter
    @Getter
    private transient String readmeFile;

    /**
     * The YAML file; only populated when read from disk.
     */
    @Setter
    @Getter
    private transient File file;

    /**
     * Aggregated from alternative names, spdx identifiers and shortname. Will be computed once.
     */
    private transient Set namesAndReferences;

    /**
     * Evidences for the license.
     */
    @Setter
    @Getter
    private Evidence evidence;

    /**
     * A license text may reference other licenses. This may then not be falsely detected.
     * References must be managed explicitly to prevent matches.
     */
    @Setter
    @Getter
    private Map references;

    /**
     * The SPDX identifier if available. Not all licenses are covered by SPDX. Licenses not covered by SPDX must not
     * receive a fallback LicenseRef identifier. LicenseRef construction may follow context specific rules.
     */
    @Setter
    @Getter
    private String spdxIdentifier;

    /**
     * In case of an expression also an SPDX expression string needs to be provided. The expression should follow
     * defined rules regarding LicenseRef composition. If not defined otherwise the ae namespace and TMD shortname
     * is to be used. The expression can then be converted to follow other context-specific conventions.
     */
    @Getter
    @Setter
    private String spdxExpression;

    /**
     * Alternative short name for the canonical name.
     */
    @Setter
    @Getter
    private String shortName;

    /**
     * Alternatives shortNames that can be used to map (not match as these are usually not precise in general text).
     */
    @Setter
    @Getter
    private Set alternativeShortNames;

    /**
     * Other ids.
     */
    @Getter
    @Setter
    private List otherIds;

    /**
     * Mapping when combined with other licenses. Can be used to model certain license
     * relationships.
     */
    @Setter
    @Getter
    private Map combinedWith;

    /**
     * The license represented is just a renamed copy of the referenced license.
     */
    @Setter
    @Getter
    private String namedEquivalence;

    /**
     * Whether the license version is unspecific (not specified).
     */
    @Setter
    @Getter
    private boolean unspecific;

    /**
     * Whether the license association with this license allows for later versions of the referenced license.
     */
    @Setter
    @Getter
    private boolean allowsLaterVersions;

    /**
     * List of references to this license. This list is collected when first evaluating all license metadata.
     */
    private final List referenceList = new ArrayList<>();

    /**
     * Boolean indicating whether the license is to be ignored.
     */
    @Getter
    @Setter
    private boolean ignore;

    /**
     * List of matches
     */
    @Setter
    @Getter
    private Masks masks;

    /**
     * Grants expressed by the license.
     */
    @Setter
    @Getter
    private Map grants;

    /**
     * List of ignored licenses during validation
     */
    @Setter
    @Getter
    private List ignoreMatches;

    /**
     * Comments
     */
    @Getter
    @Setter
    private List comments;

    /**
     * The defaultSourceCategory is decided per license. This field can be used to derive or validate
     * LicenseMetaData objects in this Inventory.
     */
    @Setter
    @Getter
    private String defaultSourceCategory;

    /**
     * Indicates whether a license notice is required for the given terms.
     */
    @Setter
    private boolean requiresAnnexNotice = true;

    /**
     * Indicates whether the license requires source code provisioning
     */
    @Getter
    @Setter
    private boolean requiresSourceCodeProvision = false;

    /**
     * Indicates whether the license requires an article in the notice
     */
    @Getter
    @Setter
    private boolean articleRequired = true;


    @Setter
    @Getter
    private String representedAs;

    /**
     * Template for the NoticeEngine
     */
    @Getter
    @Setter
    private String licenseTemplate;

    /**
     * Indicates whether the license requires a note in the Notice
     */
    @Getter
    @Setter
    private boolean requiresNote = false;

    /**
     * Indicates whether the license requires copyrights in the Notice
     */
    @Setter
    @Getter
    private Boolean requiresCopyright;

    /**
     * Indicates whether the License TermsMetaData is generated only temporary. (Only required at Notice Engine)
     */
    @Setter
    @Getter
    private boolean temporaryLMD = false;

    /**
     * Indicates whether a license requires a licenseTemplate
     */
    @Setter
    @Getter
    private Boolean requiresLicenseText;

    /**
     * Provides information for the notice
     */
    @Setter
    @Getter
    private String licenseTextRequirements;

    /**
     * Shows the revision status of a license
     */
    @Setter
    @Getter
    private List status = new ArrayList<>();

    /**
     * This List contains information about previously license naming.
     */
    @Setter
    @Getter
    private List canonicalNameHistory = new ArrayList<>();

    /**
     * Standard variable set for licenses with alternativeNames
     */
    @Setter
    @Getter
    private HashMap standardVariableSet = new HashMap<>();

    /**
     * Indicates whether the evidence of a license is expressive (Used to escape Convention Test).
     */
    private boolean expressiveEvidence;

    /**
     * Indicates a deprecated license.
     */
    @Getter
    @Setter
    private boolean deprecated;

    /**
     * displayName
     */
    @Setter
    @Getter
    private String displayName;

    /**
     * Points to the origin of the license
     */
    @Setter
    @Getter
    private String baseTerms;

    /**
     * License requires to document a copyright in the notice even if no copyright exists (copyright template)
     */
    @Setter
    @Getter
    private boolean missingCopyrightNotAllowed;

    /**
     * Provides a checklist for todos in case a note is required.
     */
    @Setter
    @Getter
    private List noteChecklist;

    /**
     * Select the notice template to use for generating a notice.
     */
    @Setter
    @Getter
    private String noticeTemplateId;

    @Getter
    @Setter
    private List expectedSpdxMatches;

    @Getter
    @Setter
    private List expectedMatches;

    @Getter
    @Setter
    private List mappingOrder;

    /**
     * Mapping texts.
     * TODO: determine when the mapping happens:
     * - pre-segmentation
     * - post-segmentation
     * - pre-normalization
     * - post-normalized
     */
    @Setter
    @Getter
    private Map mappings;

    /**
     * We use the following classification:
     * 
    *
  • permissive - matched scancodes category 'permissive'
  • *
  • copyleft
  • *
  • limited copyleft
  • *
  • weak copyleft
  • *
*/ @Getter @Setter private String classification; @Setter @Getter private Segmentation segmentation; @Setter private NormalizationMetaData normalizationMetaData = null; @Getter private List partialMatches = null; @Getter private List matchedMarkers = null; @Getter private List excludedMatches = null; /** * Currently values can be "approved" / "not approved" or null (undefined) */ @Getter private String openCoDEStatus; @Getter @Setter private String openCoDESimilarLicenseId; /** * The osiStatus can be one of "not submitted, submitted, pending, approved, withdrawn, rejected" */ @Getter @Setter private String osiStatus; @Getter @Setter private String osiCategory; @Getter @Setter private String osiRationale; @Getter @Setter private String osiSupersededBy; /** * Indicates whether the license text is publicly available. Defaults to null. Value may be derived * during parsing from other attributes. I.e. all spdx, osi and scancode licenses are implicitly publicly available. */ @Getter @Setter private Boolean publiclyAvailable; public String getCategory() { if (category == null) { return getCanonicalName(); } return category; } public List getAlternativeNames() { if (alternativeNames == null) { alternativeNames = new ArrayList<>(); } return alternativeNames; } public Boolean getExpressiveEvidence() { return expressiveEvidence; } public void setExpressiveEvidence(Boolean expressiveEvidence) {this.expressiveEvidence = expressiveEvidence;} public boolean hasVariables() { return getLicenseTemplate() != null && getLicenseTemplate().contains("{{"); } public void validate(boolean updateMatchReports) throws IOException { if (canonicalName.contains(",")) { throw new IllegalStateException("Canonical name must not contain ',': " + canonicalName); } if (ignore()) { LOG.info("Skipped ignored license meta data [{}].", getCanonicalName()); return; } validateLicenseFile(updateMatchReports); // TODO: enforce variant validation try { // validateVariants(false); } catch (Exception e) { LOG.warn(e.getMessage()); } } protected void validateVariants(boolean updateMatchReports) throws IOException { if (getFile() != null) { final File variantsFolder = new File(getFile().getParentFile(), "variants"); if (variantsFolder.exists()) { final String[] variantFiles = FileUtils.scanDirectoryForFiles(variantsFolder, "**/*"); for (String variantFile : variantFiles) { String licenseVariant = FileUtils.readFileToString(new File(variantsFolder, variantFile), "UTF-8"); validate(licenseVariant, "Variant " + variantFile, false); } } } } protected void validateLicenseFile(boolean updateMatchReports) throws IOException { if (getLicenseFile() != null) { LOG.debug("Scanning license file: {}", getLicenseFile()); final File licenseFile = new File(getLicenseFile()); final String license = FileUtils.readFileToString(licenseFile, "UTF-8"); validate(license, "Reference License " + licenseFile.getName(), updateMatchReports); } else { // only log a message when the license is really missing if (getCanonicalName().endsWith(" (undefined)")) { return; } if (getCanonicalName().endsWith(" (or any later version)")) { return; } // ignored metadata subjects need no reporting of missing license texts if (ignore()) return; // ignore markers; do not expect a text by default if (isMarker()) return; // ignore expressions; do not expect a text by default if (isExpression()) return; LOG.warn("No license text for license meta data: [{}]", getCanonicalName()); } } protected void validate(String license, String validationContext, boolean updateMatchReports) throws IOException { final FileSegmentation fileSegmentation = new FileSegmentation(license, normalizationMetaData); final File segmentsFile = new File(this.getFile().getParentFile(), ".meta/segments.txt"); if (fileSegmentation.getSegmentCount() > 1) { FileUtils.write(segmentsFile, fileSegmentation.getSegmentsString(), FileUtils.ENCODING_UTF_8); LOG.warn("More than one segment ({}) detected in license for [{}] in context [{}].", fileSegmentation.fileSegments.size(), getCanonicalName(), validationContext); // TODO: allow auditors to define the expected segments to also support multi-license cases; consider // further constraints (e.g. each segment must match an expected license) } else { if (segmentsFile.exists()) { FileUtils.forceDelete(segmentsFile); } } final StringStats normalizedLicense = fileSegmentation.mergeSegmentedText(); // produce a file for debugging purposes containing the license text after mapping/normalization if (updateMatchReports) { File file = new File(this.getFile().getParentFile(), ".meta/ae-normalized-license.txt"); FileUtils.write(file, normalizedLicense.getNormalizedString(), FileUtils.ENCODING_UTF_8); } // produce a file for debugging purposes containing the license text after mapping/normalization/masking if (updateMatchReports) { File file = new File(this.getFile().getParentFile(), ".meta/ae-normalized-masked-license.txt"); FileUtils.write(file, normalizedLicense.getNormalizedString(), FileUtils.ENCODING_UTF_8); } // validates matches and excluded matches of the license validateOthers(normalizedLicense); // licenses with multiple segments are being analyzed and processed separately and then get merged together, // licenses with one segment can be analyzed and processed as a whole ScanResultPart resultSRP = new ScanResultPart(); if (fileSegmentation.getFileSegments().size() > 1) { for (FileSegment segment : fileSegmentation.getFileSegments()) { final StringStats normalizedSegment = segment.getNormalizedContent(); final ScanResultPart segmentSRP = normalizationMetaData.doAnalyze(normalizedSegment, false, true); segmentSRP.cleanExcludedMatches(); segmentSRP.cleanLicenseMatches(); resultSRP.merge(segmentSRP); } resultSRP.process(normalizationMetaData, false, true); } else { resultSRP = normalizationMetaData.doAnalyze(normalizedLicense, false, true); } final List actualMatches = resultSRP.getMatchedTerms(); writeMatchedMarkersFile(actualMatches); // FIXME: different 'ignore' semantics (above: ignore matches; here: ignore non-unique match) if (actualMatches.isEmpty() && !ignore() && !isMarker()) { throw new IllegalStateException( String.format("License match not unique in context [%s]! No match found for license %s.", validationContext, canonicalName)); } // FIXME: how to handle markers better; currently validation on marker is not evident InventoryUtils.removeMarkers(actualMatches, normalizationMetaData); final List ignored = actualMatches.stream().filter(s -> { final TermsMetaData licenseMetaData = normalizationMetaData.getTermsMetaData(s); return licenseMetaData == null || licenseMetaData.ignore(); }).collect(Collectors.toList()); // how can these be excluded based on information in TMD instance final String queryLicense = modulateQueryLicense(); // produces a partial matches reference file and a marked html file produceReferences(updateMatchReports, resultSRP, normalizedLicense); // handling of different matching cases happens here handleMatches(validationContext, actualMatches, queryLicense); } private void validateOthers(StringStats normalizedLicense) { if (getEvidence() != null) { validateMatches(getEvidence().getMatches(), normalizedLicense); if (getEvidence().getExcludes() != null) { validateExcludeMatches(getEvidence().getExcludes(), normalizedLicense); } } if (getGrants() != null) { for (Grant grant : getGrants().values()) { validateMatches(grant.getMatches(), normalizedLicense); if (grant.getObligations() != null && grant.getObligations().values() != null) { for (Obligation obligation : grant.getObligations().values()) { if (obligation.getMatches() != null) { validateMatches(obligation.getMatches(), normalizedLicense); } } } } } if (getReferences() != null) { for (Reference reference : getReferences().values()) { validateMatches(reference.getMatches(), normalizedLicense); } } } private void produceReferences(boolean updateMatchReports, ScanResultPart resultSRP, StringStats normalizedLicense) throws IOException { // produce a reference partial match properties file Properties properties = new SortedProperties(); if (!resultSRP.getPartialMatchedTerms().isEmpty()) { properties.setProperty("partial.matches", StringUtils.toString(resultSRP.getPartialMatchedTerms())); } if (!resultSRP.getExcludeMatchedLicenses().isEmpty()) { properties.setProperty("excluded.matches", StringUtils.toString(resultSRP.getExcludeMatchedLicenses())); } final File partialFile = new File(this.getFile().getParentFile(), ".meta/partial.matches.properties"); PropertyUtils.saveProperties(partialFile, properties); // produce a marked html file for the license if (updateMatchReports) { File htmlFile = new File(this.getFile().getParentFile(), ".meta/match.html"); createMatchReportHtml(normalizedLicense, resultSRP, htmlFile, null); } } private void handleMatches(String validationContext, List actualMatches, String queryLicense) { // build the expectation locally from available data final List localExpectedMatches = new ArrayList<>(); if (this.expectedMatches != null) { localExpectedMatches.addAll(this.expectedMatches); } // at minimum - if not defined otherwise - we expect this TMD instance itself localExpectedMatches.add(canonicalName); final List ignoreMatches = getIgnoreMatches(); if (ignoreMatches != null) { actualMatches.removeAll(ignoreMatches); localExpectedMatches.removeAll(ignoreMatches); } // the startsWith compensated issues with (any later version) if (!actualMatches.isEmpty()) { actualMatches.stream().filter(s -> s.startsWith(queryLicense)).findAny().orElseThrow(() -> new IllegalStateException( String.format("License text in context [%s] contains a different license. Expected: %s. Match: %s", validationContext, queryLicense, actualMatches))); } // NOTE: // - actualMatches contain the actual // - localExpectedMatches contain the expected final List unexpectedMatches = new ArrayList<>(actualMatches); unexpectedMatches.removeAll(localExpectedMatches); final List expectedNotMatchedMatches = new ArrayList<>(localExpectedMatches); expectedNotMatchedMatches.removeAll(actualMatches); if (!unexpectedMatches.isEmpty()) { LOG.error("Subject: file://{}", getFile().getAbsolutePath()); for (String unexpectedMatch : unexpectedMatches) { // log absolute path for fast navigation of errors final TermsMetaData unexpectedTmd = normalizationMetaData.getTermsMetaData(unexpectedMatch); if (unexpectedTmd != null) { LOG.error("Matched: file://{}", unexpectedTmd.getFile().getAbsolutePath()); } } throw new IllegalStateException( String.format("Terms [%s] not matched as expected. The following matches are not expected: %s", canonicalName, unexpectedMatches)); } // we never expect that we are not matched; this is implicit until the license is ignored expectedNotMatchedMatches.remove(getCanonicalName()); if (!expectedNotMatchedMatches.isEmpty()) { LOG.error("file://{}", getFile().getAbsolutePath()); LOG.error("Matched: {}", actualMatches); throw new IllegalStateException( String.format("Terms [%s] has defined expectedMatches, but the following are not matched: %s", canonicalName, expectedNotMatchedMatches)); } } private void writeMatchedMarkersFile(List matchedTerms) { List markerList = InventoryUtils.extractMarkers(matchedTerms, normalizationMetaData); final Properties markerProperties = new SortedProperties(); markerProperties.setProperty("matched.markers", String.join(", ", markerList)); File markersFile = new File(this.getFile().getParentFile(), ".meta/matched-markers.properties"); PropertyUtils.saveProperties(markersFile, markerProperties); } private String modulateQueryLicense() { String qLicense = this.canonicalName; qLicense = qLicense.endsWith(" (or any later version)") ? qLicense.replace(" (or any later version)", "") : qLicense; qLicense = qLicense.endsWith(" (invariants)") ? qLicense.replace(" (invariants)", "") : qLicense; qLicense = qLicense.endsWith(" (no invariants)") ? qLicense.replace(" (no invariants)", "") : qLicense; return qLicense; } public String deriveId() { String id = canonicalName; if (spdxIdentifier != null) { id = spdxIdentifier; } if (shortName != null) { id = shortName; } return id; } /** * Removes alternative names, that are already by others. No alternative name is added. */ public void consolidateAlternativeNames() { if (alternativeNames == null) return; final Set strings = new HashSet<>(alternativeNames); for (String name : strings) { final StringStats normalizedName = StringStats.normalize(name, false); for (String candidate : strings) { // this prevents, that an already removed string covers another string if (alternativeNames.contains(name)) { if (!name.equals(candidate)) { final StringStats normalizedCandidate = StringStats.normalize(candidate, false); if (normalizedCandidate.contains(normalizedName, true)) { alternativeNames.remove(candidate); LOG.debug("Removed redundant alternative name '{}' for '{}'.", candidate, canonicalName); LOG.debug("'{}' covered by '{}'.", candidate, name); } } } } } } public void addOtherId(String type, String id) { if (type == null || id == null) return; if (otherIds == null) otherIds = new ArrayList<>(); otherIds.add(type.toLowerCase() + ":" + id); } public List getScanCodeIdentifiers() { List scanCodeIds = new ArrayList<>(); List otherIds = getOtherIds(); if (otherIds != null) { for (String otherId : otherIds) { if (otherId != null && otherId.matches("scancode:.*")) { String scanCodeId = otherId.replace("scancode:", "").trim(); if (StringUtils.notEmpty(scanCodeId)) { scanCodeIds.add(scanCodeId); } } } } if (scanCodeIds.isEmpty()) { // read from meta file if (getLicenseFile() != null) { final File licenseFile = new File(getLicenseFile()); final File termDefinitionDir = licenseFile.getParentFile().getParentFile(); final File scanCodeMatchProperties = new File(termDefinitionDir, ".meta/license_scancode.properties"); if (scanCodeMatchProperties.exists()) { Properties p = PropertyUtils.loadProperties(scanCodeMatchProperties); String matched = StringUtils.stripArrayBoundaries(p.getProperty("detected.licenses")); String[] split = matched.split(","); for (String s : split) { s = s.trim(); if (StringUtils.notEmpty(s)) { scanCodeIds.add(s); } } } } } return scanCodeIds; } public void addComment(String comment) { if (comment == null) return; if (comments == null) comments = new ArrayList<>(); comments.add(comment); } public void mergeExternalMetaData() throws IOException { if (isMarker()) return; if (isUnspecific()) return; if (canonicalName.contains(" + ")) return; // scancode has priority File scanCodeTmdFile = new File(getFile().getParentFile(), ".meta/scancode-tmd.yaml"); if (scanCodeTmdFile.exists()) { mergeMetaData(parseTermsMetaData(scanCodeTmdFile)); } // spdx is only supplemental File spdxTmdFile = new File(getFile().getParentFile(), ".meta/spdx-tmd.yaml"); if (spdxTmdFile.exists()) { mergeMetaData(parseTermsMetaData(spdxTmdFile)); } } public static TermsMetaData parseTermsMetaData(final File scanCodeTmdFile) throws IOException { try { final Yaml yaml = new Yaml(); final String content = FileUtils.readFileToString(scanCodeTmdFile, FileUtils.ENCODING_UTF_8); return yaml.loadAs(content, TermsMetaData.class); } catch (Exception e) { throw new IllegalStateException(String.format("Cannot parse [%s].", scanCodeTmdFile.getAbsolutePath()), e); } } final Set otherIdsFilter = new HashSet<>(Arrays.asList( "scancode:other-permissive", "scancode:unknown", "scancode:proprietary-license" )); /** * Merges the metadata from external systems including SPDX and ScanCode. * * @param externalTmd {@link TermsMetaData} instance from external. */ protected void mergeMetaData(TermsMetaData externalTmd) { // add additional ids (implies check for osi approval) if (externalTmd.getOtherIds() != null) { for (String otherId : externalTmd.getOtherIds()) { // check whether we already have managed ids of the given type String type = otherId.substring(0, otherId.indexOf(":")); if (StringUtils.isEmpty(getOtherId(type))) { if (otherIds == null) otherIds = new ArrayList<>(); if (!otherIds.contains(otherId) && !otherIdsFilter.contains(otherId)) { otherIds.add(otherId); } } } } // allow to add classification (no overwrite) if (StringUtils.isEmpty(classification)) { this.classification = externalTmd.getClassification(); } // merge comments if (externalTmd.getComments() != null) { for (String comment : externalTmd.getComments()) { if (comments == null) comments = new ArrayList<>(); if (!comments.contains(comment)) { comments.add(comment); } } } } private static class ItemMatches { MatchItem matchItem; int z; public ItemMatches(MatchItem matchItem, int z) { this.matchItem = matchItem; this.z = z; } } public void createMatchReportHtml(StringStats normalizedLicense, ScanResultPart scanResultPart, File htmlFile, List retainedPartialMatches) throws IOException { final String normalizedLicenseText = normalizedLicense.getNormalizedString(); // apply licenses data to template String template = TEMPLATE.replace("${canonicalName}", getCanonicalName()); template = template.replace("${licenseText}", normalizedLicenseText); // insert match table template = template.replace("${matchTemplate}", MATCHES_TABLE); // insert matched table template = template.replace("${matchedTableTemplate}", MATCHED_TABLE); final Map> nameToLMDMatches = new HashMap<>(); final Map> fragmentToLMDMatches = new HashMap<>(); int z = 1; for (MatchItem matchItem : scanResultPart.getMatchItems()) { boolean realMatch = matchItem.getMatchType() != NOT_MATCHED; String matchColor = matchItem.deriveColor(this); TermsMetaData lmd = matchItem.getSourceLicenseMetaData(); String canonicalName = lmd.getCanonicalName(); addMatchIndex(z, matchItem, nameToLMDMatches, fragmentToLMDMatches, retainedPartialMatches); if (realMatch) { String markedLicenseText = normalizedLicenseText; StringStats nm = matchItem.getMatchStringStats(); SimpleIntPair index = normalizedLicense.indexOf(nm, false); String errorMessage = ""; if (index != null && index.getLeft() > -1 && index.getRight() > -1) { int leftIndex = index.getLeft(); int rightIndex = Math.min(index.getRight(), normalizedLicenseText.length()); rightIndex = Math.max(leftIndex, rightIndex); if (leftIndex == rightIndex) { errorMessage = "Could not match text in license. Unable to render mark."; } else { String substring = normalizedLicenseText.substring(leftIndex, rightIndex); markedLicenseText = normalizedLicenseText.substring(0, leftIndex) + "" + substring + "" + normalizedLicenseText.substring(rightIndex); } } else { errorMessage = "Could not match text in license. Index out of range: " + index; } template = template.replace("${marker}", "
" + markedLicenseText + "
\n${marker}"); template = template.replace("${matchMarker}", "

" + canonicalName + " - " + "" + matchItem.getMatchStringStats().getOriginalString() + "" + " - " + matchItem.getMatchContext() + " - " + matchItem.getMatchType() + "

\n${matchMarker}"); String row = ROW_PATTERN; row = row.replace("${number}", String.valueOf(z)); row = row.replace("${canonicalName}", getLinkedName(lmd)); row = row.replace("${onOff}", ""); row = row.replace("${matchText}", "" + matchItem.getMatchStringStats().getOriginalString() + ""); if (!StringUtils.isEmpty(String.valueOf(matchItem.getMatchContext()))) { row = row.replace("${matchContext}", " (" + matchItem.getMatchContext() + ")"); } else { row = row.replace("${matchContext}", ""); } row = row.replace("${matchType}", String.valueOf(matchItem.getMatchType())); if (!StringUtils.isEmpty(errorMessage)) { row = row.replace("${matchError}", "
ERROR: " + errorMessage + ""); } template = template.replace("${row}", row); template = template.replace("${onScript}", "document.getElementById('match-" + z + "').style.display = \"block\";\n${onScript}"); template = template.replace("${onScript}", "document.getElementById('btn-" + z + "').style.backgroundColor = \"rgb(128,200,128)\";\n${onScript}"); template = template.replace("${offScript}", "document.getElementById('match-" + z + "').style.display = \"none\";\n${offScript}"); template = template.replace("${offScript}", "document.getElementById('btn-" + z + "').style.backgroundColor = \"rgb(200,128,128)\";\n${offScript}"); } z++; } z = 1; // fill matched table for (String fullyMatchedLicenses : scanResultPart.getMatchedTerms()) { List ms = nameToLMDMatches.get(fullyMatchedLicenses); String onOff = ""; ItemMatches first = ms.get(0); TermsMetaData lmd = first.matchItem.getSourceLicenseMetaData(); String row = MATCHED_ROW_PATTERN; row = row.replace("${number}", String.valueOf(z)); row = row.replace("${canonicalName}", getLinkedName(lmd)); row = row.replace("${matchText}", text); row = row.replace("${matchContext}", "FULL MATCH"); row = row.replace("${matchType}", ""); row = row.replace("${onOff}", onOff); template = template.replace("${matchedRow}", row); z++; } } // fill partial match table if (retainedPartialMatches != null && !retainedPartialMatches.isEmpty()) { for (String partialMatchedLicenses : retainedPartialMatches) { List ms = nameToLMDMatches.get(partialMatchedLicenses); String onOff = ""; if (!ms.isEmpty()) { ItemMatches first = ms.get(0); TermsMetaData lmd = first.matchItem.getSourceLicenseMetaData(); String row = MATCHED_ROW_PATTERN; row = row.replace("${number}", String.valueOf(z)); row = row.replace("${canonicalName}", getLinkedName(lmd)); row = row.replace("${matchText}", text); row = row.replace("${matchContext}", "PARTIAL MATCH"); row = row.replace("${matchType}", ""); row = row.replace("${onOff}", onOff); template = template.replace("${matchedRow}", row); z++; } } } // finalize matches template = template.replace("${marker}", ""); // finalize table template = template.replace("${row}", ""); template = template.replace("${matchedRow}", ""); template = template.replace("${onScript}", ""); template = template.replace("${offScript}", ""); template = template.replace("${matchError}", ""); FileUtils.write(htmlFile, template, "UTF-8"); } protected String getLinkedName(TermsMetaData lmd) { if (lmd == null) return "n.a."; String linkedName = lmd.getCanonicalName(); if (lmd.getFile() == null) return lmd.getCanonicalName(); File parentFile = lmd.getFile().getParentFile(); if (parentFile == null) return lmd.getCanonicalName(); File matchHtmlFile = new File(parentFile, ".meta/match.html"); if (!matchHtmlFile.exists()) return lmd.getCanonicalName(); return "" + linkedName + ""; } protected void addMatchIndex(int z, MatchItem matchItem, Map> nameToLMDMatches, Map> fragmentToLMDMatches, List retainedPartialMatches) { ItemMatches m = new ItemMatches(matchItem, z); String key = matchItem.getSourceLicenseMetaData().getCanonicalName(); List ms = nameToLMDMatches.computeIfAbsent(key, k -> new ArrayList<>()); if (!ms.contains(m)) { ms.add(m); } if (retainedPartialMatches == null || !retainedPartialMatches.contains(key)) { return; } key = matchItem.getMatchStringStats().getNormalizedString(); ms = fragmentToLMDMatches.computeIfAbsent(key, k -> new ArrayList<>()); if (!ms.contains(m)) { ms.add(m); } } private void validateMatches(List matches, StringStats normalizedLicense) { if (matches != null) { for (String match : matches) { final StringStats normalizedMatch = normalizeString(match, true); if (!normalizedLicense.contains(normalizedMatch, true)) { LOG.debug("Match not found in license text: [{}]", getCanonicalName()); LOG.info("Normalized license: [{}]", normalizedLicense); LOG.info("Normalized match: [{}]", normalizedMatch); LOG.info("Terms Metadata: [file://{}]", getFile().getAbsolutePath()); throw new IllegalStateException(String.format("Match not found in [%s] license text: [%s]", getCanonicalName(), match)); } LOG.debug("Validated match: {}", normalizedMatch.getNormalizedString()); } } } private void validateExcludeMatches(List excludes, StringStats normalizedLicense) { for (String exclude : excludes) { final StringStats normalizedMatch = normalizeString(exclude, true); if (normalizedLicense.contains(normalizedMatch, true)) { LOG.debug("Exclude found in license text: {}", getCanonicalName()); LOG.debug("Normalized license: {}", normalizedLicense); LOG.debug("Normalized exclude: {}", normalizedMatch); throw new IllegalStateException(String.format("Exclude must not be found in %s license text: %s", getCanonicalName(), exclude)); } LOG.debug("Validated exclude: {}", normalizedMatch.getNormalizedString()); } } /** * Matches the current {@link TermsMetaData} instance against the given text fragment. * * @param licenseStats The StringStats instance to match against the current {@link TermsMetaData} instance. * @return A {@link ScanResultPart} instance that contains the result of the match. */ public ScanResultPart analyze(StringStats licenseStats) { final ScanResultPart scanResultPart = new ScanResultPart(); if (LOG.isTraceEnabled()) { LOG.trace(getCanonicalName()); } final MatchMonitor matchMonitor = new MatchMonitor(); // verify excludes. In case an exclude matches, no further analysis (for this TermsMetaData) is performed matchExcludes(licenseStats, scanResultPart, matchMonitor); // as soon as an exclude matches, we do not proceed with any further matching; the exclude takes precedence if (matchMonitor.matchedExcludes > 0) { return scanResultPart; } matchNamesAndReferences(licenseStats, scanResultPart); final Evidence evidence = getEvidence(); if (evidence != null) { match(evidence, matchMonitor, licenseStats, scanResultPart, MatchItem.MatchType.EVIDENCE_MATCH, ""); final List oneOfMatchSetList = evidence.getOneOf(); if (oneOfMatchSetList != null) { for (MatchSet matchSet : oneOfMatchSetList) { // create a separate match monitor for the oneOf match set final MatchMonitor oneOfMatchMonitor = new MatchMonitor(); matchMonitor.addOneOfMatchMonitor(oneOfMatchMonitor); // run the matching using the dedicated match monitor match(matchSet, oneOfMatchMonitor, licenseStats, scanResultPart, MatchItem.MatchType.EVIDENCE_ONE_OF_MATCH, ""); } } } if (getGrants() != null) { // check whether all(!) grants can be found for (final Grant grant : getGrants().values()) { match(grant, matchMonitor, licenseStats, scanResultPart, MatchItem.MatchType.GRANT_MATCH, deriveMatchContext(grant)); if (grant.getObligations() != null) { for (final Obligation obligation : grant.getObligations().values()) { match(obligation, matchMonitor, licenseStats, scanResultPart, MatchItem.MatchType.OBLIGATION_MATCH, deriveMatchContext(grant, obligation)); } } } } matchMonitor.evaluate(scanResultPart, this); return scanResultPart; } private class MatchMonitor { int matched = 0; int total = 0; int matchedExcludes = 0; @Getter List oneOfMatchMonitors = new ArrayList<>(); // the unmatchedMatchedItems are reported as soon as one item matches final List unmatchedMatchItems = new ArrayList<>(); public void evaluate(ScanResultPart scanResultPart, TermsMetaData termsMetaData) { // check evidences / exclude matches if (matched > 0) { scanResultPart.addMatchItems(unmatchedMatchItems); if (LOG.isDebugEnabled()) { LOG.debug(getCanonicalName()); LOG.debug(" matchedEvidences: {}", matched); LOG.debug(" totalEvidences: {}", total); LOG.debug(" matchedExcludes: {}", matchedExcludes); } if (matched == total && matchedExcludes == 0) { scanResultPart.addLicenseMatch(termsMetaData); } else { // at least add the partial match scanResultPart.addPartialMatch(termsMetaData); } } // separately evaluate oneOf MatchMonitors if (getOneOfMatchMonitors() != null && matchedExcludes == 0) { boolean oneOfMatch = false; // check whether a oneOfMatch matched for (final MatchMonitor matchMonitor : getOneOfMatchMonitors()) { if (matchMonitor.matched == matchMonitor.total) { oneOfMatch = true; break; } } if (oneOfMatch) { scanResultPart.addLicenseMatch(termsMetaData); } } } public void addOneOfMatchMonitor(MatchMonitor matchMonitor) { oneOfMatchMonitors.add(matchMonitor); } } private void match(final MatchSet matchSet, final MatchMonitor matchMonitor, final StringStats licenseStats, final ScanResultPart scanResultPart, final MatchItem.MatchType matchType, final CharSequence matchContext) { if (matchSet == null) return; final List matches = matchSet.getMatches(); if (matches == null) return; for (final String match : matches) { final StringStats normalizedMatch = normalizeString(match, true); matchMonitor.total++; if (licenseStats.contains(normalizedMatch, true)) { matchMonitor.matched++; final MatchItem matchItem = new MatchItem(normalizedMatch, matchType, matchContext, this); scanResultPart.addMatchItem(matchItem); if (LOG.isTraceEnabled()) { LOG.trace("Matched: {}", normalizedMatch.getOriginalString()); } } else { final MatchItem matchItem = new MatchItem(normalizedMatch, NOT_MATCHED, matchContext, this); matchMonitor.unmatchedMatchItems.add(matchItem); if (LOG.isTraceEnabled()) { LOG.trace("Not matched: {}", normalizedMatch.getOriginalString()); } } } } private void matchExcludes(StringStats licenseStats, ScanResultPart scanResultPart, MatchMonitor matchMonitor) { final Evidence evidence = getEvidence(); if (evidence != null && evidence.getExcludes() != null) { for (final String exclude : evidence.getExcludes()) { final StringStats normalizedExclude = normalizeString(exclude, true); if (licenseStats.contains(normalizedExclude, true)) { if (LOG.isDebugEnabled()) { LOG.debug("Matched exclude: {}", normalizedExclude.getOriginalString()); } matchMonitor.matchedExcludes++; final MatchItem matchItem = new MatchItem(normalizedExclude, EVIDENCE_EXCLUDE, "", this); scanResultPart.addMatchItem(matchItem); scanResultPart.addExcludedMatch(this); } } } } private void matchNamesAndReferences(StringStats licenseStats, ScanResultPart scanResultPart) { // we collect all names and references in a hash set (implicit removal of duplicates) final Set namesAndReferences = collectNamesAndReferencesAndCache(); // match the consolidated names and references for (final String nameOrReference : namesAndReferences) { matchAlternativeName(licenseStats, scanResultPart, nameOrReference); } } private synchronized Set collectNamesAndReferencesAndCache() { if (this.namesAndReferences == null) { final Set namesAndReferences = new HashSet<>(); // match canonical name namesAndReferences.add(canonicalName); // match short name with context if (!StringUtils.isEmpty(shortName)) { String shortNameForMatching = shortName.replace("-?", ""); namesAndReferences.add(shortNameForMatching + " licensed"); namesAndReferences.add(shortNameForMatching + " license"); namesAndReferences.add(shortNameForMatching + " License"); namesAndReferences.add("licensed under" + shortNameForMatching); namesAndReferences.add("Licensed under" + shortNameForMatching); namesAndReferences.add("License: " + shortNameForMatching); namesAndReferences.add("license: " + shortNameForMatching); } // match spdxIdentifier with context if (!StringUtils.isEmpty(spdxIdentifier)) { namesAndReferences.add("SPDX-License-Identifier: " + spdxIdentifier); namesAndReferences.add(spdxIdentifier + " licensed"); namesAndReferences.add(spdxIdentifier + " license"); namesAndReferences.add(spdxIdentifier + " License"); namesAndReferences.add("licensed under" + spdxIdentifier); namesAndReferences.add("Licensed under" + spdxIdentifier); namesAndReferences.add("License: " + spdxIdentifier); namesAndReferences.add("license: " + spdxIdentifier); } // collect names and references if (getAlternativeNames() != null) { if (LOG.isTraceEnabled()) { LOG.trace("Matching canonical and alternative license names of [{}].", canonicalName); } // match alternative names namesAndReferences.addAll(getAlternativeNames()); } this.namesAndReferences = namesAndReferences; } return this.namesAndReferences; } protected final StringBuilder deriveMatchContext(Grant grant, Obligation obligation) { final StringBuilder sb = deriveMatchContext(grant); if (obligation.getDescription() != null) { if (sb.length() > 0) sb.append("-"); sb.append(obligation.getDescription()); } return sb; } protected final StringBuilder deriveMatchContext(Grant grant) { final StringBuilder sb = new StringBuilder(); if (grant.getAction() != null) { sb.append(grant.getAction()); } if (grant.getSubject() != null) { if (sb.length() > 0) sb.append("-"); sb.append(grant.getSubject()); } return sb; } private void matchAlternativeName(StringStats licenseStats, ScanResultPart scanResultPart, String licenseName) { if (licenseName == null) return; // prepare final StringStats normalizedLicenseName = normalizeString(licenseName, true); final SimpleIntPair matchIndex = licenseStats.indexOf(normalizedLicenseName, true); final int index = matchIndex.getLeft(); if (index >= 0) { // collect all matches for the given license name final int[] matchIndexes = licenseStats.allMatches(normalizedLicenseName); // check whether one of the references applies boolean validMatch = true; // the reference list contains all matching references to THIS license for (final Reference reference : referenceList) { if (reference == null || reference.getMatches() == null) continue; for (final String match : reference.getMatches()) { if (match == null) continue; final StringStats normalizedMatch = normalizeString(match, true); // OPTIMIZATION: 98,5% final SimpleIntPair referenceIndex = licenseStats.indexOf(normalizedMatch, true); for (final int candidateMatchIndex : matchIndexes) { if (referenceIndex.getLeft() <= candidateMatchIndex && referenceIndex.getRight() > candidateMatchIndex) { validMatch = false; scanResultPart.addMatchItem(new MatchItem(normalizedMatch, MatchItem.MatchType.REFERENCE_MATCH, "", this)); break; } } if (!validMatch) break; } if (!validMatch) break; } if (validMatch) { scanResultPart.addNameMatch(this); scanResultPart.addMatchItem(new MatchItem(normalizedLicenseName, MatchItem.MatchType.NAME_MATCH, "", this)); LOG.debug("Matched [{}] on name '{}'.", getCanonicalName(), licenseName); if (LOG.isDebugEnabled()) { LOG.debug(licenseStats.getNormalizedString()); } } else { scanResultPart.addMatchItem(new MatchItem(normalizedLicenseName, MatchItem.MatchType.IGNORED_NAME_MATCH, "", this)); } } } private StringStats normalizeString(String licenseName, boolean isMatch) { return StringStats.normalize(licenseName, isMatch); } public void addReference(Reference reference) { referenceList.add(reference); } public void writeYaml(File file) throws IOException { final String string = new Yaml(YAML_REPRESENTER). dumpAs(this, null, DumperOptions.FlowStyle.BLOCK). replace("!!" + getClass().getName(), "").trim(); FileUtils.writeStringToFile(file, string, StandardCharsets.UTF_8); } public void writeToFile(File file) { final String template = "canonicalName: ${canonicalName}\n" + "category: ${category}\n" + "spdxIdentifier: ${spdxIdentifier}\n" + "shortName: ${shortName}\n" + "\n" + "otherIds:\n" + "${otherIds}\n" + "classification: ${classification}\n" + "\n" + "alternativeNames:\n" + "${alternativeNames}\n" + "comments:\n" + "${comments}"; String content = template; content = content.replace("${canonicalName}", canonicalName); content = content.replace("${category}", category == null ? "" : category); content = content.replace("${classification}", classification == null ? "" : classification); content = content.replace("${spdxIdentifier}", spdxIdentifier == null ? "" : spdxIdentifier); if (alternativeNames != null) { content = content.replace("${alternativeNames}", alternativeNames.stream(). map(s -> " - \"" + escapeYaml(s) + "\"").collect(Collectors.joining("\n"))); } else { content = content.replace("${alternativeNames}", ""); } if (comments != null) { content = content.replace("${comments}", comments.stream(). map(s -> " - \"" + escapeYaml(s) + "\"").collect(Collectors.joining("\n"))); } else { content = content.replace("${comments}", ""); } content = replaceVariable(content, "${shortName}", this.shortName); if (otherIds != null) { content = content.replace("${otherIds}", otherIds.stream().map(s -> " - \"" + escapeYaml(s) + "\"").collect(Collectors.joining("\n"))); } else { content = content.replace("${otherIds}", ""); } try { FileUtils.write(file, content, FileUtils.ENCODING_UTF_8); } catch (IOException e) { LOG.error("Cannot write license meta data.", e); } } private String escapeYaml(String s) { s = s.replace("\\", "\\\\"); // Escape all backslashes s = s.replace("\"", "\\\""); // Escape all double quotes return s; } protected String replaceVariable(String content, String variablePlaceholder, String variableValue) { if (variableValue != null) { content = content.replace(variablePlaceholder, variableValue); } else { content = content.replace(variablePlaceholder, ""); } return content; } public boolean allowLaterVersions() { return allowsLaterVersions; } public boolean ignore() { return ignore; } @Override public boolean equals(Object obj) { if (obj == null || !(getClass().isAssignableFrom(obj.getClass()))) return false; return Objects.equals(getCanonicalName(), ((TermsMetaData) obj).getCanonicalName()); } public boolean isMarker() { return TYPE_MARKER.equals(type); } public boolean isExpression() { return TYPE_EXPRESSION.equals(type); } public boolean isException() { return TYPE_EXCEPTION.equals(type); } public boolean isReference() { return TYPE_REFERENCE.equals(type); } public boolean requiresAnnexNotice() { return requiresAnnexNotice; } /** * For OSI approval an OSI identifier is required. * * @return Whether the other id matches osi. */ public boolean isOsiApproved() { return getOtherId("osi") != null; } public String getOtherId(String type) { if (type == null) return null; final String typePrefix = type + ":"; if (getOtherIds() != null) { String scancodeId = getOtherIds().stream() .filter(Objects::nonNull) .filter(id -> id.startsWith(typePrefix)) .findFirst() .orElse(null); if (scancodeId != null) { return scancodeId.substring(scancodeId.indexOf(":") + 1); } } return null; } public List getOtherIds(String type) { if (type == null) return null; List matchingIds = new ArrayList<>(); final String typePrefix = type + ":"; if (getOtherIds() != null) { for (String otherId : getOtherIds()) { if (otherId != null && otherId.startsWith(typePrefix)) { matchingIds.add(otherId.replace(typePrefix, "")); } } } return matchingIds; } public void readPartialMatches() { File partialFile = new File(getFile().getParentFile(), ".meta/partial.matches.properties"); if (partialFile.exists()) { Properties p = PropertyUtils.loadProperties(partialFile); String pm = p.getProperty("partial.matches", ""); this.partialMatches = InventoryUtils.tokenizeLicense(pm, false, true); String em = p.getProperty("excluded.matches", ""); this.excludedMatches = InventoryUtils.tokenizeLicense(em, false, true); } } public void readMatchedMarkers() { File matchedMarkersFile = new File(getFile().getParentFile(), ".meta/matched-markers.properties"); if (matchedMarkersFile.exists()) { Properties p = PropertyUtils.loadProperties(matchedMarkersFile); String pm = p.getProperty("matched.markers", ""); this.matchedMarkers = InventoryUtils.tokenizeLicense(pm, false, true); } } private static final String TEMPLATE = "\n" + "\n" + "\n" + "${canonicalName}\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
" + "

License Text / Evaluated Text

" + "
" + "${licenseText}" + "
" + "${marker}\n" + "
" + "
" + "${matchedTableTemplate}\n" + "${matchTemplate}\n" + "
\n" + "\n" + ""; public static final String MATCHES_TABLE = "

All Matches

" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " ${row}" + "
NumberCanonical NameMatch Text
"; public static final String MATCHED_TABLE = "

Full Matches / Partial Matches

" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " ${matchedRow}" + "
NumberCanonical NameMatch Type / Match Text
"; public static final String ROW_PATTERN = " \n" + " ${number}\n" + " ${canonicalName}\n" + " ${matchContext}
${matchText}
${matchType}${matchError}\n" + " ${onOff}\n" + " \n${row}"; public static final String MATCHED_ROW_PATTERN = " \n" + " ${number}\n" + " ${canonicalName}\n" + " ${matchContext}
${matchText}
${matchType}${matchError}\n" + " ${onOff}\n" + " \n${matchedRow}"; public void setOpenCoDEStatus(String openCoDEStatus) { if (openCoDEStatus != null) { switch (openCoDEStatus) { case STATUS_NOT_APPROVED: case STATUS_APPROVED_IMPLICIT: case STATUS_APPROVED: break; default: throw new IllegalStateException("Invalid Open CoDE status [" + openCoDEStatus + "]."); } } this.openCoDEStatus = openCoDEStatus; } public boolean isOpenCodeApproved() { return STATUS_APPROVED.equals(openCoDEStatus) || STATUS_APPROVED_IMPLICIT.equals(openCoDEStatus); } public boolean isOpenCodeNotApproved() { return STATUS_NOT_APPROVED.equals(openCoDEStatus); } public boolean isOpenCodeUndefined() { return !isOpenCodeApproved() && !isOpenCodeNotApproved(); } public static final Representer YAML_REPRESENTER = new Representer(new DumperOptions()) { public static final String ATTRIBUTE_NAME = "name"; protected MappingNode representJavaBean(Set properties, Object javaBean) { final MappingNode mappingNode = super.representJavaBean(properties, javaBean); NodeTuple nameNodeTuple = null; final Set disposableNodes = new HashSet<>(); for (NodeTuple nodeTuple : mappingNode.getValue()) { final Node keyNode = nodeTuple.getKeyNode(); if (keyNode instanceof ScalarNode) { final ScalarNode sn = (ScalarNode) keyNode; if (ATTRIBUTE_NAME.equals(sn.getValue())) { nameNodeTuple = nodeTuple; } final Node valueNode = nodeTuple.getValueNode(); if (valueNode instanceof ScalarNode) { final ScalarNode svn = (ScalarNode) valueNode; if (svn.getValue() == null || svn.getValue().equals("null")) { disposableNodes.add(nodeTuple); } else { if (svn.getValue().equals("false")) { disposableNodes.add(nodeTuple); } if ("articleRequired".equals(sn.getValue())) { disposableNodes.add(nodeTuple); } } } if (valueNode instanceof SequenceNode) { final SequenceNode svn = (SequenceNode) valueNode; if (svn.getValue() == null || svn.getValue().isEmpty()) { disposableNodes.add(nodeTuple); } } if (valueNode instanceof MappingNode) { final MappingNode svn = (MappingNode) valueNode; if (svn.getValue() == null || svn.getValue().isEmpty()) { disposableNodes.add(nodeTuple); } } } } if (nameNodeTuple != null) { mappingNode.getValue().remove(nameNodeTuple); mappingNode.getValue().add(0, nameNodeTuple); } mappingNode.getValue().removeAll(disposableNodes); return mappingNode; } }; public boolean isCustomerMetaData() { final File tmdYamlFile = getFile(); return tmdYamlFile.getPath().contains("/_customer/"); } public String resolveType() { return (type == null) ? "terms" : type; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy