2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2019 Nordix Foundation.
4 * ================================================================================
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.openecomp.sdc.vendorsoftwareproduct.impl.orchestration.csar.validation;
24 import static org.openecomp.sdc.be.config.NonManoArtifactType.ONAP_PM_DICTIONARY;
25 import static org.openecomp.sdc.be.config.NonManoArtifactType.ONAP_SW_INFORMATION;
26 import static org.openecomp.sdc.be.config.NonManoArtifactType.ONAP_VES_EVENTS;
27 import static org.openecomp.sdc.tosca.csar.CSARConstants.CSAR_VERSION_1_0;
28 import static org.openecomp.sdc.tosca.csar.CSARConstants.CSAR_VERSION_1_1;
29 import static org.openecomp.sdc.tosca.csar.CSARConstants.MANIFEST_METADATA_LIMIT;
30 import static org.openecomp.sdc.tosca.csar.CSARConstants.MANIFEST_PNF_METADATA;
31 import static org.openecomp.sdc.tosca.csar.CSARConstants.MANIFEST_VNF_METADATA;
32 import static org.openecomp.sdc.tosca.csar.CSARConstants.TOSCA_MANIFEST_FILE_EXT;
33 import static org.openecomp.sdc.tosca.csar.CSARConstants.TOSCA_TYPE_PNF;
34 import static org.openecomp.sdc.tosca.csar.CSARConstants.TOSCA_TYPE_VNF;
35 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.CREATED_BY_ENTRY;
36 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.CSAR_VERSION_ENTRY;
37 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.ENTRY_DEFINITIONS;
38 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.ETSI_ENTRY_CERTIFICATE;
39 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.ETSI_ENTRY_MANIFEST;
40 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.TOSCA_META_FILE_VERSION;
41 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.TOSCA_META_FILE_VERSION_ENTRY;
42 import static org.openecomp.sdc.tosca.csar.ToscaMetaEntry.TOSCA_META_PATH_FILE_NAME;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
52 import java.util.Optional;
54 import java.util.concurrent.CopyOnWriteArrayList;
55 import java.util.stream.Collectors;
56 import org.apache.commons.collections.CollectionUtils;
57 import org.apache.commons.io.FilenameUtils;
58 import org.openecomp.core.impl.ToscaDefinitionImportHandler;
59 import org.openecomp.core.utilities.file.FileContentHandler;
60 import org.openecomp.sdc.be.config.NonManoArtifactType;
61 import org.openecomp.sdc.be.csar.pnf.PnfSoftwareInformation;
62 import org.openecomp.sdc.be.csar.pnf.SoftwareInformationArtifactYamlParser;
63 import org.openecomp.sdc.be.datatypes.enums.ResourceTypeEnum;
64 import org.openecomp.sdc.common.errors.Messages;
65 import org.openecomp.sdc.common.utils.SdcCommon;
66 import org.openecomp.sdc.datatypes.error.ErrorLevel;
67 import org.openecomp.sdc.datatypes.error.ErrorMessage;
68 import org.openecomp.sdc.logging.api.Logger;
69 import org.openecomp.sdc.logging.api.LoggerFactory;
70 import org.openecomp.sdc.tosca.csar.Manifest;
71 import org.openecomp.sdc.tosca.csar.OnboardingToscaMetadata;
72 import org.openecomp.sdc.tosca.csar.SOL004ManifestOnboarding;
73 import org.openecomp.sdc.tosca.csar.ToscaMetaEntry;
74 import org.openecomp.sdc.tosca.csar.ToscaMetadata;
75 import org.openecomp.sdc.vendorsoftwareproduct.impl.onboarding.OnboardingPackageContentHandler;
76 import org.openecomp.sdc.vendorsoftwareproduct.impl.orchestration.csar.validation.exception.MissingCertificateException;
77 import org.openecomp.sdc.vendorsoftwareproduct.impl.orchestration.exceptions.InvalidManifestMetadataException;
78 import org.openecomp.sdc.vendorsoftwareproduct.security.SecurityManager;
79 import org.openecomp.sdc.vendorsoftwareproduct.security.SecurityManagerException;
80 import org.yaml.snakeyaml.Yaml;
83 * Validates the contents of the package to ensure it complies with the "CSAR with TOSCA-Metadata directory" structure
84 * as defined in ETSI GS NFV-SOL 004 v2.6.1.
86 class SOL004MetaDirectoryValidator implements Validator {
88 private static final Logger LOGGER = LoggerFactory.getLogger(SOL004MetaDirectoryValidator.class);
90 private static final String MANIFEST_SOURCE = "Source";
91 private static final String MANIFEST_NON_MANO_SOURCE = "Non-MANO Source";
92 private final List<ErrorMessage> errorsByFile = new CopyOnWriteArrayList<>();
93 private final SecurityManager securityManager;
94 private OnboardingPackageContentHandler contentHandler;
95 private Set<String> folderList;
96 private ToscaMetadata toscaMetadata;
98 public SOL004MetaDirectoryValidator() {
99 securityManager = SecurityManager.getInstance();
103 SOL004MetaDirectoryValidator(final SecurityManager securityManager) {
104 this.securityManager = securityManager;
108 public Map<String, List<ErrorMessage>> validateContent(final FileContentHandler fileContentHandler) {
109 this.contentHandler = (OnboardingPackageContentHandler) fileContentHandler;
110 this.folderList = contentHandler.getFolderList();
111 parseToscaMetadata();
112 verifyMetadataFile();
114 if (packageHasCertificate()) {
117 return Collections.unmodifiableMap(getAnyValidationErrors());
120 private boolean packageHasCertificate() {
121 final String certificatePath = getCertificatePath().orElse(null);
122 return contentHandler.containsFile(certificatePath);
125 private Optional<String> getCertificatePath() {
126 return toscaMetadata.getEntry(ETSI_ENTRY_CERTIFICATE);
130 * Parses the {@link ToscaMetaEntry#TOSCA_META_PATH_FILE_NAME;} file
132 private void parseToscaMetadata() {
135 OnboardingToscaMetadata
136 .parseToscaMetadataFile(contentHandler.getFileContentAsStream(TOSCA_META_PATH_FILE_NAME.getName()));
137 } catch (final IOException e) {
138 reportError(ErrorLevel.ERROR, Messages.METADATA_PARSER_INTERNAL.getErrorMessage());
139 LOGGER.error(Messages.METADATA_PARSER_INTERNAL.getErrorMessage(), e.getMessage(), e);
143 private void verifyMetadataFile() {
144 if (toscaMetadata.isValid() && hasETSIMetadata()) {
145 verifyManifestNameAndExtension();
146 handleMetadataEntries();
148 errorsByFile.addAll(toscaMetadata.getErrors());
152 private void verifySignedFiles() {
153 final Map<String, String> signedFileMap = contentHandler.getFileAndSignaturePathMap(SecurityManager.ALLOWED_SIGNATURE_EXTENSIONS);
154 final String packageCertificatePath = getCertificatePath().orElse(null);
155 final byte[] packageCert = contentHandler.getFileContent(packageCertificatePath);
156 if(packageCert == null) {
157 throw new MissingCertificateException("Expected package certificate");
159 signedFileMap.entrySet().stream().filter(entry -> entry.getValue() != null).forEach(entry -> {
160 final String filePath = entry.getKey();
161 final String fileSignaturePath = entry.getValue();
162 final byte[] fileBytes = contentHandler.getFileContent(filePath);
163 final byte[] fileSignatureBytes = contentHandler.getFileContent(fileSignaturePath);
165 if (!securityManager.verifySignedData(fileSignatureBytes, packageCert, fileBytes)) {
166 reportError(ErrorLevel.ERROR,
167 Messages.ARTIFACT_INVALID_SIGNATURE.formatMessage(fileSignaturePath, filePath));
169 } catch (final SecurityManagerException e) {
170 final String errorMessage = Messages.ARTIFACT_SIGNATURE_VALIDATION_ERROR
171 .formatMessage(fileSignaturePath, filePath, packageCertificatePath, e.getMessage());
172 reportError(ErrorLevel.ERROR, errorMessage);
173 LOGGER.error(errorMessage, e);
178 private void verifyManifestNameAndExtension() {
179 final Map<String, String> entries = toscaMetadata.getMetaEntries();
180 final String manifestFileName = getFileName(entries.get(ETSI_ENTRY_MANIFEST.getName()));
181 final String manifestExtension = getFileExtension(entries.get(ETSI_ENTRY_MANIFEST.getName()));
182 final String mainDefinitionFileName = getFileName(entries.get(ENTRY_DEFINITIONS.getName()));
183 if (!(TOSCA_MANIFEST_FILE_EXT).equals(manifestExtension)) {
184 reportError(ErrorLevel.ERROR, Messages.MANIFEST_INVALID_EXT.getErrorMessage());
186 if (!mainDefinitionFileName.equals(manifestFileName)) {
187 reportError(ErrorLevel.ERROR, String.format(Messages.MANIFEST_INVALID_NAME.getErrorMessage(),
188 manifestFileName, mainDefinitionFileName));
192 private String getFileExtension(final String filePath) {
193 return FilenameUtils.getExtension(filePath);
196 private String getFileName(final String filePath) {
197 return FilenameUtils.getBaseName(filePath);
200 private boolean hasETSIMetadata() {
201 final Map<String, String> entries = toscaMetadata.getMetaEntries();
202 return hasEntry(entries, TOSCA_META_FILE_VERSION_ENTRY.getName())
203 && hasEntry(entries, CSAR_VERSION_ENTRY.getName())
204 && hasEntry(entries, CREATED_BY_ENTRY.getName());
207 private boolean hasEntry(final Map<String, String> entries, final String mandatoryEntry) {
208 if (!entries.containsKey(mandatoryEntry)) {
209 reportError(ErrorLevel.ERROR,
210 String.format(Messages.METADATA_MISSING_ENTRY.getErrorMessage(), mandatoryEntry));
216 private void handleMetadataEntries() {
217 toscaMetadata.getMetaEntries().entrySet().parallelStream().forEach(this::handleEntry);
220 private void handleEntry(final Map.Entry<String, String> entry) {
221 final String key = entry.getKey();
222 final ToscaMetaEntry toscaMetaEntry = ToscaMetaEntry.parse(entry.getKey()).orElse(null);
223 if (toscaMetaEntry == null) {
224 reportError(ErrorLevel.ERROR, Messages.METADATA_UNSUPPORTED_ENTRY.formatMessage(key));
225 LOGGER.warn(Messages.METADATA_UNSUPPORTED_ENTRY.getErrorMessage(), key);
228 final String value = entry.getValue();
230 switch (toscaMetaEntry) {
231 case TOSCA_META_FILE_VERSION_ENTRY:
232 case CSAR_VERSION_ENTRY:
233 case CREATED_BY_ENTRY:
234 verifyMetadataEntryVersions(key, value);
236 case ENTRY_DEFINITIONS:
237 validateDefinitionFile(value);
239 case ETSI_ENTRY_MANIFEST:
240 validateManifestFile(value);
242 case ETSI_ENTRY_CHANGE_LOG:
243 validateChangeLog(value);
245 case ETSI_ENTRY_TESTS:
246 case ETSI_ENTRY_LICENSES:
247 validateOtherEntries(entry);
249 case ETSI_ENTRY_CERTIFICATE:
250 validateCertificate(value);
253 reportError(ErrorLevel.ERROR, Messages.METADATA_UNSUPPORTED_ENTRY.formatMessage(key));
254 LOGGER.warn(Messages.METADATA_UNSUPPORTED_ENTRY.getErrorMessage(), key);
259 private void validateOtherEntries(final Map.Entry entry) {
260 final String manifestFile = toscaMetadata.getMetaEntries().get(ETSI_ENTRY_MANIFEST.getName());
261 if (verifyFileExists(contentHandler.getFileList(), manifestFile)) {
262 final Manifest onboardingManifest = new SOL004ManifestOnboarding();
263 onboardingManifest.parse(contentHandler.getFileContentAsStream(manifestFile));
264 final Optional<ResourceTypeEnum> resourceType = onboardingManifest.getType();
265 if (resourceType.isPresent() && resourceType.get() == ResourceTypeEnum.VF) {
266 final String value = (String) entry.getValue();
267 validateOtherEntries(value);
269 final String key = (String) entry.getKey();
270 reportError(ErrorLevel.ERROR,
271 String.format(Messages.MANIFEST_INVALID_PNF_METADATA.getErrorMessage(), key));
277 private void verifyMetadataEntryVersions(final String key, final String version) {
278 if (!(isValidTOSCAVersion(key, version) || isValidCSARVersion(key, version)
279 || CREATED_BY_ENTRY.getName().equals(key))) {
280 errorsByFile.add(new ErrorMessage(ErrorLevel.ERROR,
281 String.format(Messages.METADATA_INVALID_VERSION.getErrorMessage(), key, version)));
282 LOGGER.error("{}: key {} - value {} ", Messages.METADATA_INVALID_VERSION.getErrorMessage(), key, version);
286 private boolean isValidTOSCAVersion(final String key, final String version) {
287 return TOSCA_META_FILE_VERSION_ENTRY.getName().equals(key) && TOSCA_META_FILE_VERSION.getName().equals(version);
290 private boolean isValidCSARVersion(final String value, final String version) {
291 return CSAR_VERSION_ENTRY.getName().equals(value) && (CSAR_VERSION_1_1.equals(version)
292 || CSAR_VERSION_1_0.equals(version));
295 private void validateDefinitionFile(final String filePath) {
296 final Set<String> existingFiles = contentHandler.getFileList();
298 if (verifyFileExists(existingFiles, filePath)) {
299 final ToscaDefinitionImportHandler toscaDefinitionImportHandler =
300 new ToscaDefinitionImportHandler(contentHandler.getFiles(), filePath);
301 final List<ErrorMessage> validationErrorList = toscaDefinitionImportHandler.getErrors();
302 if (CollectionUtils.isNotEmpty(validationErrorList)) {
303 errorsByFile.addAll(validationErrorList);
306 reportError(ErrorLevel.ERROR, String.format(Messages.MISSING_DEFINITION_FILE.getErrorMessage(), filePath));
310 private boolean verifyFileExists(final Set<String> existingFiles, final String filePath) {
311 return existingFiles.contains(filePath);
314 private void validateManifestFile(final String filePath) {
315 final Set<String> existingFiles = contentHandler.getFileList();
316 if (verifyFileExists(existingFiles, filePath)) {
317 final Manifest onboardingManifest = new SOL004ManifestOnboarding();
318 onboardingManifest.parse(contentHandler.getFileContentAsStream(filePath));
319 if (onboardingManifest.isValid()) {
321 verifyManifestMetadata(onboardingManifest.getMetadata());
322 } catch (final InvalidManifestMetadataException e) {
323 reportError(ErrorLevel.ERROR, e.getMessage());
324 LOGGER.error(e.getMessage(), e);
326 verifyManifestSources(onboardingManifest);
328 final List<String> manifestErrors = onboardingManifest.getErrors();
329 manifestErrors.forEach(error -> reportError(ErrorLevel.ERROR, error));
332 reportError(ErrorLevel.ERROR, String.format(Messages.MANIFEST_NOT_FOUND.getErrorMessage(), filePath));
336 private void verifyManifestMetadata(final Map<String, String> metadata) {
337 if (metadata.size() != MANIFEST_METADATA_LIMIT) {
338 reportError(ErrorLevel.ERROR,
339 String.format(Messages.MANIFEST_METADATA_DOES_NOT_MATCH_LIMIT.getErrorMessage(),
340 MANIFEST_METADATA_LIMIT));
342 if (isPnfMetadata(metadata)) {
343 handleMetadataEntries(metadata, MANIFEST_PNF_METADATA);
345 handleMetadataEntries(metadata, MANIFEST_VNF_METADATA);
349 private boolean isPnfMetadata(final Map<String, String> metadata) {
350 final String firstMetadataDefinition = metadata.keySet().iterator().next();
351 final String expectedMetadataType =
352 firstMetadataDefinition.contains(TOSCA_TYPE_PNF) ? TOSCA_TYPE_PNF : TOSCA_TYPE_VNF;
353 if (metadata.keySet().stream()
354 .anyMatch((final String metadataEntry) -> !metadataEntry.contains(expectedMetadataType))) {
355 throw new InvalidManifestMetadataException(Messages.MANIFEST_METADATA_INVALID_ENTRY.getErrorMessage());
358 return TOSCA_TYPE_PNF.equals(expectedMetadataType);
361 private void handleMetadataEntries(final Map<String, String> metadata, final Set<String> manifestMetadata) {
362 manifestMetadata.stream()
363 .filter(requiredEntry -> !metadata.containsKey(requiredEntry))
364 .forEach(requiredEntry ->
365 reportError(ErrorLevel.ERROR,
366 String.format(Messages.MANIFEST_METADATA_MISSING_ENTRY.getErrorMessage(), requiredEntry)));
370 * Checks if all manifest sources exists within the package and if all package files are being referred.
372 * @param onboardingManifest The manifest
374 private void verifyManifestSources(final Manifest onboardingManifest) {
375 final Set<String> packageFiles = contentHandler.getFileList();
376 final List<String> sources = filterSources(onboardingManifest.getSources());
377 verifyFilesExist(packageFiles, sources, MANIFEST_SOURCE);
379 final Map<String, List<String>> nonManoArtifacts = onboardingManifest.getNonManoSources();
381 final List<String> nonManoValidFilePaths = new ArrayList<>();
382 nonManoArtifacts.forEach((nonManoType, files) -> {
383 final List<String> internalNonManoFileList = filterSources(files);
384 nonManoValidFilePaths.addAll(internalNonManoFileList);
385 final NonManoArtifactType nonManoArtifactType = NonManoArtifactType.parse(nonManoType).orElse(null);
386 if (nonManoArtifactType == ONAP_PM_DICTIONARY || nonManoArtifactType == ONAP_VES_EVENTS) {
387 internalNonManoFileList.forEach(this::validateYaml);
388 } else if (nonManoArtifactType == ONAP_SW_INFORMATION) {
389 validateSoftwareInformationNonManoArtifact(files);
393 verifyFilesExist(packageFiles, nonManoValidFilePaths, MANIFEST_NON_MANO_SOURCE);
395 final Set<String> allReferredFiles = new HashSet<>();
396 allReferredFiles.addAll(sources);
397 allReferredFiles.addAll(nonManoValidFilePaths);
398 verifyFilesBeingReferred(allReferredFiles, packageFiles);
401 private void validateSoftwareInformationNonManoArtifact(final List<String> files) {
402 if (CollectionUtils.isEmpty(files)) {
403 reportError(ErrorLevel.ERROR, Messages.EMPTY_SW_INFORMATION_NON_MANO_ERROR.getErrorMessage());
406 if (files.size() != 1) {
407 final String formattedFileList = files.stream()
408 .map(filePath -> String.format("'%s'", filePath))
409 .collect(Collectors.joining(", "));
410 reportError(ErrorLevel.ERROR,
411 Messages.UNIQUE_SW_INFORMATION_NON_MANO_ERROR.formatMessage(formattedFileList));
414 final String swInformationFilePath = files.get(0);
415 final byte[] swInformationYaml = contentHandler.getFileContent(swInformationFilePath);
416 final Optional<PnfSoftwareInformation> parsedYaml = SoftwareInformationArtifactYamlParser
417 .parse(swInformationYaml);
418 if(!parsedYaml.isPresent()) {
419 reportError(ErrorLevel.ERROR,
420 Messages.INVALID_SW_INFORMATION_NON_MANO_ERROR.formatMessage(swInformationFilePath));
422 final PnfSoftwareInformation pnfSoftwareInformation = parsedYaml.get();
423 if (!pnfSoftwareInformation.isValid()) {
424 reportError(ErrorLevel.ERROR,
425 Messages.INCORRECT_SW_INFORMATION_NON_MANO_ERROR.formatMessage(swInformationFilePath));
431 * Validates if a YAML file has the correct extension, is not empty and the content is a valid YAML. Reports each
434 * @param filePath the file path inside the package
436 private void validateYaml(final String filePath) {
437 if (!contentHandler.containsFile(filePath)) {
440 final String fileExtension = getFileExtension(filePath);
441 if (!"yaml".equalsIgnoreCase(fileExtension) && !"yml".equalsIgnoreCase(fileExtension)) {
442 reportError(ErrorLevel.ERROR, Messages.INVALID_YAML_EXTENSION.formatMessage(filePath));
446 try (final InputStream fileContent = contentHandler.getFileContentAsStream(filePath)) {
447 if (fileContent == null) {
448 reportError(ErrorLevel.ERROR, Messages.EMPTY_YAML_FILE_1.formatMessage(filePath));
451 new Yaml().loadAll(fileContent).iterator().next();
452 } catch (final IOException e) {
453 final String errorMsg = Messages.FILE_LOAD_CONTENT_ERROR.formatMessage(filePath);
454 reportError(ErrorLevel.ERROR, errorMsg);
455 LOGGER.debug(errorMsg, e);
456 } catch (final Exception e) {
457 final String message = Messages.INVALID_YAML_FORMAT_1.formatMessage(filePath, e.getMessage());
458 LOGGER.debug(message, e);
459 reportError(ErrorLevel.ERROR, message);
464 * Checks if all package files are referred in manifest. Reports missing references.
466 * @param referredFileSet the list of referred files path
467 * @param packageFileSet the list of package file path
469 private void verifyFilesBeingReferred(final Set<String> referredFileSet, final Set<String> packageFileSet) {
470 packageFileSet.forEach(filePath -> {
471 if (!referredFileSet.contains(filePath)) {
472 reportError(ErrorLevel.ERROR,
473 String.format(Messages.MISSING_MANIFEST_REFERENCE.getErrorMessage(), filePath));
478 private List<String> filterSources(final List<String> source) {
479 return source.stream()
480 .filter(this::externalFileReferences)
481 .collect(Collectors.toList());
484 private boolean externalFileReferences(final String filePath) {
485 return !filePath.contains("://");
488 private void validateOtherEntries(final String folderPath) {
489 if (!verifyFoldersExist(folderList, folderPath)) {
490 reportError(ErrorLevel.ERROR, String.format(Messages.METADATA_MISSING_OPTIONAL_FOLDERS.getErrorMessage(),
495 private void validateCertificate(final String file) {
496 final Set<String> packageFiles = contentHandler.getFileList();
497 if (!verifyFileExist(packageFiles, file)) {
498 reportError(ErrorLevel.ERROR,
499 String.format(Messages.MISSING_METADATA_FILES.getErrorMessage(), file, file));
503 private boolean verifyFoldersExist(final Set<String> folderList, final String folderPath) {
504 return folderList.contains(folderPath + "/");
507 private void verifyFilesExist(final Set<String> existingFiles, final List<String> sources, final String type) {
508 sources.forEach(file -> {
509 if (!existingFiles.contains(file)) {
510 reportError(ErrorLevel.ERROR,
511 String.format(Messages.MISSING_MANIFEST_SOURCE.getErrorMessage(), type, file));
516 private boolean verifyFileExist(final Set<String> existingFiles, final String file) {
517 return existingFiles.contains(file);
520 private void validateChangeLog(final String filePath) {
521 if (!verifyFileExists(contentHandler.getFileList(), filePath)) {
522 reportError(ErrorLevel.ERROR, String.format(Messages.MISSING_METADATA_FILES.getErrorMessage(), filePath));
526 private void reportError(final ErrorLevel errorLevel, final String errorMessage) {
527 errorsByFile.add(new ErrorMessage(errorLevel, errorMessage));
530 private Map<String, List<ErrorMessage>> getAnyValidationErrors() {
531 if (errorsByFile.isEmpty()) {
532 return Collections.emptyMap();
534 final Map<String, List<ErrorMessage>> errors = new HashMap<>();
535 errors.put(SdcCommon.UPLOAD_FILE, errorsByFile);