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}",
"" +
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" +
" Number \n" +
" Canonical Name \n" +
" Match Text \n" +
" \n" +
" \n" +
" ${row}" +
"
";
public static final String MATCHED_TABLE = "Full Matches / Partial Matches
" +
"\n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" Number \n" +
" Canonical Name \n" +
" Match Type / Match Text \n" +
" \n" +
" \n" +
" ${matchedRow}" +
"
";
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;
}
}
\n${marker}");
template = template.replace("${matchMarker}",
"
© 2015 - 2025 Weber Informatics LLC | Privacy Policy