diff --git a/backend/src-common/pom.xml b/backend/src-common/pom.xml
index 9f8ce85d44..46b95cb588 100644
--- a/backend/src-common/pom.xml
+++ b/backend/src-common/pom.xml
@@ -55,5 +55,13 @@
org.apache.commons
commons-lang3
+
+ org.cyclonedx
+ cyclonedx-core-java
+
+
+ com.github.package-url
+ packageurl-java
+
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/components/summary/ReleaseSummary.java b/backend/src-common/src/main/java/org/eclipse/sw360/components/summary/ReleaseSummary.java
index 83366ad798..c944c2fcbb 100644
--- a/backend/src-common/src/main/java/org/eclipse/sw360/components/summary/ReleaseSummary.java
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/components/summary/ReleaseSummary.java
@@ -109,6 +109,7 @@ private void setShortSummaryFields(Release document, Release copy) {
copyField(document, copy, _Fields.RELEASE_DATE);
copyField(document, copy, _Fields.SOURCE_CODE_DOWNLOADURL);
copyField(document, copy, _Fields.BINARY_DOWNLOADURL);
+ copyField(document, copy, _Fields.PACKAGE_IDS);
}
private void setAdditionalFieldsForSummariesOtherThanShortAndDetailedExport(Release document, Release copy){
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMImporter.java b/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMImporter.java
new file mode 100644
index 0000000000..fa8479f9a9
--- /dev/null
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/cyclonedx/CycloneDxBOMImporter.java
@@ -0,0 +1,870 @@
+/*
+ * Copyright Siemens Healthineers GmBH, 2023. Part of the SW360 Portal Project.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.sw360.cyclonedx;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.thrift.TException;
+import org.cyclonedx.exception.ParseException;
+import org.cyclonedx.model.Bom;
+import org.cyclonedx.model.ExternalReference;
+import org.cyclonedx.model.Metadata;
+import org.cyclonedx.parsers.JsonParser;
+import org.cyclonedx.parsers.Parser;
+import org.cyclonedx.parsers.XmlParser;
+import org.eclipse.sw360.commonIO.AttachmentFrontendUtils;
+import org.eclipse.sw360.datahandler.common.CommonUtils;
+import org.eclipse.sw360.datahandler.common.SW360Constants;
+import org.eclipse.sw360.datahandler.common.SW360Utils;
+import org.eclipse.sw360.datahandler.common.ThriftEnumUtils;
+import org.eclipse.sw360.datahandler.couchdb.AttachmentConnector;
+import org.eclipse.sw360.datahandler.db.ComponentDatabaseHandler;
+import org.eclipse.sw360.datahandler.db.PackageDatabaseHandler;
+import org.eclipse.sw360.datahandler.db.ProjectDatabaseHandler;
+import org.eclipse.sw360.datahandler.thrift.AddDocumentRequestStatus;
+import org.eclipse.sw360.datahandler.thrift.AddDocumentRequestSummary;
+import org.eclipse.sw360.datahandler.thrift.MainlineState;
+import org.eclipse.sw360.datahandler.thrift.ProjectReleaseRelationship;
+import org.eclipse.sw360.datahandler.thrift.ReleaseRelationship;
+import org.eclipse.sw360.datahandler.thrift.RequestStatus;
+import org.eclipse.sw360.datahandler.thrift.RequestSummary;
+import org.eclipse.sw360.datahandler.thrift.SW360Exception;
+import org.eclipse.sw360.datahandler.thrift.Visibility;
+import org.eclipse.sw360.datahandler.thrift.attachments.Attachment;
+import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentContent;
+import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentType;
+import org.eclipse.sw360.datahandler.thrift.attachments.CheckStatus;
+import org.eclipse.sw360.datahandler.thrift.components.Component;
+import org.eclipse.sw360.datahandler.thrift.components.ComponentType;
+import org.eclipse.sw360.datahandler.thrift.components.Release;
+import org.eclipse.sw360.datahandler.thrift.packages.Package;
+import org.eclipse.sw360.datahandler.thrift.packages.PackageManagerType;
+import org.eclipse.sw360.datahandler.thrift.projects.Project;
+import org.eclipse.sw360.datahandler.thrift.projects.ProjectType;
+import org.eclipse.sw360.datahandler.thrift.users.User;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.github.jsonldjava.shaded.com.google.common.io.Files;
+import com.github.packageurl.MalformedPackageURLException;
+import com.github.packageurl.PackageURL;
+import com.google.common.net.MediaType;
+import com.google.gson.Gson;
+
+/**
+ * CycloneDX BOM import implementation.
+ * Supports both XML and JSON format of CycloneDX SBOM
+ *
+ * @author abdul.kapti@siemens-healthineers.com
+ */
+public class CycloneDxBOMImporter {
+ private static final Logger log = LogManager.getLogger(CycloneDxBOMImporter.class);
+ private static final String SCHEMA_PATTERN = ".+://(\\w*(?:[\\-@.\\\\s,_:/][/(.\\-)A-Za-z0-9]+)*)";
+ private static final String DOT_GIT = ".git";
+ private static final String SLASH = "/";
+ private static final String DOT = ".";
+ private static final String HASH = "#";
+ private static final String JOINER = "||";
+ private static final String XML_FILE_EXTENSION = "xml";
+ private static final String JSON_FILE_EXTENSION = "json";
+ private static final Pattern THIRD_SLASH_PATTERN = Pattern.compile("[^/]*(/[^/]*){2}");
+ private static final Pattern FIRST_SLASH_PATTERN = Pattern.compile("/(.*)");
+ private static final String VCS_HTTP_REGEX = "^[^:]+://";
+ private static final String CLEAN_PUBLISHER_REGEX = "<[^>]+>";
+ private static final String HTTPS_SCHEME = "https://";
+ private static final String COMP_CREATION_COUNT_KEY = "compCreationCount";
+ private static final String COMP_REUSE_COUNT_KEY = "compReuseCount";
+ private static final String REL_CREATION_COUNT_KEY = "relCreationCount";
+ private static final String REL_REUSE_COUNT_KEY = "relReuseCount";
+ private static final String PKG_CREATION_COUNT_KEY = "pkgCreationCount";
+ private static final String PKG_REUSE_COUNT_KEY = "pkgReuseCount";
+ private static final String DUPLICATE_COMPONENT = "dupComp";
+ private static final String DUPLICATE_RELEASE = "dupRel";
+ private static final String DUPLICATE_PACKAGE = "dupPkg";
+ private static final String INVALID_COMPONENT = "invalidComp";
+ private static final String INVALID_RELEASE = "invalidRel";
+ private static final String INVALID_PACKAGE = "invalidPkg";
+ private static final String DEFAULT_CATEGORY = "Default_Category";
+ private static final String PROJECT_ID = "projectId";
+ private static final String PROJECT_NAME = "projectName";
+ private static final boolean IS_PACKAGE_PORTLET_ENABLED = SW360Constants.IS_PACKAGE_PORTLET_ENABLED;
+ private static final Predicate typeFilter = type -> ExternalReference.Type.VCS.equals(type);
+
+ private final ProjectDatabaseHandler projectDatabaseHandler;
+ private final ComponentDatabaseHandler componentDatabaseHandler;
+ private final PackageDatabaseHandler packageDatabaseHandler;
+ private final User user;
+ private final AttachmentConnector attachmentConnector;
+
+ public CycloneDxBOMImporter(ProjectDatabaseHandler projectDatabaseHandler, ComponentDatabaseHandler componentDatabaseHandler,
+ PackageDatabaseHandler packageDatabaseHandler, AttachmentConnector attachmentConnector, User user) {
+ this.projectDatabaseHandler = projectDatabaseHandler;
+ this.componentDatabaseHandler = componentDatabaseHandler;
+ this.packageDatabaseHandler = packageDatabaseHandler;
+ this.attachmentConnector = attachmentConnector;
+ this.user = user;
+ }
+
+ /**
+ * Creating the Map of Sanitized VCS URLs to List of Component -> Grouping by VCS URLs
+ * Sanitizing VCS URLs:
+ * git+https://github.com/microsoft/ApplicationInsights-JS.git/tree/master/shared/AppInsightsCommon
+ * --> microsoft.applicationinsights-js
+ * @param components
+ * @return Map>
+ */
+ private Map> getVcsToComponentMap(List components) {
+ return components.stream().filter(Objects::nonNull)
+ .flatMap(comp -> CommonUtils.nullToEmptyList(comp.getExternalReferences()).stream()
+ .filter(Objects::nonNull)
+ .filter(ref -> ExternalReference.Type.VCS.equals(ref.getType()))
+ .map(ExternalReference::getUrl)
+ .map(String::toLowerCase)
+ .map(url -> url.replaceAll(SCHEMA_PATTERN, "$1"))
+ .map(url -> THIRD_SLASH_PATTERN.matcher(url))
+ .filter(matcher -> matcher.find())
+ .map(matcher -> matcher.group())
+ .map(url -> FIRST_SLASH_PATTERN.matcher(url))
+ .filter(matcher -> matcher.find())
+ .map(matcher -> matcher.group(1))
+ .map(url -> StringUtils.substringBefore(url, HASH))
+ .map(url -> StringUtils.removeEnd(url, DOT_GIT))
+ .map(url -> url.replaceAll(SLASH, DOT))
+ .map(url -> new AbstractMap.SimpleEntry<>(url, comp)))
+ .collect(Collectors.groupingBy(e -> e.getKey(),
+ Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
+ }
+
+ @SuppressWarnings("unchecked")
+ public RequestSummary importFromBOM(InputStream inputStream, AttachmentContent attachmentContent, String projectId, User user) {
+ RequestSummary requestSummary = new RequestSummary();
+ Map messageMap = new HashMap<>();
+ requestSummary.setRequestStatus(RequestStatus.FAILURE);
+ String fileExtension = Files.getFileExtension(attachmentContent.getFilename());
+ Parser parser;
+
+ if (fileExtension.equalsIgnoreCase(XML_FILE_EXTENSION)) {
+ parser = new XmlParser();
+ } else if (fileExtension.equalsIgnoreCase(JSON_FILE_EXTENSION)) {
+ parser = new JsonParser();
+ } else {
+ requestSummary.setMessage(String.format("Invalid file format <%s>. Only XML & JSON CycloneDX SBOM are supported!", fileExtension));
+ return requestSummary;
+ }
+
+ try {
+ // parsing the input stream into CycloneDx org.cyclonedx.model.Bom
+ Bom bom = parser.parse(IOUtils.toByteArray(inputStream));
+ Metadata bomMetadata = bom.getMetadata();
+
+ // Getting List of org.cyclonedx.model.Component from the Bom
+ List components = CommonUtils.nullToEmptyList(bom.getComponents());
+
+ long vcsCount = components.stream().map(org.cyclonedx.model.Component::getExternalReferences)
+ .filter(Objects::nonNull).flatMap(List::stream).map(ExternalReference::getType).filter(typeFilter).count();
+ long componentsCount = components.size();
+ org.cyclonedx.model.Component compMetadata = bomMetadata.getComponent();
+ Map> vcsToComponentMap = new HashMap<>();
+
+ if (!IS_PACKAGE_PORTLET_ENABLED) {
+ vcsToComponentMap.put("", components);
+ requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent);
+ } else {
+
+ vcsToComponentMap = getVcsToComponentMap(components);
+ if (componentsCount == vcsCount) {
+
+ requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent);
+ } else if (componentsCount > vcsCount) {
+
+ requestSummary = importSbomAsProject(compMetadata, vcsToComponentMap, projectId, attachmentContent);
+
+ if (requestSummary.requestStatus.equals(RequestStatus.SUCCESS)) {
+
+ String jsonMessage = requestSummary.getMessage();
+ messageMap = new Gson().fromJson(jsonMessage, Map.class);
+ String projId = messageMap.get("projectId");
+
+ if (CommonUtils.isNullEmptyOrWhitespace(projId)) {
+ return requestSummary;
+ }
+ final Set duplicatePackages = new HashSet<>();
+ final Set componentsWithoutVcs = new HashSet<>();
+ final Set invalidPackages = new HashSet<>();
+
+ Integer relReuseCount = Integer.valueOf(messageMap.get(REL_REUSE_COUNT_KEY));
+ Integer pkgReuseCount = Integer.valueOf(messageMap.get(PKG_REUSE_COUNT_KEY));
+ Integer pkgCreationCount = Integer.valueOf(messageMap.get(PKG_CREATION_COUNT_KEY));
+
+ String packages = messageMap.get(DUPLICATE_PACKAGE);
+ if (CommonUtils.isNotNullEmptyOrWhitespace(packages)) {
+ duplicatePackages.addAll(Arrays.asList(packages.split(JOINER)));
+ packages = "";
+ }
+ packages = messageMap.get(INVALID_PACKAGE);
+ if (CommonUtils.isNotNullEmptyOrWhitespace(packages)) {
+ invalidPackages.addAll(Arrays.asList(packages.split(JOINER)));
+ packages = "";
+ }
+ Project project = projectDatabaseHandler.getProjectById(projId, user);
+
+ for (org.cyclonedx.model.Component comp : components) {
+ if (CommonUtils.isNullOrEmptyCollection(comp.getExternalReferences())
+ || comp.getExternalReferences().stream().map(ExternalReference::getType).filter(typeFilter).count() == 0) {
+
+ final var fullName = SW360Utils.getVersionedName(comp.getName(), comp.getVersion());
+ final var licenses = getLicenseFromBomComponent(comp);
+ final Package pkg = createPackage(comp, null, licenses);
+
+ if (pkg == null || CommonUtils.isNullEmptyOrWhitespace(pkg.getName()) || CommonUtils.isNullEmptyOrWhitespace(pkg.getVersion())
+ || CommonUtils.isNullEmptyOrWhitespace(pkg.getPurl())) {
+ invalidPackages.add(fullName);
+ log.error(String.format("Invalid package '%s' found in SBoM, missing name or version or purl! ", fullName));
+ continue;
+ }
+
+ try {
+ AddDocumentRequestSummary pkgAddSummary = packageDatabaseHandler.addPackage(pkg, user);
+ componentsWithoutVcs.add(fullName);
+
+ if (CommonUtils.isNotNullEmptyOrWhitespace(pkgAddSummary.getId())) {
+ project.addToPackageIds(pkgAddSummary.getId());
+ pkg.setId(pkgAddSummary.getId());
+ if (AddDocumentRequestStatus.DUPLICATE.equals(pkgAddSummary.getRequestStatus())) {
+ pkgReuseCount++;
+ Package dupPkg = packageDatabaseHandler.getPackageById(pkgAddSummary.getId());
+ if (CommonUtils.isNotNullEmptyOrWhitespace(dupPkg.getReleaseId())) {
+ if (!CommonUtils.nullToEmptyMap(project.getReleaseIdToUsage()).containsKey(pkgAddSummary.getId())) {
+ project.putToReleaseIdToUsage(pkgAddSummary.getId(), getDefaultRelation());
+ }
+ relReuseCount++;
+ }
+ } else {
+ pkgCreationCount++;
+ }
+ } else {
+ // in case of more than 1 duplicate found, then return and show error message in UI.
+ log.warn("found multiple packages: " + fullName);
+ duplicatePackages.add(fullName);
+ continue;
+ }
+ } catch (SW360Exception e) {
+ log.error("An error occured while creating/adding package from SBOM: " + e.getMessage());
+ continue;
+ }
+
+ }
+ }
+ RequestStatus updateStatus = projectDatabaseHandler.updateProject(project, user);
+ if (RequestStatus.SUCCESS.equals(updateStatus)) {
+ log.info("linking packages to project successfull: " + projId);
+ }
+ // all components does not have VCS, so return & show appropriate error in UI
+ messageMap.put(INVALID_COMPONENT, String.join(JOINER, componentsWithoutVcs));
+ messageMap.put(INVALID_PACKAGE, String.join(JOINER, invalidPackages));
+ messageMap.put(DUPLICATE_PACKAGE, String.join(JOINER, duplicatePackages));
+ messageMap.put(SW360Constants.MESSAGE,
+ String.format("VCS information is missing for %s / %s Components!",
+ componentsCount - vcsCount, componentsCount));
+ messageMap.put(REL_REUSE_COUNT_KEY, String.valueOf(relReuseCount));
+ messageMap.put(PKG_CREATION_COUNT_KEY, String.valueOf(pkgCreationCount));
+ messageMap.put(PKG_REUSE_COUNT_KEY, String.valueOf(pkgReuseCount));
+ requestSummary.setMessage(convertCollectionToJSONString(messageMap));
+ }
+ } else {
+ // this case is not possible, so return & show appropriate error in UI
+ requestSummary.setMessage(String.format(String.format(
+ "SBOM import aborted with error: Multiple vcs information found in compnents, vcs found: %s and total components: %s",
+ vcsCount, componentsCount)));
+ return requestSummary;
+ }
+ }
+
+ if (RequestStatus.SUCCESS.equals(requestSummary.getRequestStatus())) {
+ String jsonMessage = requestSummary.getMessage();
+ messageMap = new Gson().fromJson(jsonMessage, Map.class);
+ String projId = messageMap.get("projectId");
+ Project project = projectDatabaseHandler.getProjectById(projId, user);
+ try {
+ // link SBOM attachment to Project
+ if (attachmentContent != null) {
+ Attachment attachment = makeAttachmentFromContent(attachmentContent);
+ project.addToAttachments(attachment);
+ }
+ RequestStatus updateStatus = projectDatabaseHandler.updateProject(project, user, true);
+ if (RequestStatus.SUCCESS.equals(updateStatus)) {
+ log.info("SBOM attachment linked to project successfully: " + project.getId());
+ } else {
+ log.info("failed to link SBOM Import status attachment to project with status: " + updateStatus);
+ }
+ messageMap.put("result", requestSummary.getRequestStatus().toString());
+ messageMap.put("fileName", attachmentContent.getFilename());
+ final StringBuilder fileName = new StringBuilder(attachmentContent.getFilename()).append("_ImportStatus.").append(JSON_FILE_EXTENSION);
+ final InputStream inStream = IOUtils.toInputStream(convertCollectionToJSONString(messageMap), Charset.defaultCharset());
+ final AttachmentContent importResultAttachmentContent = makeAttachmentContent(fileName.toString(), MediaType.JSON_UTF_8.toString());
+ Attachment attachment = new AttachmentFrontendUtils().uploadAttachmentContent(importResultAttachmentContent, inStream, user);
+ attachment.setAttachmentType(AttachmentType.OTHER);
+ StringBuilder comment = new StringBuilder("Auto Generated: CycloneDX SBOM import result for: '").append(attachmentContent.getFilename()).append("'");
+ attachment.setCreatedComment(comment.toString());
+ attachment.setSha1(attachmentConnector.getSha1FromAttachmentContentId(importResultAttachmentContent.getId()));
+ project = projectDatabaseHandler.getProjectById(projId, user);
+ project.addToAttachments(attachment);
+ updateStatus = projectDatabaseHandler.updateProject(project, user, true);
+ if (RequestStatus.SUCCESS.equals(updateStatus)) {
+ log.info("SBOM Import status attachment linked to project successfully: " + project.getId());
+ } else {
+ log.info("failed to link SBOM Import status attachment to project with status: " + updateStatus);
+ }
+ } catch (SW360Exception e) {
+ log.error("An error occured while updating project from SBOM: " + e.getMessage());
+ requestSummary.setMessage("An error occured while updating project during SBOM import, please delete the project and re-import SBOM!");
+ return requestSummary;
+ }
+ }
+
+ } catch (IOException e) {
+ log.error("IOException occured while importing CycloneDX SBoM: ", e);
+ requestSummary.setMessage("IOException occured while importing CycloneDX SBoM: " + e.getMessage());
+ } catch (ParseException e) {
+ log.error("ParseException occured while importing CycloneDX SBoM: ", e);
+ requestSummary.setMessage("ParseException occured while importing CycloneDX SBoM: " + e.getMessage());
+ } catch (SW360Exception e) {
+ log.error("SW360Exception occured while importing CycloneDX SBoM: ", e);
+ requestSummary.setMessage("SW360Exception occured while importing CycloneDX SBoM: " + e.getMessage());
+ } catch (Exception e) {
+ log.error("Exception occured while importing CycloneDX SBoM: ", e);
+ requestSummary.setMessage("An Exception occured while importing CycloneDX SBoM: " + e.getMessage());
+ }
+ return requestSummary;
+ }
+
+ public RequestSummary importSbomAsProject(org.cyclonedx.model.Component compMetadata,
+ Map> vcsToComponentMap, String projectId, AttachmentContent attachmentContent)
+ throws SW360Exception {
+ final RequestSummary summary = new RequestSummary();
+ summary.setRequestStatus(RequestStatus.FAILURE);
+
+ Project project;
+ AddDocumentRequestSummary projectAddSummary = new AddDocumentRequestSummary();
+ AddDocumentRequestStatus addStatus = projectAddSummary.getRequestStatus();
+ Map messageMap = new HashMap<>();
+
+ try {
+ if (CommonUtils.isNotNullEmptyOrWhitespace(projectId)) {
+ project = projectDatabaseHandler.getProjectById(projectId, user);
+ log.info("reusing existing project: " + projectId);
+ } else {
+ // Metadata component is used to created the project
+ project = createProject(compMetadata);
+ projectAddSummary = projectDatabaseHandler.addProject(project, user);
+ addStatus = projectAddSummary.getRequestStatus();
+
+ if (CommonUtils.isNotNullEmptyOrWhitespace(projectAddSummary.getId())) {
+ if (AddDocumentRequestStatus.SUCCESS.equals(addStatus)) {
+ project = projectDatabaseHandler.getProjectById(projectAddSummary.getId(), user);
+ log.info("project created successfully: " + projectAddSummary.getId());
+ } else if (AddDocumentRequestStatus.DUPLICATE.equals(addStatus)) {
+ log.error("cannot import SBOM for an existing project from Project List / Home page - " + projectAddSummary.getId());
+ summary.setRequestStatus(getRequestStatusFromAddDocRequestStatus(addStatus));
+ messageMap.put(PROJECT_ID, projectAddSummary.getId());
+ messageMap.put(PROJECT_NAME, SW360Utils.getVersionedName(project.getName(), project.getVersion()));
+ messageMap.put(SW360Constants.MESSAGE, "A project with same name and version already exists in SW360, Please import this SBOM from project details page!");
+ summary.setMessage(convertCollectionToJSONString(messageMap));
+ return summary;
+ }
+ } else {
+ summary.setRequestStatus(getRequestStatusFromAddDocRequestStatus(addStatus));
+ summary.setMessage("Invalid Projct metadata present in SBOM or Multiple project with same name and version is already present in SW360!");
+ return summary;
+ }
+
+ }
+ } catch (SW360Exception e) {
+ log.error("An error occured while importing project from SBOM: " + e.getMessage());
+ summary.setMessage("An error occured while importing project from SBOM!");
+ return summary;
+ }
+
+ if (IS_PACKAGE_PORTLET_ENABLED) {
+ messageMap = importAllComponentsAsPackages(vcsToComponentMap, project);
+ } else {
+ messageMap = importAllComponentsAsReleases(vcsToComponentMap, project);
+ }
+ RequestStatus updateStatus = projectDatabaseHandler.updateProject(project, user, true);
+ if (RequestStatus.SUCCESS.equals(updateStatus)) {
+ log.info("project updated successfully: " + project.getId());
+ } else {
+ log.info("failed to update project with status: " + updateStatus);
+ }
+ summary.setMessage(convertCollectionToJSONString(messageMap));
+ summary.setRequestStatus(RequestStatus.SUCCESS);
+ return summary;
+ }
+
+ private Map importAllComponentsAsReleases(Map> vcsToComponentMap, Project project) {
+
+ final var countMap = new HashMap();
+ final Set duplicateComponents = new HashSet<>();
+ final Set duplicateReleases = new HashSet<>();
+ final Set invalidReleases = new HashSet<>();
+ countMap.put(COMP_CREATION_COUNT_KEY, 0); countMap.put(COMP_REUSE_COUNT_KEY, 0);
+ countMap.put(REL_CREATION_COUNT_KEY, 0); countMap.put(REL_REUSE_COUNT_KEY, 0);
+ int compCreationCount = 0, compReuseCount = 0, relCreationCount = 0, relReuseCount = 0;
+
+ final List components = vcsToComponentMap.get("");
+ for (org.cyclonedx.model.Component bomComp : components) {
+ Component comp = createComponent(bomComp);
+ if (CommonUtils.isNullEmptyOrWhitespace(comp.getName()) ) {
+ log.error("component name is not present in SBoM: " + project.getId());
+ continue;
+ }
+ String relName = "";
+ AddDocumentRequestSummary compAddSummary;
+ try {
+ compAddSummary = componentDatabaseHandler.addComponent(comp, user.getEmail());
+
+ if (CommonUtils.isNotNullEmptyOrWhitespace(compAddSummary.getId())) {
+ comp.setId(compAddSummary.getId());
+ if (AddDocumentRequestStatus.SUCCESS.equals(compAddSummary.getRequestStatus())) {
+ compCreationCount++;
+ } else {
+ compReuseCount++;
+ }
+ } else {
+ // in case of more than 1 duplicate found, then continue and show error message in UI.
+ log.warn("found multiple components: " + comp.getName());
+ duplicateComponents.add(comp.getName());
+ continue;
+ }
+
+ Release rel = new Release();
+ Set licenses = getLicenseFromBomComponent(bomComp);
+ rel = createRelease(bomComp, comp, licenses);
+ if (CommonUtils.isNullEmptyOrWhitespace(rel.getVersion()) ) {
+ log.error("release version is not present in SBoM for component: " + comp.getName());
+ invalidReleases.add(comp.getName());
+ continue;
+ }
+ relName = SW360Utils.getVersionedName(rel.getName(), rel.getVersion());
+
+ try {
+ AddDocumentRequestSummary relAddSummary = componentDatabaseHandler.addRelease(rel, user);
+ if (CommonUtils.isNotNullEmptyOrWhitespace(relAddSummary.getId())) {
+ rel.setId(relAddSummary.getId());
+ if (AddDocumentRequestStatus.SUCCESS.equals(relAddSummary.getRequestStatus())) {
+ relCreationCount++;
+ } else {
+ relReuseCount++;
+ }
+ } else {
+ // in case of more than 1 duplicate found, then continue and show error message in UI.
+ log.warn("found multiple releases: " + relName);
+ duplicateReleases.add(relName);
+ continue;
+ }
+ project.putToReleaseIdToUsage(rel.getId(), getDefaultRelation());
+ } catch (SW360Exception e) {
+ log.error("An error occured while creating/adding release from SBOM: " + e.getMessage());
+ continue;
+ }
+
+ // update components specific fields
+ comp = componentDatabaseHandler.getComponent(compAddSummary.getId(), user);
+ if (null != bomComp.getType()) {
+ Set categories = new HashSet<>(comp.getCategories());
+ categories.add(bomComp.getType().getTypeName());
+ categories.remove(DEFAULT_CATEGORY);
+ comp.setCategories(categories);
+ }
+ StringBuilder description = new StringBuilder();
+ if (CommonUtils.isNullEmptyOrWhitespace(comp.getDescription())) {
+ description.append(CommonUtils.nullToEmptyString(bomComp.getDescription()).trim());
+ } else {
+ description.append(" | ").append(CommonUtils.nullToEmptyString(bomComp.getDescription()).trim());
+ }
+ if (comp.isSetMainLicenseIds()) {
+ comp.getMainLicenseIds().addAll(licenses);
+ } else {
+ comp.setMainLicenseIds(licenses);
+ }
+ comp.setDescription(description.toString());
+ RequestStatus updateStatus = componentDatabaseHandler.updateComponent(comp, user, true);
+ if (RequestStatus.SUCCESS.equals(updateStatus)) {
+ log.info("updating component successfull: " + comp.getName());
+ }
+ } catch (SW360Exception e) {
+ log.error("An error occured while creating/adding component from SBOM: " + e.getMessage());
+ continue;
+ }
+ }
+
+ final Map messageMap = new HashMap<>();
+ messageMap.put(DUPLICATE_COMPONENT, String.join(JOINER, duplicateComponents));
+ messageMap.put(DUPLICATE_RELEASE, String.join(JOINER, duplicateReleases));
+ messageMap.put(INVALID_RELEASE, String.join(JOINER, invalidReleases));
+ messageMap.put(PROJECT_ID, project.getId());
+ messageMap.put(PROJECT_NAME, SW360Utils.getVersionedName(project.getName(), project.getVersion()));
+ messageMap.put(COMP_CREATION_COUNT_KEY, String.valueOf(compCreationCount));
+ messageMap.put(COMP_REUSE_COUNT_KEY, String.valueOf(compReuseCount));
+ messageMap.put(REL_CREATION_COUNT_KEY, String.valueOf(relCreationCount));
+ messageMap.put(REL_REUSE_COUNT_KEY, String.valueOf(relReuseCount));
+ return messageMap;
+ }
+
+ private Map importAllComponentsAsPackages(Map> vcsToComponentMap, Project project) {
+
+ final var countMap = new HashMap();
+ final Set duplicateComponents = new HashSet<>();
+ final Set duplicateReleases = new HashSet<>();
+ final Set duplicatePackages = new HashSet<>();
+ final Set invalidReleases = new HashSet<>();
+ final Set invalidPackages = new HashSet<>();
+ final Map releaseRelationMap = CommonUtils.isNullOrEmptyMap(project.getReleaseIdToUsage()) ? new HashMap<>() : project.getReleaseIdToUsage();
+ countMap.put(REL_CREATION_COUNT_KEY, 0); countMap.put(REL_REUSE_COUNT_KEY, 0);
+ countMap.put(PKG_CREATION_COUNT_KEY, 0); countMap.put(PKG_REUSE_COUNT_KEY, 0);
+ int relCreationCount = 0, relReuseCount = 0, pkgCreationCount = 0, pkgReuseCount = 0;
+
+ for (Map.Entry> entry : vcsToComponentMap.entrySet()) {
+ Component comp = createComponent(entry.getKey());
+ Release rel = new Release();
+ String relName = "";
+ StringBuilder description = new StringBuilder();
+ AddDocumentRequestSummary compAddSummary;
+ try {
+ compAddSummary = componentDatabaseHandler.addComponent(comp, user.getEmail());
+
+ if (CommonUtils.isNotNullEmptyOrWhitespace(compAddSummary.getId())) {
+ comp.setId(compAddSummary.getId());
+ if (AddDocumentRequestStatus.SUCCESS.equals(compAddSummary.getRequestStatus())) {
+ } else {
+ }
+ } else {
+ // in case of more than 1 duplicate found, then continue and show error message in UI.
+ log.warn("found multiple components: " + comp.getName());
+ duplicateComponents.add(comp.getName());
+ continue;
+ }
+
+ for (org.cyclonedx.model.Component bomComp : entry.getValue()) {
+
+ Set licenses = getLicenseFromBomComponent(bomComp);
+ rel = createRelease(bomComp, comp, licenses);
+ if (CommonUtils.isNullEmptyOrWhitespace(rel.getVersion()) ) {
+ log.error("release version is not present in SBoM for component: " + comp.getName());
+ invalidReleases.add(comp.getName());
+ continue;
+ }
+ relName = SW360Utils.getVersionedName(rel.getName(), rel.getVersion());
+
+ try {
+ AddDocumentRequestSummary relAddSummary = componentDatabaseHandler.addRelease(rel, user);
+ if (CommonUtils.isNotNullEmptyOrWhitespace(relAddSummary.getId())) {
+ rel.setId(relAddSummary.getId());
+ if (AddDocumentRequestStatus.SUCCESS.equals(relAddSummary.getRequestStatus())) {
+ relCreationCount = releaseRelationMap.containsKey(rel.getId()) ? relCreationCount : relCreationCount + 1;
+ } else {
+ relReuseCount = releaseRelationMap.containsKey(rel.getId()) ? relReuseCount : relReuseCount + 1;
+ }
+ } else {
+ // in case of more than 1 duplicate found, then continue and show error message in UI.
+ log.warn("found multiple releases: " + relName);
+ duplicateReleases.add(relName);
+ continue;
+ }
+ } catch (SW360Exception e) {
+ log.error("An error occured while creating/adding release from SBOM: " + e.getMessage());
+ continue;
+ }
+
+ // update components specific fields
+ comp = componentDatabaseHandler.getComponent(compAddSummary.getId(), user);
+ if (null != bomComp.getType()) {
+ Set categories = new HashSet<>(comp.getCategories());
+ categories.add(bomComp.getType().getTypeName());
+ categories.remove(DEFAULT_CATEGORY);
+ comp.setCategories(categories);
+ }
+ if (CommonUtils.isNullEmptyOrWhitespace(comp.getDescription())) {
+ description.append(CommonUtils.nullToEmptyString(bomComp.getDescription()).trim());
+ } else {
+ description.append(" | ").append(CommonUtils.nullToEmptyString(bomComp.getDescription()).trim());
+ }
+ if (comp.isSetMainLicenseIds()) {
+ comp.getMainLicenseIds().addAll(licenses);
+ } else {
+ comp.setMainLicenseIds(licenses);
+ }
+ comp.setDescription(description.toString());
+ RequestStatus updateStatus = componentDatabaseHandler.updateComponent(comp, user, true);
+ if (RequestStatus.SUCCESS.equals(updateStatus)) {
+ log.info("updating component successfull: " + comp.getName());
+ }
+
+ Package pkg = createPackage(bomComp, rel, licenses);
+ String pkgName = SW360Utils.getVersionedName(rel.getName(), rel.getVersion());
+ if (pkg == null || CommonUtils.isNullEmptyOrWhitespace(pkg.getName()) || CommonUtils.isNullEmptyOrWhitespace(pkg.getVersion())
+ || CommonUtils.isNullEmptyOrWhitespace(pkg.getPurl())) {
+ invalidPackages.add(pkgName);
+ log.error(String.format("Invalid package '%s' found in SBoM, missing name or version or purl! ", pkgName));
+ continue;
+ }
+
+ try {
+ AddDocumentRequestSummary pkgAddSummary = packageDatabaseHandler.addPackage(pkg, user);
+ if (CommonUtils.isNotNullEmptyOrWhitespace(pkgAddSummary.getId())) {
+ pkg.setId(pkgAddSummary.getId());
+ if (AddDocumentRequestStatus.DUPLICATE.equals(pkgAddSummary.getRequestStatus())) {
+ Package dupPkg = packageDatabaseHandler.getPackageById(pkg.getId());
+ if (!rel.getId().equals(dupPkg.getReleaseId())) {
+ log.error("Release Id of Package from BOM: '%s' and Database: '%s' is not equal!", rel.getId(), dupPkg.getReleaseId());
+ dupPkg.setReleaseId(rel.getId());
+ packageDatabaseHandler.updatePackage(dupPkg, user, true);
+ }
+ pkgReuseCount++;
+ } else {
+ pkgCreationCount++;
+ }
+ } else {
+ // in case of more than 1 duplicate found, then continue and show error message in UI.
+ log.warn("found multiple packages: " + pkgName);
+ duplicatePackages.add(pkgName);
+ continue;
+ }
+ project.addToPackageIds(pkg.getId());
+ releaseRelationMap.putIfAbsent(rel.getId(), getDefaultRelation());
+ } catch (SW360Exception e) {
+ log.error("An error occured while creating/adding package from SBOM: " + e.getMessage());
+ continue;
+ }
+ }
+ } catch (SW360Exception e) {
+ log.error("An error occured while creating/adding component from SBOM: " + e.getMessage());
+ continue;
+ }
+ }
+
+ project.setReleaseIdToUsage(releaseRelationMap);
+ final Map messageMap = new HashMap<>();
+ messageMap.put(DUPLICATE_COMPONENT, String.join(JOINER, duplicateComponents));
+ messageMap.put(DUPLICATE_RELEASE, String.join(JOINER, duplicateReleases));
+ messageMap.put(DUPLICATE_PACKAGE, String.join(JOINER, duplicatePackages));
+ messageMap.put(INVALID_RELEASE, String.join(JOINER, invalidReleases));
+ messageMap.put(INVALID_PACKAGE, String.join(JOINER, invalidPackages));
+ messageMap.put(PROJECT_ID, project.getId());
+ messageMap.put(PROJECT_NAME, SW360Utils.getVersionedName(project.getName(), project.getVersion()));
+ messageMap.put(REL_CREATION_COUNT_KEY, String.valueOf(relCreationCount));
+ messageMap.put(REL_REUSE_COUNT_KEY, String.valueOf(relReuseCount));
+ messageMap.put(PKG_CREATION_COUNT_KEY, String.valueOf(pkgCreationCount));
+ messageMap.put(PKG_REUSE_COUNT_KEY, String.valueOf(pkgReuseCount));
+ return messageMap;
+ }
+
+ private Set getLicenseFromBomComponent(org.cyclonedx.model.Component comp) {
+ Set licenses = new HashSet<>();
+ if (Objects.nonNull(comp.getLicenseChoice()) && CommonUtils.isNotEmpty(comp.getLicenseChoice().getLicenses())) {
+ licenses.addAll(comp.getLicenseChoice().getLicenses().stream()
+ .map(lic -> (null == lic.getId()) ? lic.getName() : lic.getId()).collect(Collectors.toSet()));
+ }
+ return licenses;
+ }
+
+ private String convertCollectionToJSONString(Map map) throws SW360Exception {
+ try {
+ JsonFactory factory = new JsonFactory();
+ StringWriter jsonObjectWriter = new StringWriter();
+ JsonGenerator jsonGenerator = factory.createGenerator(jsonObjectWriter);
+ jsonGenerator.useDefaultPrettyPrinter();
+ jsonGenerator.writeStartObject();
+ for (Map.Entry entry : map.entrySet()) {
+ jsonGenerator.writeStringField(entry.getKey(), entry.getValue());
+ }
+ jsonGenerator.writeEndObject();
+ jsonGenerator.close();
+ return jsonObjectWriter.toString();
+ } catch (IOException e) {
+ throw new SW360Exception("An exception occured while generating JSON info for BOM import! " + e.getMessage());
+ }
+ }
+
+ private RequestStatus getRequestStatusFromAddDocRequestStatus(AddDocumentRequestStatus status) {
+ switch (status) {
+ case SUCCESS:
+ return RequestStatus.SUCCESS;
+ case DUPLICATE:
+ return RequestStatus.DUPLICATE;
+ case NAMINGERROR:
+ return RequestStatus.NAMINGERROR;
+ case INVALID_INPUT:
+ return RequestStatus.INVALID_INPUT;
+ default:
+ return RequestStatus.FAILURE;
+ }
+ }
+
+ private AttachmentContent makeAttachmentContent(String filename, String contentType) {
+ AttachmentContent attachment = new AttachmentContent()
+ .setContentType(contentType)
+ .setFilename(filename)
+ .setOnlyRemote(false);
+ return makeAttachmentContent(attachment);
+ }
+
+ private AttachmentContent makeAttachmentContent(AttachmentContent content) {
+ try {
+ return new AttachmentFrontendUtils().makeAttachmentContent(content);
+ } catch (TException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Attachment makeAttachmentFromContent(AttachmentContent attachmentContent) {
+ Attachment attachment = new Attachment();
+ attachment.setAttachmentContentId(attachmentContent.getId());
+ attachment.setAttachmentType(AttachmentType.SBOM);
+ StringBuilder comment = new StringBuilder("Auto Generated: Used for importing CycloneDX SBOM.");
+ attachment.setCreatedComment(comment.toString());
+ attachment.setFilename(attachmentContent.getFilename());
+ attachment.setCreatedOn(SW360Utils.getCreatedOn());
+ attachment.setCreatedBy(user.getEmail());
+ attachment.setCreatedTeam(user.getDepartment());
+ attachment.setCheckStatus(CheckStatus.NOTCHECKED);
+ return attachment;
+ }
+
+ private Project createProject(org.cyclonedx.model.Component compMetadata) {
+ return new Project(CommonUtils.nullToEmptyString(compMetadata.getName()).trim())
+ .setVersion(CommonUtils.nullToEmptyString(compMetadata.getVersion()).trim())
+ .setDescription(CommonUtils.nullToEmptyString(compMetadata.getDescription()).trim()).setType(ThriftEnumUtils.enumToString(ProjectType.PRODUCT))
+ .setBusinessUnit(user.getDepartment()).setVisbility(Visibility.EVERYONE);
+ }
+
+ private Component createComponent(String name) {
+ Component component = new Component();
+ component.setName(name);
+ component.setComponentType(ComponentType.OSS);
+ return component;
+ }
+
+ private Component createComponent(org.cyclonedx.model.Component componentFromBom) {
+ Component component = new Component();
+ component.setName(getComponentName(componentFromBom).trim());
+ component.setComponentType(ComponentType.OSS);
+ return component;
+ }
+
+ private String getComponentName(org.cyclonedx.model.Component comp) {
+ String name = CommonUtils.nullToEmptyString(comp.getName());
+ if (CommonUtils.isNotNullEmptyOrWhitespace(comp.getGroup())) {
+ return new StringBuilder(comp.getGroup()).append(DOT).append(name).toString();
+ } else if (CommonUtils.isNotNullEmptyOrWhitespace(comp.getPublisher())) {
+ return new StringBuilder(comp.getPublisher().replaceAll(CLEAN_PUBLISHER_REGEX, StringUtils.EMPTY)).append(DOT).append(name).toString();
+ } else if (CommonUtils.isNotNullEmptyOrWhitespace(comp.getAuthor())) {
+ //.replaceAll(CLEAN_PUBLISHER_REGEX, StringUtils.EMPTY) --> Use this to remove Publisher email id.
+ return new StringBuilder(comp.getAuthor().replaceAll(CLEAN_PUBLISHER_REGEX, StringUtils.EMPTY)).append(DOT).append(name).toString();
+ } else {
+ return name;
+ }
+ }
+
+ private Release createRelease(org.cyclonedx.model.Component componentFromBom, Component component,
+ Set licenses) {
+ Release release = new Release(component.getName(), CommonUtils.nullToEmptyString(componentFromBom.getVersion()).trim(), component.getId());
+ release.setCreatorDepartment(user.getDepartment());
+ if (release.isSetMainLicenseIds()) {
+ release.getMainLicenseIds().addAll(licenses);
+ } else {
+ release.setMainLicenseIds(licenses);
+ }
+ return release;
+ }
+
+ private static ProjectReleaseRelationship getDefaultRelation() {
+ return new ProjectReleaseRelationship(ReleaseRelationship.UNKNOWN, MainlineState.OPEN);
+ }
+
+ private String getPackageName(PackageURL packageURL, org.cyclonedx.model.Component comp, String delimiter) {
+ String name = CommonUtils.nullToEmptyString(packageURL.getName());
+ if (CommonUtils.isNotNullEmptyOrWhitespace(packageURL.getNamespace())) {
+ return new StringBuilder(packageURL.getNamespace()).append(delimiter).append(name).toString();
+ } else if (CommonUtils.isNotNullEmptyOrWhitespace(comp.getGroup())) {
+ return new StringBuilder(comp.getGroup()).append(delimiter).append(name).toString();
+ } else if (CommonUtils.isNotNullEmptyOrWhitespace(comp.getPublisher())) {
+ //.replaceAll(CLEAN_PUBLISHER_REGEX, StringUtils.EMPTY) --> Use this to remove Publisher email id.
+ return new StringBuilder(comp.getPublisher()).append(delimiter).append(name).toString();
+ } else {
+ return name;
+ }
+ }
+
+ private Package createPackage(org.cyclonedx.model.Component componentFromBom, Release release, Set licenses) {
+ Package pckg = new Package();
+ String purl = componentFromBom.getPurl();
+ if (CommonUtils.isNotNullEmptyOrWhitespace(purl)) {
+ try {
+ purl = purl.toLowerCase().trim();
+ PackageURL packageURL = new PackageURL(purl);
+ pckg.setPurl(purl);
+ String packageName = StringUtils.EMPTY;
+ if (PackageManagerType.NPM.toString().equalsIgnoreCase(packageURL.getType())
+ || PackageManagerType.GOLANG.toString().equalsIgnoreCase(packageURL.getType())) {
+ packageName = getPackageName(packageURL, componentFromBom, SLASH).trim();
+ } else {
+ packageName = getPackageName(packageURL, componentFromBom, DOT).replaceAll(SLASH, DOT).trim();
+ }
+ pckg.setName(packageName);
+ pckg.setVersion(packageURL.getVersion());
+ } catch (MalformedPackageURLException e) {
+ log.error("Malformed PURL for component: " + componentFromBom.getName(), e);
+ return null;
+ }
+ } else {
+ return null;
+ }
+
+ if (release != null && release.isSetId()) {
+ pckg.setReleaseId(release.getId());
+ }
+ pckg.setDescription(CommonUtils.nullToEmptyString(componentFromBom.getDescription()).trim());
+
+ for (ExternalReference ref : CommonUtils.nullToEmptyList(componentFromBom.getExternalReferences())) {
+ if (ExternalReference.Type.WEBSITE.equals(ref.getType())) {
+ pckg.setHomepageUrl(ref.getUrl());
+ }
+ if (ExternalReference.Type.VCS.equals(ref.getType())) {
+ pckg.setVcs(ref.getUrl().replaceAll(VCS_HTTP_REGEX, HTTPS_SCHEME));
+ }
+ }
+
+ if (pckg.isSetLicenseIds()) {
+ pckg.getLicenseIds().addAll(licenses);
+ } else {
+ pckg.setLicenseIds(licenses);
+ }
+ return pckg;
+ }
+}
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/AttachmentAwareDatabaseHandler.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/AttachmentAwareDatabaseHandler.java
index 076cd224c3..34c50559a9 100644
--- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/AttachmentAwareDatabaseHandler.java
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/AttachmentAwareDatabaseHandler.java
@@ -28,6 +28,7 @@
import org.eclipse.sw360.datahandler.thrift.attachments.CheckStatus;
import org.eclipse.sw360.datahandler.thrift.components.Component;
import org.eclipse.sw360.datahandler.thrift.components.Release;
+import org.eclipse.sw360.datahandler.thrift.packages.Package;
import org.eclipse.sw360.datahandler.thrift.projects.Project;
import com.cloudant.client.api.CloudantClient;
@@ -101,6 +102,10 @@ protected void deleteAttachmentUsagesOfUnlinkedReleases(Source usedBy, Set httpClient, String dbName, String attachmentDbName, ComponentModerator moderator, ReleaseModerator releaseModerator, ProjectModerator projectModerator) throws MalformedURLException {
super(httpClient, dbName, attachmentDbName);
DatabaseConnectorCloudant db = new DatabaseConnectorCloudant(httpClient, dbName);
@@ -160,6 +165,7 @@ public ComponentDatabaseHandler(Supplier httpClient, String dbNa
componentRepository = new ComponentRepository(db, releaseRepository, vendorRepository);
projectRepository = new ProjectRepository(db);
userRepository = new UserRepository(db);
+ packageRepository = new PackageRepository(db);
// Create the moderator
this.moderator = moderator;
@@ -185,6 +191,7 @@ public ComponentDatabaseHandler(Supplier httpClient, String dbNa
public ComponentDatabaseHandler(Supplier supplier, String dbName, String attachmentDbName) throws MalformedURLException {
this(supplier, dbName, attachmentDbName, new ComponentModerator(), new ReleaseModerator(), new ProjectModerator());
}
+
public ComponentDatabaseHandler(Supplier supplier, String dbName, String changelogsDbName, String attachmentDbName) throws MalformedURLException {
this(supplier, dbName, attachmentDbName, new ComponentModerator(), new ReleaseModerator(), new ProjectModerator());
DatabaseConnectorCloudant db = new DatabaseConnectorCloudant(supplier, changelogsDbName);
@@ -488,7 +495,7 @@ public AddDocumentRequestSummary addRelease(Release release, User user) throws S
if(isDuplicate(release)) {
final AddDocumentRequestSummary addDocumentRequestSummary = new AddDocumentRequestSummary()
.setRequestStatus(AddDocumentRequestStatus.DUPLICATE);
- List duplicates = releaseRepository.searchByNameAndVersion(release.getName(), release.getVersion());
+ List duplicates = releaseRepository.searchByNameAndVersion(release.getName(), release.getVersion(), true);
if (duplicates.size() == 1) {
duplicates.stream()
.map(Release::getId)
@@ -497,7 +504,8 @@ public AddDocumentRequestSummary addRelease(Release release, User user) throws S
return addDocumentRequestSummary;
}
- if (!isDependenciesExistsInRelease(release)) {
+ if (!isDependenciesExistsInRelease(release)
+ || verifyLinkedPackages(Collections.emptySet(), CommonUtils.nullToEmptySet(release.getPackageIds()), "", user)) {
return new AddDocumentRequestSummary()
.setRequestStatus(AddDocumentRequestStatus.INVALID_INPUT);
}
@@ -542,7 +550,8 @@ public AddDocumentRequestSummary addRelease(Release release, User user) throws S
updateReleaseDependentFieldsForComponent(component, release);
updateModifiedFields(component, user.getEmail());
componentRepository.update(component);
-
+ // update linked packages
+ updateLinkedPackages(Collections.emptySet(), CommonUtils.nullToEmptySet(release.getPackageIds()), id, user);
sendMailNotificationsForNewRelease(release, user.getEmail());
dbHandlerUtil.addChangeLogs(release, null, user.getEmail(), Operation.CREATE, attachmentConnector,
Lists.newArrayList(), null, null);
@@ -573,7 +582,7 @@ private boolean isDuplicate(String releaseName, String releaseVersion) {
if (isNullEmptyOrWhitespace(releaseName)) {
return false;
}
- List duplicates = releaseRepository.searchByNameAndVersion(releaseName, releaseVersion);
+ List duplicates = releaseRepository.searchByNameAndVersion(releaseName, releaseVersion, true);
return duplicates.size()>0;
}
@@ -734,6 +743,11 @@ private boolean isDependenciesExistsInRelease(Release release) {
String vendorId = release.getVendorId();
isValidDependentIds = DatabaseHandlerUtil.isAllIdInSetExists(Sets.newHashSet(vendorId), vendorRepository);
}
+
+ if (isValidDependentIds && release.isSetPackageIds()) {
+ Set pacakgeIds = release.getPackageIds();
+ isValidDependentIds = DatabaseHandlerUtil.isAllIdInSetExists(pacakgeIds, packageRepository);
+ }
return isValidDependentIds;
}
@@ -1061,7 +1075,8 @@ public RequestStatus updateRelease(Release release, User user, Iterable permissions = makePermission(actual, user);
@@ -1107,6 +1122,8 @@ public RequestStatus updateRelease(Release release, User user, Iterable releases, User user) t
return RepositoryUtils.doBulk(prepareReleases(releases), user, releaseRepository);
}
- public RequestStatus updateReleaseFromAdditionsAndDeletions(Release releaseAdditions, Release releaseDeletions, User user){
+ public RequestStatus updateReleaseFromAdditionsAndDeletions(Release releaseAdditions, Release releaseDeletions, User user) {
try {
Release release = getRelease(releaseAdditions.getId(), user);
@@ -1342,6 +1359,74 @@ public Component updateReleaseDependentFieldsForComponentId(String componentId,
return component;
}
+ /**
+ * return false if verification is successful
+ * return true if verification is failed
+ * verify existence of newly linked packageIds
+ * verify all newly linked packages are orphan packages
+ **/
+ private boolean verifyLinkedPackages(Set currentPackageIds, Set updatedPackageIds, String releaseId, User user) throws SW360Exception {
+ Set addedPacakgeIds = Sets.difference(updatedPackageIds, currentPackageIds);
+ PackageService.Iface packageClient = new ThriftClients().makePackageClient();
+ if (CommonUtils.isNotEmpty(addedPacakgeIds)) {
+ try {
+ long addedCount = addedPacakgeIds.size();
+ List addedPackages = packageClient.getPackageByIds(addedPacakgeIds);
+ Predicate orphanReleaseFilter = pkg -> CommonUtils.isNullEmptyOrWhitespace(pkg.getReleaseId());
+ Predicate linkedReleaseFilter = pkg -> releaseId.equals(pkg.getReleaseId());
+ long orphanCount = addedPackages.stream().filter(orphanReleaseFilter).count();
+ long linkedCount = addedPackages.stream().filter(linkedReleaseFilter).count();
+ if (CommonUtils.isNotNullEmptyOrWhitespace(releaseId) && addedCount != orphanCount) {
+ return addedCount != linkedCount;
+ } else {
+ return addedCount != orphanCount;
+ }
+ } catch (TException e) {
+ log.error(String.format("An error occured while updating linked packages of release: %s", releaseId), e.getCause());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void updateLinkedPackages(Set currentPackageIds, Set updatedPackageIds, String releaseId, User user) throws SW360Exception {
+ Set removedPacakgeIds = Sets.difference(currentPackageIds, updatedPackageIds);
+ Set addedPacakgeIds = Sets.difference(updatedPackageIds, currentPackageIds);
+ PackageService.Iface packageClient = new ThriftClients().makePackageClient();
+ try {
+ if (CommonUtils.isNotEmpty(removedPacakgeIds)) {
+ List removedPackages = packageRepository.get(removedPacakgeIds);
+ for (Package pkg : removedPackages) {
+ String relId = pkg.getReleaseId();
+ // update the package, if it contains linked release Id
+ if (CommonUtils.isNotNullEmptyOrWhitespace(relId) && releaseId.equals(relId)) {
+ pkg.unsetReleaseId();
+ RequestStatus status = packageClient.updatePackage(pkg, user);
+ log.info(String.format("Unlinked package <%s> from release <%s>, Unlinking status: <%s>", pkg.getId(), releaseId, status.name()));
+ }
+ }
+ }
+ if (CommonUtils.isNotEmpty(addedPacakgeIds)) {
+ List addedPackages = packageClient.getPackageByIds(addedPacakgeIds);
+ for (Package pkg : addedPackages) {
+ String relId = pkg.getReleaseId();
+ // update only orphan packages
+ if (CommonUtils.isNullEmptyOrWhitespace(relId)) {
+ pkg.setReleaseId(releaseId);
+ RequestStatus status = packageClient.updatePackage(pkg, user);
+ log.info(String.format("Linked package <%s> to release <%s>, Linking status: <%s>", pkg.getId(), releaseId, status.name()));
+ } else if (!relId.equals(releaseId)) {
+ log.warn(String.format("Linked-ReleasId <%s> in Package <%s>, and Linked-PackageId <%s> in Release <%s> association is incorrect",
+ relId, pkg.getId(), pkg.getId(), releaseId));
+ }
+ }
+ }
+ } catch (TException e) {
+ log.error(String.format("An error occured while updating linked packages of release: %s", releaseId), e.getCause());
+ throw new SW360Exception(e.getMessage());
+ }
+ }
+
public void recomputeReleaseDependentFields(Component component, String skipThisReleaseId) {
resetReleaseDependentFields(component);
@@ -1771,7 +1856,7 @@ public RequestStatus deleteRelease(String id, User user, boolean forceDelete) th
Release release = releaseRepository.get(id);
assertNotNull(release);
- if (checkIfInUse(id)) return RequestStatus.IN_USE;
+ if (release.getPackageIdsSize() > 0 || checkIfInUse(id)) return RequestStatus.IN_USE;
if (makePermission(release, user).isActionAllowed(RequestedAction.DELETE) || forceDelete) {
Component componentBefore = componentRepository.get(release.getComponentId());
@@ -1968,6 +2053,11 @@ public List getReleases(Set ids) {
return releaseRepository.makeSummary(SummaryType.SHORT, ids);
}
+ // return direct release from db, without making summary.
+ public List getReleasesWithoutSummary(Set ids) {
+ return releaseRepository.get(ids);
+ }
+
public List getAccessibleReleases(Set ids, User user) {
return getAccessibleReleaseList(releaseRepository.makeSummary(SummaryType.SHORT, ids), user);
}
@@ -2192,7 +2282,7 @@ public String getCyclicLinkedReleasePath(Release release, User user) throws TExc
}
public List searchComponentByNameForExport(String name, boolean caseSensitive) {
- return componentRepository.searchByNameForExport(name, caseSensitive);
+ return componentRepository.searchComponentByName(name, caseSensitive);
}
public Set getUsingComponents(String releaseId) {
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java
index 3381f8dd92..4f3cd3fbb0 100644
--- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ComponentRepository.java
@@ -37,15 +37,15 @@
*/
public class ComponentRepository extends SummaryAwareRepository {
private static final String ALL = "function(doc) { if (doc.type == 'component') emit(null, doc._id) }";
- private static final String BYCREATEDON = "function(doc) { if(doc.type == 'component') { emit(doc.createdOn, doc._id) } }";
- private static final String USEDATTACHMENTCONTENTS = "function(doc) { " +
+ private static final String BY_CREATED_ON = "function(doc) { if(doc.type == 'component') { emit(doc.createdOn, doc._id) } }";
+ private static final String USED_ATTACHMENT_CONTENTS = "function(doc) { " +
" if(doc.type == 'release' || doc.type == 'component' || doc.type == 'project') {" +
" for(var i in doc.attachments){" +
" emit(null, doc.attachments[i].attachmentContentId);" +
" }" +
" }" +
"}";
- private static final String MYCOMPONENTS = "function(doc) {" +
+ private static final String MY_COMPONENTS = "function(doc) {" +
" if (doc.type == 'component') {" +
" emit(doc.createdBy, doc._id);" +
" } " +
@@ -57,29 +57,29 @@ public class ComponentRepository extends SummaryAwareRepository {
" }" +
" }" +
"}";
- private static final String BYNAME = "function(doc) {" +
+ private static final String BY_NAME = "function(doc) {" +
" if (doc.type == 'component') {" +
" emit(doc.name, doc._id);" +
" } " +
"}";
- private static final String BYCOMPONENTTYPE = "function(doc) {" +
+ private static final String BY_COMPONENT_TYPE = "function(doc) {" +
" if (doc.type == 'component') {" +
" emit(doc.componentType, doc._id);" +
" } " +
"}";
- private static final String FULLBYNAME = "function(doc) {" +
+ private static final String FULL_BY_NAME = "function(doc) {" +
" if (doc.type == 'component') {" +
" emit(doc.name, doc._id);" +
" } " +
"}";
- private static final String BYLINKINGRELEASE = "function(doc) {" +
+ private static final String BY_LINKING_RELEASE = "function(doc) {" +
" if (doc.type == 'release') {" +
" for(var i in doc.releaseIdToRelationship) {" +
" emit(i, doc.componentId);" +
" }" +
" }" +
"}";
- private static final String BYFOSSOLOGYID = "function(doc) {\n" +
+ private static final String BY_FOSSOLOGY_ID = "function(doc) {\n" +
" if (doc.type == 'release') {\n" +
" if (Array.isArray(doc.externalToolProcesses)) {\n" +
" for (var i = 0; i < doc.externalToolProcesses.length; i++) {\n" +
@@ -97,7 +97,7 @@ public class ComponentRepository extends SummaryAwareRepository {
" }\n" +
" }\n" +
"}";
- private static final String BYEXTERNALIDS = "function(doc) {" +
+ private static final String BY_EXTERNAL_IDS = "function(doc) {" +
" if (doc.type == 'component') {" +
" for (var externalId in doc.externalIds) {" +
" try {" +
@@ -115,19 +115,19 @@ public class ComponentRepository extends SummaryAwareRepository {
" }" +
" }" +
"}";
- private static final String BYDEFAULTVENDORID = "function(doc) {" +
+ private static final String BY_DEFAULT_VENDOR_ID = "function(doc) {" +
" if (doc.type == 'component') {" +
" emit( doc.defaultVendorId , doc._id);" +
" }" +
"}";
- private static final String BYNAMELOWERCASE = "function(doc) {" +
+ private static final String BY_NAME_LOWERCASE = "function(doc) {" +
" if (doc.type == 'component') {" +
- " emit(doc.name.toLowerCase(), doc._id);" +
+ " emit(doc.name.toLowerCase().trim(), doc._id);" +
" } " +
"}";
- private static final String BYMAINLICENSE = "function(doc) {" +
+ private static final String BY_MAIN_LICENSE = "function(doc) {" +
" if (doc.type == 'component') {" +
" if(doc.mainLicenseIds) {" +
" emit(doc.mainLicenseIds.join(), doc._id);" +
@@ -137,7 +137,7 @@ public class ComponentRepository extends SummaryAwareRepository {
" }" +
"}";
- private static final String BYVENDOR = "function(doc) {" +
+ private static final String BY_VENDOR = "function(doc) {" +
" if (doc.type == 'component') {" +
" if(doc.vendorNames) {" +
" emit(doc.vendorNames.join(), doc._id);" +
@@ -151,20 +151,20 @@ public ComponentRepository(DatabaseConnectorCloudant db, ReleaseRepository relea
super(Component.class, db, new ComponentSummary(releaseRepository, vendorRepository));
Map views = new HashMap();
views.put("all", createMapReduce(ALL, null));
- views.put("byCreatedOn", createMapReduce(BYCREATEDON, null));
- views.put("usedAttachmentContents", createMapReduce(USEDATTACHMENTCONTENTS, null));
- views.put("mycomponents", createMapReduce(MYCOMPONENTS, null));
+ views.put("byCreatedOn", createMapReduce(BY_CREATED_ON, null));
+ views.put("usedAttachmentContents", createMapReduce(USED_ATTACHMENT_CONTENTS, null));
+ views.put("mycomponents", createMapReduce(MY_COMPONENTS, null));
views.put("subscribers", createMapReduce(SUBSCRIBERS, null));
- views.put("byname", createMapReduce(BYNAME, null));
- views.put("bycomponenttype", createMapReduce(BYCOMPONENTTYPE, null));
- views.put("fullbyname", createMapReduce(FULLBYNAME, null));
- views.put("byLinkingRelease", createMapReduce(BYLINKINGRELEASE, null));
- views.put("byFossologyId", createMapReduce(BYFOSSOLOGYID, null));
- views.put("byExternalIds", createMapReduce(BYEXTERNALIDS, null));
- views.put("byDefaultVendorId", createMapReduce(BYDEFAULTVENDORID, null));
- views.put("bynamelowercase", createMapReduce(BYNAMELOWERCASE, null));
- views.put("bymainlicense", createMapReduce(BYMAINLICENSE, null));
- views.put("byvendor", createMapReduce(BYVENDOR, null));
+ views.put("byname", createMapReduce(BY_NAME, null));
+ views.put("bycomponenttype", createMapReduce(BY_COMPONENT_TYPE, null));
+ views.put("fullbyname", createMapReduce(FULL_BY_NAME, null));
+ views.put("byLinkingRelease", createMapReduce(BY_LINKING_RELEASE, null));
+ views.put("byFossologyId", createMapReduce(BY_FOSSOLOGY_ID, null));
+ views.put("byExternalIds", createMapReduce(BY_EXTERNAL_IDS, null));
+ views.put("byDefaultVendorId", createMapReduce(BY_DEFAULT_VENDOR_ID, null));
+ views.put("bynamelowercase", createMapReduce(BY_NAME_LOWERCASE, null));
+ views.put("bymainlicense", createMapReduce(BY_MAIN_LICENSE, null));
+ views.put("byvendor", createMapReduce(BY_VENDOR, null));
initStandardDesignDocument(views, db);
}
@@ -215,7 +215,7 @@ public Set getComponentIdsByName(String name, boolean caseInsenstive) {
return queryForIdsAsValue("byname", name);
}
- public List searchByNameForExport(String name, boolean caseSensitive) {
+ public List searchComponentByName(String name, boolean caseSensitive) {
Set componentIds;
if (caseSensitive) {
componentIds = queryForIdsAsValueByPrefix("fullbyname", name);
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java
index b9ae179cc3..6e06d91ed8 100644
--- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/DatabaseHandlerUtil.java
@@ -96,6 +96,7 @@
import org.eclipse.sw360.datahandler.thrift.components.Release;
import org.eclipse.sw360.datahandler.thrift.components.Repository;
import org.eclipse.sw360.datahandler.thrift.moderation.ModerationRequest;
+import org.eclipse.sw360.datahandler.thrift.packages.Package;
import org.eclipse.sw360.datahandler.thrift.projects.ObligationStatusInfo;
import org.eclipse.sw360.datahandler.thrift.projects.Project;
import org.eclipse.sw360.datahandler.thrift.projects.ProjectProjectRelationship;
@@ -379,6 +380,13 @@ public static void trimStringFields(T obj, List listOfStrFields) {
if (fieldValueObj instanceof String) {
eccInformation.setFieldValue(eccInformationField, fieldValueObj.toString().trim());
}
+ } else if (obj instanceof Package) {
+ Package._Fields pkgField = (Package._Fields) strField;
+ Package pkg = (Package) obj;
+ Object fieldValueObj = pkg.getFieldValue(pkgField);
+ if (fieldValueObj instanceof String) {
+ pkg.setFieldValue(pkgField, fieldValueObj.toString().trim());
+ }
}
});
}
@@ -400,9 +408,9 @@ public static void trimStringFields(T obj, List listOfStrFields) {
changeLog.setDocumentType(newProjVer.getType());
changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE);
} else if (newDocVersion instanceof ObligationList) {
- ObligationList newProjVer = (ObligationList) newDocVersion;
- changeLog.setDocumentId(newProjVer.getId());
- changeLog.setDocumentType(newProjVer.getType());
+ ObligationList newOblListVer = (ObligationList) newDocVersion;
+ changeLog.setDocumentId(newOblListVer.getId());
+ changeLog.setDocumentType(newOblListVer.getType());
changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE);
} else if (newDocVersion instanceof AttachmentContent) {
AttachmentContent newAttachmentContentVer = (AttachmentContent) newDocVersion;
@@ -412,19 +420,19 @@ public static void trimStringFields(T obj, List listOfStrFields) {
info.put(AttachmentContent._Fields.FILENAME.name(), newAttachmentContentVer.getFilename());
info.put(AttachmentContent._Fields.CONTENT_TYPE.name(), newAttachmentContentVer.getContentType());
} else if (newDocVersion instanceof Component) {
- Component newProjVer = (Component) newDocVersion;
- changeLog.setDocumentId(newProjVer.getId());
- changeLog.setDocumentType(newProjVer.getType());
+ Component newCompVer = (Component) newDocVersion;
+ changeLog.setDocumentId(newCompVer.getId());
+ changeLog.setDocumentType(newCompVer.getType());
changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE);
} else if (newDocVersion instanceof Release) {
- Release newProjVer = (Release) newDocVersion;
- changeLog.setDocumentId(newProjVer.getId());
- changeLog.setDocumentType(newProjVer.getType());
+ Release newRelVer = (Release) newDocVersion;
+ changeLog.setDocumentId(newRelVer.getId());
+ changeLog.setDocumentType(newRelVer.getType());
changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE);
} else if (newDocVersion instanceof ModerationRequest) {
- ModerationRequest newProjVer = (ModerationRequest) newDocVersion;
- changeLog.setDocumentId(newProjVer.getId());
- changeLog.setDocumentType(newProjVer.getType());
+ ModerationRequest newModReqVer = (ModerationRequest) newDocVersion;
+ changeLog.setDocumentId(newModReqVer.getId());
+ changeLog.setDocumentType(newModReqVer.getType());
changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE);
} else if (newDocVersion instanceof SPDXDocument) {
SPDXDocument newProjVer = (SPDXDocument) newDocVersion;
@@ -442,9 +450,14 @@ public static void trimStringFields(T obj, List listOfStrFields) {
changeLog.setDocumentType(newProjVer.getType());
changeLog.setDbName(DatabaseSettings.COUCH_DB_SPDX);
} else if (newDocVersion instanceof Obligation) {
- Obligation newProjVer = (Obligation) newDocVersion;
- changeLog.setDocumentId(newProjVer.getId());
- changeLog.setDocumentType(newProjVer.getType());
+ Obligation newOblVer = (Obligation) newDocVersion;
+ changeLog.setDocumentId(newOblVer.getId());
+ changeLog.setDocumentType(newOblVer.getType());
+ changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE);
+ } else if (newDocVersion instanceof Package) {
+ Package newPkgVer = (Package) newDocVersion;
+ changeLog.setDocumentId(newPkgVer.getId());
+ changeLog.setDocumentType(newPkgVer.getType());
changeLog.setDbName(DatabaseSettings.COUCH_DB_DATABASE);
}
@@ -576,7 +589,6 @@ private static Runnable prepareChangeLogRunnable(T newDocVersi
List referenceDocLogList, String parentDocId, Operation parentOperation) {
return () -> {
try {
- log.info("Generating ChangeLogs.");
ChangeLogs changeLogParent = initChangeLogsObj(newDocVersion, userEdited, parentDocId, operation,
parentOperation);
if (oldDocVersion == null) {
@@ -635,6 +647,8 @@ private static void changeLogsForNewlyCreatedOrDeleted(T newor
fields = PackageInformation._Fields.values();
} else if (neworDeletedVersion instanceof Obligation) {
fields = Obligation._Fields.values();
+ } else if (neworDeletedVersion instanceof Package) {
+ fields = Package._Fields.values();
} else {
return;
}
@@ -667,6 +681,8 @@ private static void changeLogsForNewlyCreatedOrDeleted(T newor
fields = PackageInformation._Fields.values();
} else if (newVersion instanceof Obligation) {
fields = Obligation._Fields.values();
+ } else if (newVersion instanceof Package) {
+ fields = Package._Fields.values();
} else {
return;
}
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageDatabaseHandler.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageDatabaseHandler.java
new file mode 100644
index 0000000000..ebd5be3a8b
--- /dev/null
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageDatabaseHandler.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright Siemens Healthineers GmBH, 2023. Part of the SW360 Portal Project.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.sw360.datahandler.db;
+
+import static org.eclipse.sw360.datahandler.common.CommonUtils.isNullEmptyOrWhitespace;
+import static org.eclipse.sw360.datahandler.common.SW360Assert.assertNotNull;
+import static org.eclipse.sw360.datahandler.common.SW360Assert.fail;
+
+import java.net.MalformedURLException;
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.apache.http.HttpStatus;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.eclipse.sw360.cyclonedx.CycloneDxBOMImporter;
+import org.eclipse.sw360.datahandler.cloudantclient.DatabaseConnectorCloudant;
+import org.eclipse.sw360.datahandler.common.CommonUtils;
+import org.eclipse.sw360.datahandler.common.Duration;
+import org.eclipse.sw360.datahandler.common.SW360Assert;
+import org.eclipse.sw360.datahandler.common.SW360Utils;
+import org.eclipse.sw360.datahandler.couchdb.AttachmentConnector;
+import org.eclipse.sw360.datahandler.thrift.AddDocumentRequestStatus;
+import org.eclipse.sw360.datahandler.thrift.AddDocumentRequestSummary;
+import org.eclipse.sw360.datahandler.thrift.PaginationData;
+import org.eclipse.sw360.datahandler.thrift.RequestStatus;
+import org.eclipse.sw360.datahandler.thrift.SW360Exception;
+import org.eclipse.sw360.datahandler.thrift.ThriftUtils;
+import org.eclipse.sw360.datahandler.thrift.ThriftValidate;
+import org.eclipse.sw360.datahandler.thrift.changelogs.Operation;
+import org.eclipse.sw360.datahandler.thrift.components.Release;
+import org.eclipse.sw360.datahandler.thrift.packages.Package;
+import org.eclipse.sw360.datahandler.thrift.packages.PackageManagerType;
+import org.eclipse.sw360.datahandler.thrift.users.User;
+
+import com.cloudant.client.api.CloudantClient;
+import com.cloudant.client.api.model.Response;
+import com.github.packageurl.MalformedPackageURLException;
+import com.github.packageurl.PackageURL;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+/**
+ * Class for accessing the CouchDB for Packages.
+ *
+ * @author: abdul.kapti@siemens-healthineers.com
+ */
+public class PackageDatabaseHandler extends AttachmentAwareDatabaseHandler {
+
+ private final AttachmentConnector attachmentConnector;
+ private final DatabaseConnectorCloudant db;
+ private final PackageRepository packageRepository;
+ private final ProjectRepository projectRepository;
+ private final ComponentDatabaseHandler componentDatabaseHandler;
+ private final DatabaseHandlerUtil databaseHandlerUtil;
+
+ private static final Logger log = LogManager.getLogger(CycloneDxBOMImporter.class);
+
+ private static final ImmutableList listOfStringFieldsInPackageToTrim = ImmutableList.of(
+ Package._Fields.NAME, Package._Fields.VERSION, Package._Fields.VCS, Package._Fields.DESCRIPTION,
+ Package._Fields.HOMEPAGE_URL, Package._Fields.PURL, Package._Fields.HASH);
+
+ public PackageDatabaseHandler(Supplier httpClient, String dbName, String attachmentDbName, String changeLogsDbName,
+ AttachmentDatabaseHandler attachmentDatabaseHandler, ComponentDatabaseHandler componentDatabaseHandler) throws MalformedURLException {
+
+ super(attachmentDatabaseHandler);
+ db = new DatabaseConnectorCloudant(httpClient, dbName);
+
+ // Create the repositories
+ packageRepository = new PackageRepository(db);
+ projectRepository = new ProjectRepository(db);
+
+ // Create the attachment connector
+ attachmentConnector = new AttachmentConnector(httpClient, attachmentDbName, Duration.durationOf(30, TimeUnit.SECONDS));
+
+ this.componentDatabaseHandler = componentDatabaseHandler;
+ DatabaseConnectorCloudant changeLogsDb = new DatabaseConnectorCloudant(httpClient, changeLogsDbName);
+ this.databaseHandlerUtil = new DatabaseHandlerUtil(changeLogsDb);
+ }
+
+ public PackageDatabaseHandler(Supplier httpClient, String dbName, String changeLogsDbName, String attachmentDbName)
+ throws MalformedURLException {
+
+ this(httpClient, dbName, attachmentDbName, changeLogsDbName, new AttachmentDatabaseHandler(httpClient, dbName, attachmentDbName),
+ new ComponentDatabaseHandler(httpClient, dbName, changeLogsDbName, attachmentDbName));
+ }
+
+ public Package getPackageById(String id) throws SW360Exception {
+ Package pkg = packageRepository.get(id);
+ assertNotNull(pkg, "Invalid Package Id");
+ return pkg;
+ }
+
+ public List getPackageByIds(Set ids) throws SW360Exception {
+ List packages = packageRepository.get(ids);
+ SW360Assert.assertEquals(ids.size(), CommonUtils.nullToEmptyList(packages).size(), "At least one package id was invalid!" );
+ return packages;
+ }
+
+ public List getPackageWithReleaseByPackageIds(Set ids) throws SW360Exception {
+ List packages = packageRepository.get(ids);
+ Set releaseIds = packages.stream().filter(pkg -> CommonUtils.isNotNullEmptyOrWhitespace(pkg.getReleaseId()))
+ .map(Package::getReleaseId).collect(Collectors.toSet());
+ if (CommonUtils.isNotEmpty(releaseIds)) {
+ List releases = componentDatabaseHandler.getReleasesWithoutSummary(releaseIds);
+ Map releaseIdToReleaseMap = releases.stream()
+ .map(rel -> new AbstractMap.SimpleEntry<>(rel.getId(), rel))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldVal, newVal) -> newVal));
+ packages.forEach(pkg -> {
+ if (CommonUtils.isNotNullEmptyOrWhitespace(pkg.getReleaseId())) {
+ pkg.setRelease(releaseIdToReleaseMap.get(pkg.getReleaseId()));
+ }
+ });
+ }
+ return packages;
+ }
+
+ public Set getPackagesByReleaseId(String id) {
+ Set packages = Sets.newHashSet();
+ packages.addAll(packageRepository.getPackagesByReleaseId(id));
+ return packages;
+ }
+
+ public Set getPackagesByReleaseIds(Set ids) {
+ Set packages = Sets.newHashSet();
+ for (String id : ids) {
+ packages.addAll(packageRepository.getPackagesByReleaseId(id));
+ }
+ return packages;
+ }
+
+ public List getAllPackages() {
+ return packageRepository.getAll();
+ }
+
+ public List getAllOrphanPackages() {
+ return packageRepository.getOrphanPackage();
+ }
+
+ public List searchPackages(PackageSearchHandler searchHandler, String searchText) {
+ return searchHandler.searchPackages(searchText);
+ }
+
+ public List searchPackagesWithFilter(String searchText, PackageSearchHandler searchHandler, Map> subQueryRestrictions) {
+ return searchHandler.searchPackagesWithRestrictions(searchText, subQueryRestrictions);
+ }
+
+ public int getTotalPackageCount() {
+ return packageRepository.getDocumentCount();
+ }
+
+ public List searchOrphanPackages(PackageSearchHandler searchHandler, String searchText) {
+ List packages = searchPackages(searchHandler, searchText);
+ Predicate orphanReleaseFilter = pkg -> CommonUtils.isNullEmptyOrWhitespace(pkg.getReleaseId());
+ return packages.stream().filter(orphanReleaseFilter).collect(Collectors.toList());
+ }
+
+ private void preparePackage(Package pkg) throws SW360Exception {
+ // Prepare package for database
+ ThriftValidate.preparePackage(pkg);
+ }
+
+ public AddDocumentRequestSummary addPackage(Package pkg, User user) throws SW360Exception {
+ removeLeadingTrailingWhitespace(pkg);
+ String name = pkg.getName();
+ String version = pkg.getVersion();
+
+ if (name == null || name.isEmpty() || version == null || version.isEmpty()) {
+ return new AddDocumentRequestSummary().setRequestStatus(AddDocumentRequestStatus.NAMINGERROR);
+ }
+
+ try {
+ PackageURL purl = new PackageURL(pkg.getPurl());
+ pkg.setPackageManagerType(PackageManagerType.valueOf(purl.getType().toUpperCase()));
+ } catch (MalformedPackageURLException e) {
+ log.error(String.format("Invalid PURL for package: '%s' with Id: '%s'", SW360Utils.printName(pkg), pkg.getId()), e);
+ return new AddDocumentRequestSummary().setRequestStatus(AddDocumentRequestStatus.INVALID_INPUT).setMessage("Invalid Pacakge URL");
+ } catch (IllegalArgumentException e) {
+ log.error(String.format("Invalid Package Manager type for package: '%s' with Id: '%s'", SW360Utils.printName(pkg), pkg.getId()), e);
+ return new AddDocumentRequestSummary().setRequestStatus(AddDocumentRequestStatus.INVALID_INPUT).setMessage("Invalid Pacakge Manager type");
+ }
+
+ // Prepare package for database
+ preparePackage(pkg);
+ List duplicatePackages = getPackageByNameAndVersion(name, version);
+ if (duplicatePackages.size() > 0) {
+ final AddDocumentRequestSummary addDocumentRequestSummary = new AddDocumentRequestSummary()
+ .setRequestStatus(AddDocumentRequestStatus.DUPLICATE);
+ if (duplicatePackages.size() == 1) {
+ addDocumentRequestSummary.setId(duplicatePackages.get(0).getId());
+ }
+ return addDocumentRequestSummary;
+ }
+ pkg.setCreatedBy(user.getEmail());
+ pkg.setCreatedOn(SW360Utils.getCreatedOn());
+ AddDocumentRequestSummary summary = new AddDocumentRequestSummary()
+ .setRequestStatus(AddDocumentRequestStatus.FAILURE);
+
+ // Ensure that release exists
+ if (CommonUtils.isNotNullEmptyOrWhitespace(pkg.getReleaseId())) {
+ try {
+ Release release = componentDatabaseHandler.getRelease(pkg.getReleaseId(), user);
+ // create new package
+ boolean updateStatus = packageRepository.add(pkg);
+ // Update the underlying release
+ if (updateStatus) {
+ release.addToPackageIds(pkg.getId());
+ componentDatabaseHandler.updateRelease(release, user, ThriftUtils.IMMUTABLE_OF_RELEASE, true);
+ summary.setMessage("Package successfully created with linked release");
+ } else {
+ log.error(String.format("Failed to add package: %s ", SW360Utils.printName(pkg)));
+ return new AddDocumentRequestSummary().setRequestStatus(AddDocumentRequestStatus.FAILURE).setMessage("Failed to add package!");
+ }
+ } catch (SW360Exception e) {
+ log.error(String.format("Invalid release id %s while adding package %s ", pkg.getReleaseId(), SW360Utils.printName(pkg)), e);
+ return new AddDocumentRequestSummary().setRequestStatus(AddDocumentRequestStatus.INVALID_INPUT).setMessage("Invalid Release Id");
+ }
+ } else {
+ // create new orphan package (without linked releases)
+ packageRepository.add(pkg);
+ summary.setMessage(String.format("An Orphan Package with id: <%s> successfully created (without linked release)", pkg.getId()));
+ }
+ databaseHandlerUtil.addChangeLogs(pkg, null, user.getEmail(), Operation.CREATE, attachmentConnector, Lists.newArrayList(), null, null);
+ return summary.setId(pkg.getId()).setRequestStatus(AddDocumentRequestStatus.SUCCESS);
+ }
+
+ public RequestStatus updatePackage(Package pkg, User user) throws SW360Exception {
+ return updatePackage(pkg, user, false);
+ }
+
+ public RequestStatus updatePackage(Package updatedPkg, User user, boolean forceUpdate) throws SW360Exception {
+ removeLeadingTrailingWhitespace(updatedPkg);
+ String name = updatedPkg.getName();
+ String version = updatedPkg.getVersion();
+ if (CommonUtils.isNullEmptyOrWhitespace(name) || CommonUtils.isNullEmptyOrWhitespace(version)) {
+ return RequestStatus.NAMINGERROR;
+ }
+
+ if (!SW360Utils.isWriteAccessUser(updatedPkg, user)) {
+ log.error(String.format("User %s does not have write access to package: %s ", user.getEmail(), updatedPkg.getId()));
+ return RequestStatus.ACCESS_DENIED;
+ }
+
+ if (CommonUtils.isNotNullEmptyOrWhitespace(updatedPkg.getReleaseId()) &&
+ DatabaseHandlerUtil.isAllIdInSetExists(Sets.newHashSet(updatedPkg.getReleaseId()), packageRepository)) {
+ log.error(String.format("Invalid linked release id %s for package: %s ", updatedPkg.getReleaseId(), updatedPkg.getId()));
+ return RequestStatus.INVALID_INPUT;
+ }
+
+ try {
+ PackageURL purl = new PackageURL(updatedPkg.getPurl());
+ updatedPkg.setPackageManagerType(PackageManagerType.valueOf(purl.getType().toUpperCase()));
+ } catch (MalformedPackageURLException e) {
+ log.error(String.format("Invalid PURL for package: %s - Error Message: %s", SW360Utils.printName(updatedPkg)), e);
+ return RequestStatus.INVALID_INPUT;
+ } catch (IllegalArgumentException e) {
+ log.error(String.format("Invalid Package Manager type for package: %s - Error Message: %s", SW360Utils.printName(updatedPkg)), e);
+ return RequestStatus.INVALID_INPUT;
+ }
+
+ // Prepare package for database
+ preparePackage(updatedPkg);
+ // Get actual document for members that should not change
+ String packageId = updatedPkg.getId();
+ Package actualPkg = packageRepository.get(packageId);
+ if (actualPkg == null) {
+ throw fail(404, String.format("Could not fetch package from database! id = %s", packageId));
+ }
+
+ if (changeWouldResultInDuplicate(actualPkg, updatedPkg)) {
+ return RequestStatus.DUPLICATE;
+ }
+
+ copyImmutableFields(updatedPkg, actualPkg);
+ updateModifiedFields(updatedPkg, user.getEmail());
+
+ String actualReleaseId = CommonUtils.nullToEmptyString(actualPkg.getReleaseId());
+ String newReleaseId = CommonUtils.nullToEmptyString(updatedPkg.getReleaseId());
+ // update linked release if there is a change in linked release.
+ if (!newReleaseId.equals(actualReleaseId)) {
+ // Ensure that release exists
+ try {
+ // once we are sure that release exists, update the package
+ Response resp = packageRepository.updatewithResponse(updatedPkg);
+ if (CommonUtils.isNotNullEmptyOrWhitespace(actualReleaseId)) {
+ Release actualRelease = componentDatabaseHandler.getRelease(actualReleaseId, user);
+ Set packageIds = CommonUtils.nullToEmptySet(actualRelease.getPackageIds());
+ // Update the previously linked release, if it contain package id
+ if (resp.getStatusCode() == HttpStatus.SC_CREATED) {
+ if (packageIds.contains(packageId)) {
+ packageIds.remove(packageId);
+ actualRelease.setPackageIds(packageIds);
+ componentDatabaseHandler.updateRelease(actualRelease, user, ThriftUtils.IMMUTABLE_OF_RELEASE, true);
+ } else {
+ log.info(String.format("Linked pacakgeId: %s is not present in release: %s", packageId, actualReleaseId));
+ }
+ } else {
+ log.error(String.format("Failed to update package: %s with id: %s", SW360Utils.printName(updatedPkg), packageId));
+ return RequestStatus.FAILURE;
+ }
+ }
+ if (CommonUtils.isNotNullEmptyOrWhitespace(newReleaseId)) {
+ Release newRelease = componentDatabaseHandler.getRelease(newReleaseId, user);
+ Set packageIds = CommonUtils.nullToEmptySet(newRelease.getPackageIds());
+ // Update the newly linked release, if it does not contain package id
+ if (resp.getStatusCode() == HttpStatus.SC_CREATED) {
+ if (!packageIds.contains(packageId)) {
+ newRelease.addToPackageIds(packageId);
+ componentDatabaseHandler.updateRelease(newRelease, user, ThriftUtils.IMMUTABLE_OF_RELEASE, true);
+ } else {
+ log.info(String.format("Linked pacakgeId: %s is already present in release: %s", packageId, newReleaseId));
+ }
+ } else {
+ log.error(String.format("Failed to update package: %s with id: %s", SW360Utils.printName(updatedPkg), packageId));
+ return RequestStatus.FAILURE;
+ }
+ }
+
+ } catch (SW360Exception e) {
+ log.error(String.format("Invalid release id %s while adding package %s ", newReleaseId, SW360Utils.printName(updatedPkg)), e);
+ return RequestStatus.INVALID_INPUT;
+ }
+ } else {
+ // update orphan package (without linked releases)
+ packageRepository.update(updatedPkg);
+ }
+ databaseHandlerUtil.addChangeLogs(updatedPkg, actualPkg, user.getEmail(), Operation.UPDATE, attachmentConnector, Lists.newArrayList(), null, null);
+ return RequestStatus.SUCCESS;
+ }
+
+ public RequestStatus deletePackage(String id, User user) throws SW360Exception {
+ return deletePackage(id, user, false);
+ }
+
+ public RequestStatus deletePackage(String id, User user, boolean forceDelete) throws SW360Exception {
+ Package pkg = packageRepository.get(id);
+ assertNotNull(pkg);
+
+ if (checkIfInUse(id)) {
+ return RequestStatus.IN_USE;
+ }
+
+ if (!SW360Utils.isWriteAccessUser(pkg, user)) {
+ log.error(String.format("User %s does not have write access to package: %s ", user.getEmail(), pkg.getId()));
+ return RequestStatus.ACCESS_DENIED;
+ }
+
+ packageRepository.remove(pkg);
+ RequestStatus status = cleanupPackageDependentFieldsInRelease(pkg, user);
+ if (RequestStatus.SUCCESS.equals(status)) {
+ databaseHandlerUtil.addChangeLogs(null, pkg, user.getEmail(), Operation.DELETE, attachmentConnector,
+ Lists.newArrayList(), null, null);
+ return status;
+ }
+ return status;
+ }
+
+ private boolean checkIfInUse(String packageId) {
+ return projectRepository.getCountByPackageId(packageId) > 0;
+ }
+
+ private RequestStatus cleanupPackageDependentFieldsInRelease(Package pkg, User user) throws SW360Exception {
+ String releaseId = pkg.getReleaseId();
+ if (CommonUtils.isNotNullEmptyOrWhitespace(releaseId)) {
+ Release release = componentDatabaseHandler.getRelease(releaseId, user);
+ Set packageIds = release.getPackageIds();
+ if (CommonUtils.isNotEmpty(packageIds) && packageIds.contains(pkg.getId())) {
+ packageIds.remove(pkg.getId());
+ release.setPackageIds(packageIds);
+ return componentDatabaseHandler.updateRelease(release, user, ThriftUtils.IMMUTABLE_OF_RELEASE, true);
+ }
+ }
+ return RequestStatus.SUCCESS;
+ }
+
+ private boolean changeWouldResultInDuplicate(Package before, Package after) {
+ if (before.getName().equalsIgnoreCase(after.getName())
+ && before.getVersion().equalsIgnoreCase(after.getVersion())) {
+ return false;
+ }
+ List duplicates = getPackageByNameAndVersion(after.getName(), after.getVersion());
+ return duplicates.size() > 0;
+ }
+
+ private List getPackageByNameAndVersion(String pkgName, String pkgVersion) {
+ if (isNullEmptyOrWhitespace(pkgName)) {
+ return Collections.emptyList();
+ }
+ return packageRepository.searchByNameAndVersion(pkgName, pkgVersion, true);
+ }
+
+ private void copyImmutableFields(Package destination, Package source) {
+ ThriftUtils.copyField(source, destination, Package._Fields.CREATED_ON);
+ ThriftUtils.copyField(source, destination, Package._Fields.CREATED_BY);
+ }
+
+ private void removeLeadingTrailingWhitespace(Package pkg) {
+ DatabaseHandlerUtil.trimStringFields(pkg, listOfStringFieldsInPackageToTrim);
+ pkg.setLicenseIds(DatabaseHandlerUtil.trimSetOfString(pkg.getLicenseIds()));
+ }
+
+ public Map> getPackagesWithPagination(PaginationData pageData) {
+ return packageRepository.getPackagesWithPagination(pageData);
+ }
+}
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageRepository.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageRepository.java
new file mode 100644
index 0000000000..33ff81fb31
--- /dev/null
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageRepository.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright Siemens Healthineers GmBH, 2023. Part of the SW360 Portal Project.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.sw360.datahandler.db;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.sw360.datahandler.cloudantclient.DatabaseConnectorCloudant;
+import org.eclipse.sw360.datahandler.cloudantclient.DatabaseRepositoryCloudantClient;
+import org.eclipse.sw360.datahandler.thrift.PaginationData;
+import org.eclipse.sw360.datahandler.thrift.packages.Package;
+import org.eclipse.sw360.datahandler.thrift.users.User;
+
+import com.cloudant.client.api.model.DesignDocument.MapReduce;
+import com.cloudant.client.api.views.Key;
+import com.cloudant.client.api.views.ViewRequest;
+import com.cloudant.client.api.views.ViewRequestBuilder;
+import com.cloudant.client.api.views.ViewResponse;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * CRUD access for the Package class
+ *
+ * @author abdul.kapti@siemens-healthineers.com
+ */
+
+public class PackageRepository extends DatabaseRepositoryCloudantClient {
+
+ private static final String ALL = "function(doc) { if (doc.type == 'package') emit(null, doc._id) }";
+ private static final String ORPHAN = "function(doc) { if (doc.type == 'package' && !doc.releaseId) emit(null, doc._id) }";
+ private static final String BY_NAME = "function(doc) { if (doc.type == 'package') { emit(doc.name.trim(), doc._id) } }";
+ private static final String BY_NAME_LOWERCASE = "function(doc) { if (doc.type == 'package') { emit(doc.name.toLowerCase().trim(), doc._id) } }";
+ private static final String BY_VESION_LOWERCASE = "function(doc) { if (doc.type == 'package') { emit(doc.version.toLowerCase().trim(), doc._id) } }";
+ private static final String BY_PKG_MANAGER_TYPE = "function(doc) { if (doc.type == 'package') { emit(doc.packageManagerType.toLowerCase().trim(), doc._id) } }";
+ private static final String BY_CREATOR = "function(doc) { if (doc.type == 'package') { emit(doc.createdBy, doc._id) } }";
+ private static final String BY_CREATED_ON = "function(doc) { if (doc.type == 'package') { emit(doc.createdOn, doc._id) } }";
+ private static final String BY_RELEASE_ID = "function(doc) { if (doc.type == 'package') { emit(doc.releaseId, doc._id); } }";
+ private static final String BY_LICENSE_IDS = "function(doc) { if (doc.type == 'package') { if (doc.licenseIds) { emit(doc.licenseIds.join(), doc._id); } else { emit('', doc._id); } } }";
+
+ public PackageRepository(DatabaseConnectorCloudant db) {
+ super(db, Package.class);
+ Map views = new HashMap();
+ views.put("all", createMapReduce(ALL, null));
+ views.put("orphan", createMapReduce(ORPHAN, null));
+ views.put("byName", createMapReduce(BY_NAME, null));
+ views.put("byNameLowerCase", createMapReduce(BY_NAME_LOWERCASE, null));
+ views.put("byVersionLowerCase", createMapReduce(BY_VESION_LOWERCASE, null));
+ views.put("byPackageManagerType", createMapReduce(BY_PKG_MANAGER_TYPE, null));
+ views.put("byCreator", createMapReduce(BY_CREATOR, null));
+ views.put("byCreatedOn", createMapReduce(BY_CREATED_ON, null));
+ views.put("byReleaseId", createMapReduce(BY_RELEASE_ID, null));
+ views.put("byLicenseIds", createMapReduce(BY_LICENSE_IDS, null));
+ initStandardDesignDocument(views, db);
+ }
+
+ public List getOrphanPackage() {
+ return queryView("orphan");
+ }
+
+ public List getPackagesByReleaseId(String id) {
+ return queryView("byReleaseId", id);
+ }
+
+ public List searchByName(String name, User user) {
+ return queryView("byName", name);
+ }
+
+ public List searchByNameLowerCase(String name, User user) {
+ return queryView("byNameLowerCase", name.toLowerCase());
+ }
+
+ public List searchByVersionLowerCase(String version, User user) {
+ return queryView("byVersionLowerCase", version.toLowerCase());
+ }
+
+ public List searchByPackageManagerType(String pkgType, User user) {
+ return queryView("byPackageManagerType", pkgType.toUpperCase());
+ }
+
+ public List searchByCreator(String email, User user) {
+ return queryView("byCreator", email);
+ }
+
+ public List searchByLicenseeId(String id) {
+ return queryView("byLicenseIds", id);
+ }
+
+ public List searchByNameAndVersion(String name, String version, boolean caseInsenstive) {
+ List packagesMatchingName;
+ if (caseInsenstive) {
+ packagesMatchingName = new ArrayList(queryView("byNameLowerCase", name.toLowerCase()));
+ } else {
+ packagesMatchingName = new ArrayList(queryView("byName", name));
+ }
+ List releasesMatchingNameAndVersion = packagesMatchingName.stream()
+ .filter(r -> isNullOrEmpty(version) ? isNullOrEmpty(r.getVersion()) : version.equalsIgnoreCase(r.getVersion()))
+ .collect(Collectors.toList());
+ return releasesMatchingNameAndVersion;
+ }
+
+ public Map> getPackagesWithPagination(PaginationData pageData) {
+ final int rowsPerPage = pageData.getRowsPerPage();
+ Map> result = Maps.newHashMap();
+ List packages = Lists.newArrayList();
+ final boolean ascending = pageData.isAscending();
+ final int sortColumnNo = pageData.getSortColumnNumber();
+
+ ViewRequestBuilder query;
+ switch (sortColumnNo) {
+ case -1:
+ query = getConnector().createQuery(Package.class, "byCreatedOn");
+ break;
+ case 0:
+ query = getConnector().createQuery(Package.class, "byNameLowerCase");
+ break;
+ case 3:
+ query = getConnector().createQuery(Package.class, "byLicenseIds");
+ break;
+ case 4:
+ query = getConnector().createQuery(Package.class, "byPackageManagerType");
+ break;
+ default:
+ query = getConnector().createQuery(Package.class, "all");
+ break;
+ }
+
+ ViewRequest request = null;
+ if (rowsPerPage == -1) {
+ request = query.newRequest(Key.Type.STRING, Object.class).descending(!ascending).includeDocs(true).build();
+ } else {
+ request = query.newPaginatedRequest(Key.Type.STRING, Object.class).rowsPerPage(rowsPerPage)
+ .descending(!ascending).includeDocs(true).build();
+ }
+
+ ViewResponse response = null;
+ try {
+ response = request.getResponse();
+ int pageNo = pageData.getDisplayStart() / rowsPerPage;
+ int i = 1;
+ while (i <= pageNo) {
+ response = response.nextPage();
+ i++;
+ }
+ packages = response.getDocsAs(Package.class);
+ } catch (Exception e) {
+ log.error("Error getting packages from repository: ", e);
+ }
+ pageData.setTotalRowCount(response.getTotalRowCount());
+ result.put(pageData, packages);
+ return result;
+ }
+}
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageSearchHandler.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageSearchHandler.java
new file mode 100644
index 0000000000..1f8e9d7bbf
--- /dev/null
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/PackageSearchHandler.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright Siemens Healthineers GmBH, 2023. Part of the SW360 Portal Project.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.sw360.datahandler.db;
+
+import org.eclipse.sw360.datahandler.couchdb.lucene.LuceneAwareDatabaseConnector;
+import org.eclipse.sw360.datahandler.couchdb.lucene.LuceneSearchView;
+import org.eclipse.sw360.datahandler.thrift.packages.Package;
+import org.ektorp.http.HttpClient;
+
+import com.cloudant.client.api.CloudantClient;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import static org.eclipse.sw360.datahandler.couchdb.lucene.LuceneAwareDatabaseConnector.prepareWildcardQuery;
+
+public class PackageSearchHandler {
+
+ private static final LuceneSearchView luceneSearchView = new LuceneSearchView("lucene", "packages",
+ "function(doc) {" +
+ " var ret = new Document();" +
+ " if (!doc.type) return ret;" +
+ " if (doc.type != 'package') return ret;" +
+ " function idx(obj) {" +
+ " for (var key in obj) {" +
+ " switch (typeof obj[key]) {" +
+ " case 'object':" +
+ " idx(obj[key]);" +
+ " break;" +
+ " case 'function':" +
+ " break;" +
+ " default:" +
+ " ret.add(obj[key]);" +
+ " break;" +
+ " }" +
+ " }" +
+ " };" +
+ " idx(doc);" +
+ " if (doc.name) { "+
+ " ret.add(doc.name, {\"field\": \"name\"} );" +
+ " }" +
+ " if (doc.version) { "+
+ " ret.add(doc.version, {\"field\": \"version\"} );" +
+ " }" +
+ " if (doc.purl) { "+
+ " ret.add(doc.purl, {\"field\": \"purl\"} );" +
+ " }" +
+ " if (doc.releaseId) { "+
+ " ret.add(doc.releaseId, {\"field\": \"releaseId\"} );" +
+ " }" +
+ " if (doc.vcs) { "+
+ " ret.add(doc.vcs, {\"field\": \"vcs\"} );" +
+ " }" +
+ " if (doc.packageManagerType) { "+
+ " ret.add(doc.packageManagerType, {\"field\": \"packageManagerType\"} );" +
+ " }" +
+ " if (doc.createdBy) { "+
+ " ret.add(doc.createdBy, {\"field\": \"createdBy\"} );" +
+ " }" +
+ " if (doc.createdOn) { "+
+ " ret.add(doc.createdOn, {\"field\": \"createdOn\", \"type\": \"date\"} );" +
+ " }" +
+ " for (var i in doc.licenseIds) {" +
+ " ret.add(doc.licenseIds[i], {\"field\": \"licenseIds\"} );" +
+ " }" +
+ " return ret;" +
+ "}");
+
+
+ private final LuceneAwareDatabaseConnector connector;
+
+ public PackageSearchHandler(Supplier httpClient, Supplier cloudantClient, String dbName) throws IOException {
+ connector = new LuceneAwareDatabaseConnector(httpClient, cloudantClient, dbName);
+ connector.addView(luceneSearchView);
+ }
+
+ public List searchPackagesWithRestrictions(String text, final Map> subQueryRestrictions) {
+ return connector.searchViewWithRestrictions(Package.class, luceneSearchView, text, subQueryRestrictions);
+ }
+
+ public List searchPackages(String searchText) {
+ return connector.searchView(Package.class, luceneSearchView, prepareWildcardQuery(searchText));
+ }
+
+}
diff --git a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java
index 232aa8ee53..5fa83ccb8e 100644
--- a/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java
+++ b/backend/src-common/src/main/java/org/eclipse/sw360/datahandler/db/ProjectDatabaseHandler.java
@@ -17,11 +17,14 @@
import com.google.common.collect.*;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
+
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.thrift.TException;
import org.eclipse.sw360.common.utils.BackendUtils;
import org.eclipse.sw360.components.summary.SummaryType;
+import org.eclipse.sw360.cyclonedx.CycloneDxBOMImporter;
import org.eclipse.sw360.datahandler.businessrules.ReleaseClearingStateSummaryComputer;
import org.eclipse.sw360.datahandler.cloudantclient.DatabaseConnectorCloudant;
import org.eclipse.sw360.datahandler.common.*;
@@ -37,6 +40,8 @@
import org.eclipse.sw360.datahandler.thrift.changelogs.Operation;
import org.eclipse.sw360.datahandler.thrift.components.*;
import org.eclipse.sw360.datahandler.thrift.moderation.ModerationRequest;
+import org.eclipse.sw360.datahandler.thrift.packages.Package;
+import org.eclipse.sw360.datahandler.thrift.packages.PackageService;
import org.eclipse.sw360.datahandler.thrift.projects.*;
import org.eclipse.sw360.datahandler.thrift.users.RequestedAction;
import org.eclipse.sw360.datahandler.thrift.users.User;
@@ -54,6 +59,7 @@
import java.io.*;
import java.net.MalformedURLException;
+import java.nio.charset.Charset;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.TimeUnit;
@@ -96,6 +102,8 @@ public class ProjectDatabaseHandler extends AttachmentAwareDatabaseHandler {
private final ProjectModerator moderator;
private final AttachmentConnector attachmentConnector;
private final ComponentDatabaseHandler componentDatabaseHandler;
+ private final PackageDatabaseHandler packageDatabaseHandler;
+ private final PackageRepository packageRepository;
private static final Pattern PLAUSIBLE_GID_REGEXP = Pattern.compile("^[zZ].{7}$");
private final RelationsUsageRepository relUsageRepository;
@@ -134,27 +142,29 @@ public class ProjectDatabaseHandler extends AttachmentAwareDatabaseHandler {
public ProjectDatabaseHandler(Supplier httpClient, String dbName, String attachmentDbName) throws MalformedURLException {
this(httpClient, dbName, attachmentDbName, new ProjectModerator(),
new ComponentDatabaseHandler(httpClient,dbName,attachmentDbName),
+ new PackageDatabaseHandler(httpClient, dbName, DatabaseSettings.COUCH_DB_CHANGE_LOGS, attachmentDbName),
new AttachmentDatabaseHandler(httpClient, dbName, attachmentDbName));
}
public ProjectDatabaseHandler(Supplier httpClient, String dbName, String changeLogDbName, String attachmentDbName) throws MalformedURLException {
this(httpClient, dbName, changeLogDbName, attachmentDbName, new ProjectModerator(),
- new ComponentDatabaseHandler(httpClient,dbName,attachmentDbName),
+ new ComponentDatabaseHandler(httpClient, dbName, attachmentDbName),
+ new PackageDatabaseHandler(httpClient, dbName, changeLogDbName, attachmentDbName),
new AttachmentDatabaseHandler(httpClient, dbName, attachmentDbName));
}
@VisibleForTesting
public ProjectDatabaseHandler(Supplier httpClient, String dbName, String changeLogsDbName, String attachmentDbName, ProjectModerator moderator,
- ComponentDatabaseHandler componentDatabaseHandler,
+ ComponentDatabaseHandler componentDatabaseHandler, PackageDatabaseHandler packageDatabaseHandler,
AttachmentDatabaseHandler attachmentDatabaseHandler) throws MalformedURLException {
- this(httpClient, dbName, attachmentDbName, moderator, componentDatabaseHandler, attachmentDatabaseHandler);
+ this(httpClient, dbName, attachmentDbName, moderator, componentDatabaseHandler, packageDatabaseHandler, attachmentDatabaseHandler);
DatabaseConnectorCloudant db = new DatabaseConnectorCloudant(httpClient, changeLogsDbName);
this.dbHandlerUtil = new DatabaseHandlerUtil(db);
}
@VisibleForTesting
public ProjectDatabaseHandler(Supplier httpClient, String dbName, String attachmentDbName, ProjectModerator moderator,
- ComponentDatabaseHandler componentDatabaseHandler,
+ ComponentDatabaseHandler componentDatabaseHandler, PackageDatabaseHandler packageDatabaseHandler,
AttachmentDatabaseHandler attachmentDatabaseHandler) throws MalformedURLException {
super(attachmentDatabaseHandler);
DatabaseConnectorCloudant db = new DatabaseConnectorCloudant(httpClient, dbName);
@@ -166,6 +176,7 @@ public ProjectDatabaseHandler(Supplier httpClient, String dbName
relUsageRepository = new RelationsUsageRepository(db);
vendorRepository = new VendorRepository(db);
releaseRepository = new ReleaseRepository(db, vendorRepository);
+ packageRepository = new PackageRepository(db);
// Create the moderator
this.moderator = moderator;
@@ -174,6 +185,7 @@ public ProjectDatabaseHandler(Supplier httpClient, String dbName
attachmentConnector = new AttachmentConnector(httpClient, attachmentDbName, Duration.durationOf(30, TimeUnit.SECONDS));
this.componentDatabaseHandler = componentDatabaseHandler;
+ this.packageDatabaseHandler = packageDatabaseHandler;
DatabaseConnectorCloudant dbChangelogs = new DatabaseConnectorCloudant(httpClient, DatabaseSettings.COUCH_DB_CHANGE_LOGS);
this.dbHandlerUtil = new DatabaseHandlerUtil(dbChangelogs);
}
@@ -359,7 +371,7 @@ public AddDocumentRequestSummary addProject(Project project, User user) throws S
return addDocumentRequestSummary;
}
- if (!isDependenciesExists(project, user)) {
+ if (!isDependenciesExists(project, user) || (SW360Constants.IS_PACKAGE_PORTLET_ENABLED && isLinkedReleasesUpdateFromLinkedPackagesFailed(project, Collections.emptySet()))) {
return new AddDocumentRequestSummary()
.setRequestStatus(AddDocumentRequestStatus.INVALID_INPUT);
}
@@ -417,7 +429,8 @@ public RequestStatus updateProject(Project project, User user, boolean forceUpda
return RequestStatus.CLOSED_UPDATE_NOT_ALLOWED;
} else if (!changePassesSanityCheck(project, actual)){
return RequestStatus.FAILED_SANITY_CHECK;
- } else if (!isDependenciesExists(project, user)) {
+ } else if (!isDependenciesExists(project, user) || (SW360Constants.IS_PACKAGE_PORTLET_ENABLED &&
+ isLinkedReleasesUpdateFromLinkedPackagesFailed(project, CommonUtils.nullToEmptySet(actual.getPackageIds())))) {
return RequestStatus.INVALID_INPUT;
} else if (isWriteActionAllowedOnProject(actual, user) || forceUpdate) {
copyImmutableFields(project,actual);
@@ -518,6 +531,11 @@ private boolean isDependenciesExists(Project project, User user) {
isValidDependentIds = DatabaseHandlerUtil.isAllIdInSetExists(Sets.newHashSet(project.getVendorId()),
vendorRepository);
}
+
+ if (isValidDependentIds && project.isSetPackageIds()) {
+ Set pacakgeIds = project.getPackageIds();
+ isValidDependentIds = DatabaseHandlerUtil.isAllIdInSetExists(pacakgeIds, packageRepository);
+ }
return isValidDependentIds;
}
@@ -724,6 +742,81 @@ private void updateProjectDependentLinkedFields(Project updated, Project actual)
}
}
+ /**
+ * Link the Release of newly linked Packages to the Project.
+ * Unlink the Release of unlinked Packages from the Project.
+ * @return true if linking or unlinking is failed, else returns false.
+ */
+ private boolean isLinkedReleasesUpdateFromLinkedPackagesFailed(Project updatedProject, Set currentPackageIds) throws SW360Exception {
+ Set updatedPackageIds = CommonUtils.nullToEmptySet(updatedProject.getPackageIds());
+ if (updatedPackageIds.equals(currentPackageIds)) {
+ return false;
+ }
+ Set linkedPacakgeIds = Sets.difference(updatedPackageIds, currentPackageIds);
+ Set unlinkedPacakgeIds = Sets.difference(currentPackageIds, updatedPackageIds);
+ final ProjectReleaseRelationship releaseRelation = new ProjectReleaseRelationship(ReleaseRelationship.UNKNOWN, MainlineState.OPEN);
+ if (CommonUtils.isNotEmpty(linkedPacakgeIds)) {
+ try {
+ PackageService.Iface packageClient = new ThriftClients().makePackageClient();
+ List addedPackages = packageClient.getPackageByIds(linkedPacakgeIds);
+
+ Map releaseIdToUsageMap = addedPackages.stream().map(Package::getReleaseId)
+ .filter(CommonUtils::isNotNullEmptyOrWhitespace)
+ .map(relId -> new AbstractMap.SimpleEntry<>(relId, releaseRelation))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldVal, newVal) -> newVal));
+
+ /* Check if the releaseId found from linked Packages are present in database.
+ * If present, then link the Release to the Project.
+ * If not present, then return true (status failed).
+ */
+ if (DatabaseHandlerUtil.isAllIdInSetExists(releaseIdToUsageMap.keySet(), releaseRepository)) {
+ Map targetMap = CommonUtils.nullToEmptyMap(updatedProject.getReleaseIdToUsage());
+ if (CommonUtils.isNullOrEmptyMap(targetMap)) {
+ updatedProject.setReleaseIdToUsage(releaseIdToUsageMap);
+ } else {
+ for (Map.Entry entry : releaseIdToUsageMap.entrySet()) {
+ targetMap.putIfAbsent(entry.getKey(), entry.getValue());
+ }
+ }
+ } else {
+ return true;
+ }
+ } catch (TException e) {
+ log.error(String.format("Error fetching newly added linked package info of project: %s", updatedProject.getId()), e.getCause());
+ throw new SW360Exception(e.getMessage());
+ }
+ }
+
+ if (CommonUtils.isNotEmpty(unlinkedPacakgeIds)) {
+ try {
+ PackageService.Iface packageClient = new ThriftClients().makePackageClient();
+ List removedPackages = packageClient.getPackageWithReleaseByPackageIds(unlinkedPacakgeIds);
+
+ Map> releaseIdToPackageIdsMap = removedPackages.stream().map(Package::getRelease)
+ .filter(rel -> CommonUtils.isNotEmpty(rel.getPackageIds()))
+ .map(rel -> new AbstractMap.SimpleEntry<>(rel.getId(), rel.getPackageIds()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldVal, newVal) -> newVal));
+
+ Map targetMap = CommonUtils.nullToEmptyMap(updatedProject.getReleaseIdToUsage());
+
+ /* If Project contains releaseId of unlinked Package,
+ * & check if Project contains at least one Package from the Release of unlinked Package.
+ * If No, unlink the Release from Project.
+ */
+ for (Map.Entry> entry : releaseIdToPackageIdsMap.entrySet()) {
+ if (targetMap.containsKey(entry.getKey()) &&
+ CommonUtils.isNotEmpty(CommonUtils.nullToEmptySet(Sets.intersection(entry.getValue(), unlinkedPacakgeIds)))) {
+ targetMap.remove(entry.getKey());
+ }
+ }
+ } catch (TException e) {
+ log.error(String.format("Error fetching removed linked package info of project: %s", updatedProject.getId()), e.getCause());
+ throw new SW360Exception(e.getMessage());
+ }
+ }
+ return false;
+ }
+
private boolean changePassesSanityCheck(Project updated, Project current) {
return !(nullToEmptyMap(current.getReleaseIdToUsage()).size() > DELETION_SANITY_CHECK_THRESHOLD &&
nullToEmptyMap(updated.getReleaseIdToUsage()).size() == 0 ||
@@ -790,6 +883,18 @@ public boolean checkIfInUse(String projectId) {
return !usingProjects.isEmpty();
}
+ public Set searchByPackageId(String id, User user) {
+ return repository.searchByPackageId(id, user);
+ }
+
+ public Set searchByPackageIds(Set ids, User user) {
+ return repository.searchByPackageIds(ids, user);
+ }
+
+ public int getProjectCountByPackageId(String packageId) {
+ return repository.getCountByPackageId(packageId);
+ }
+
private void removeProjectAndCleanUp(Project project, User user) throws SW360Exception {
attachmentConnector.deleteAttachments(project.getAttachments());
attachmentDatabaseHandler.deleteUsagesBy(Source.projectId(project.getId()));
@@ -1096,7 +1201,7 @@ public List getReleaseClearingStatusesWithAccessibili
ReleaseClearingStatusData releaseClearingStatusData = new ReleaseClearingStatusData(release)
.setProjectNames(joinStrings(projectNames))
.setMainlineStates(joinStrings(mainlineStates))
- .setComponentType(componentsById.get(release.getComponentId()).getComponentType());
+ .setComponentType(componentsById.get(release.getComponentId()).getComponentType());
boolean isAccessible = componentDatabaseHandler.isReleaseActionAllowed(release, user, RequestedAction.READ);
releaseClearingStatusData.setAccessible(isAccessible);
@@ -1696,6 +1801,38 @@ public RequestSummary importBomFromAttachmentContent(User user, String attachmen
}
}
+ public RequestSummary importCycloneDxFromAttachmentContent(User user, String attachmentContentId, String projectId) throws SW360Exception {
+ final AttachmentContent attachmentContent = attachmentConnector.getAttachmentContent(attachmentContentId);
+ final Duration timeout = Duration.durationOf(30, TimeUnit.SECONDS);
+ try {
+ final AttachmentStreamConnector attachmentStreamConnector = new AttachmentStreamConnector(timeout);
+ try (final InputStream inputStream = attachmentStreamConnector
+ .unsafeGetAttachmentStream(attachmentContent)) {
+ final CycloneDxBOMImporter cycloneDxBOMImporter = new CycloneDxBOMImporter(this,
+ componentDatabaseHandler, packageDatabaseHandler, attachmentConnector, user);
+ return cycloneDxBOMImporter.importFromBOM(inputStream, attachmentContent, projectId, user);
+ }
+ } catch (IOException e) {
+ log.error("Exception while parsing CycloneDX SBOM! ", e);
+ throw new SW360Exception(e.getMessage());
+ }
+ }
+
+ public String getSbomImportInfoFromAttachmentAsString(String attachmentContentId) throws SW360Exception {
+ final AttachmentContent attachmentContent = attachmentConnector.getAttachmentContent(attachmentContentId);
+ final Duration timeout = Duration.durationOf(30, TimeUnit.SECONDS);
+ try {
+ final AttachmentStreamConnector attachmentStreamConnector = new AttachmentStreamConnector(timeout);
+ try (final InputStream inputStream = attachmentStreamConnector
+ .unsafeGetAttachmentStream(attachmentContent)) {
+ return IOUtils.toString(inputStream, Charset.defaultCharset());
+ }
+ } catch (IOException e) {
+ log.error("Error while getting sbom import info from attachment! ", e);
+ throw new SW360Exception(e.getMessage());
+ }
+ }
+
private void removeLeadingTrailingWhitespace(Project project) {
DatabaseHandlerUtil.trimStringFields(project, listOfStringFieldsInProjToTrim);
@@ -1778,7 +1915,7 @@ private void flattenClearingStatusForReleases(Map releaseIdToRelationship = rel.getReleaseIdToRelationship();
releaseOrigin.put(releaseId, SW360Utils.printName(rel));
@@ -1808,7 +1945,7 @@ private void flattenlinkedReleaseOfRelease(Map rele
if (releaseOrigin.containsKey(releaseId))
return;
Release rel = componentDatabaseHandler.getRelease(releaseId, user);
-
+
if (!isInaccessibleLinkMasked || componentDatabaseHandler.isReleaseActionAllowed(rel, user, RequestedAction.READ)) {
Map subReleaseIdToRelationship = rel.getReleaseIdToRelationship();
releaseOrigin.put(releaseId, SW360Utils.printName(rel));
@@ -1870,7 +2007,7 @@ private Map createReleaseCSRow(String relation, String projectMa
clearingStatusList.add(row);
return row;
}
-
+
private Map createInaccessibleReleaseCSRow(List