661cadc7385f1af5b7e51327a695bed1de8ef927
[sdc.git] /
1 /*
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
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  *
16  *  SPDX-License-Identifier: Apache-2.0
17  *  ============LICENSE_END=========================================================
18  */
19
20 package org.openecomp.core.impl;
21
22 import static org.openecomp.sdc.tosca.csar.CSARConstants.NON_FILE_IMPORT_ATTRIBUTES;
23
24 import java.net.URI;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Optional;
31 import lombok.Getter;
32 import org.apache.commons.collections.CollectionUtils;
33 import org.apache.commons.io.FilenameUtils;
34 import org.apache.commons.lang3.StringUtils;
35 import org.openecomp.core.converter.ServiceTemplateReaderService;
36 import org.openecomp.core.impl.services.ServiceTemplateReaderServiceImpl;
37 import org.openecomp.sdc.common.errors.Messages;
38 import org.openecomp.sdc.datatypes.error.ErrorLevel;
39 import org.openecomp.sdc.datatypes.error.ErrorMessage;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * Handles TOSCA definition imports, checking for import definition errors.
45  */
46 public class ToscaDefinitionImportHandler {
47
48     private static final Logger LOGGER = LoggerFactory.getLogger(ToscaDefinitionImportHandler.class);
49
50     private final Map<String, byte[]> fileMap;
51
52     /**
53      * Stores all processed files during the import handling
54      */
55     @Getter
56     private final Map<String, ServiceTemplateReaderService> handledImportDefinitionFileMap = new HashMap<>();
57     private final List<ErrorMessage> validationErrorList = new ArrayList<>();
58     private String currentFile;
59
60     /**
61      * Reads the provided package structure starting from a main definition yaml file.
62      * @param fileStructureMap      The package structure with file path and respective file byte
63      * @param mainDefinitionFilePath    The main descriptor yaml file to start the reading
64      */
65     public ToscaDefinitionImportHandler(final Map<String, byte[]> fileStructureMap,
66                                         final String mainDefinitionFilePath) {
67         this.fileMap = fileStructureMap;
68         readImportsFromMainDefinition(mainDefinitionFilePath);
69     }
70
71     private void readImportsFromMainDefinition(final String mainDefinitionFilePath) {
72         if(!checkMainDefinitionExists(mainDefinitionFilePath)) {
73             return;
74         }
75         final ServiceTemplateReaderService readerService = parseToServiceTemplate(mainDefinitionFilePath).orElse(null);
76         if (readerService == null) {
77             return;
78         }
79         final List<String> importFileList = extractFileImports(readerService.getImports());
80         if (CollectionUtils.isNotEmpty(importFileList)) {
81             for (final String importFilePath : importFileList) {
82                 final String resolvedPath = resolveImportPath(FilenameUtils.getPath(mainDefinitionFilePath), importFilePath);
83                 handleImports(resolvedPath);
84             }
85         }
86     }
87
88     private Optional<ServiceTemplateReaderService> parseToServiceTemplate(final String definitionFile) {
89         try {
90             return Optional.of(new ServiceTemplateReaderServiceImpl(fileMap.get(definitionFile)));
91         } catch (final Exception ex) {
92             LOGGER.debug("Could not parse '{}' to a ServiceTemplateReader", definitionFile, ex);
93             reportError(ErrorLevel.ERROR,
94                 String.format(Messages.INVALID_YAML_FORMAT.getErrorMessage(), ex.getMessage()));
95         }
96
97         return Optional.empty();
98     }
99
100     /**
101      * Reads and validates the descriptor imports recursively.
102      * Starts from the provided descriptor and goes until the end of the import tree.
103      * Processes each file just once.
104      *
105      * @param fileName      the descriptor file path
106      */
107     private void handleImports(final String fileName) {
108         currentFile = fileName;
109         if (!checkImportExists(fileName)) {
110             return;
111         }
112         final ServiceTemplateReaderService readerService = parseToServiceTemplate(fileName).orElse(null);
113         if (readerService == null) {
114             return;
115         }
116
117         handledImportDefinitionFileMap.put(fileName, readerService);
118         final List<Object> imports = readerService.getImports();
119         final List<String> extractImportFiles = extractFileImports(imports);
120         extractImportFiles.stream()
121             .map(importedFile -> resolveImportPath(FilenameUtils.getPath(fileName), importedFile))
122             .filter(resolvedPath -> !handledImportDefinitionFileMap.containsKey(resolvedPath))
123             .forEach(this::handleImports);
124     }
125
126     /**
127      * Iterates reads each import statement in the given list.
128      * <pre>
129      * example of a descriptor.yaml import statement
130      * imports:
131      * - /Artifacts/anImportedDescriptor.yaml
132      * - anotherDescriptor: anotherImportedDescriptor.yaml
133      * - yetAnotherDescriptor:
134      *     yetAnotherDescriptor: ../Definitions/yetAnotherDescriptor.yaml
135      * </pre>
136      * @param imports   the import statements
137      * @return
138      *  The list of import file paths found
139      */
140     private List<String> extractFileImports(final List<Object> imports) {
141         final List<String> importedFileList = new ArrayList<>();
142         imports.forEach(importObject -> importedFileList.addAll(readImportStatement(importObject)));
143
144         return importedFileList;
145     }
146
147     /**
148      * Reads an import statement which can be a value, a [key:value] or a [key:[key:value]].
149      * Ignores entries which contains the same keys as
150      * {@link org.openecomp.sdc.tosca.csar.CSARConstants#NON_FILE_IMPORT_ATTRIBUTES}.
151      * Reports invalid import statements.
152      * <pre>
153      * example of yaml imports statements:
154      * - /Artifacts/anImportedDescriptor.yaml
155      * - anotherDescriptor: anotherImportedDescriptor.yaml
156      * - yetAnotherDescriptor:
157      *     yetAnotherDescriptor: ../Definitions/yetAnotherDescriptor.yaml
158      * </pre>
159      * @param importObject      the object representing the yaml import statement
160      * @return
161      *  The list of import file paths found
162      */
163     private List<String> readImportStatement(final Object importObject) {
164         final List<String> importedFileList = new ArrayList<>();
165         if (importObject instanceof String) {
166             importedFileList.add((String) importObject);
167         } else if (importObject instanceof Map) {
168             final Map<String, Object> importObjectMap = (Map) importObject;
169             for (final Entry<String, Object> entry : importObjectMap.entrySet()) {
170                 if (NON_FILE_IMPORT_ATTRIBUTES.stream().noneMatch(attr -> entry.getKey().equals(attr))) {
171                     importedFileList.addAll(readImportStatement(entry.getValue()));
172                 }
173             }
174         } else {
175             reportError(ErrorLevel.ERROR,
176                 String.format(Messages.INVALID_IMPORT_STATEMENT.getErrorMessage(), currentFile, importObject));
177         }
178
179         return importedFileList;
180     }
181
182     /**
183      * Given a directory path, resolves the import path.
184      * @param directoryPath     A directory path to resolve the import path
185      * @param importPath        An import statement path
186      * @return
187      *  The resolved path of the import, using as base the directory path
188      */
189     private String resolveImportPath(final String directoryPath, final String importPath) {
190         final String fixedParentDir;
191         if (StringUtils.isEmpty(directoryPath)) {
192             fixedParentDir = "/";
193         } else {
194             fixedParentDir = String.format("%s%s%s",
195                 directoryPath.startsWith("/") ? "" : "/"
196                 , directoryPath
197                 , directoryPath.endsWith("/") ? "" : "/");
198         }
199
200         final URI parentDirUri = URI.create(fixedParentDir);
201
202         String resolvedImportPath = parentDirUri.resolve(importPath).toString();
203         if (resolvedImportPath.contains("../")) {
204             reportError(ErrorLevel.ERROR,
205                 Messages.INVALID_IMPORT_STATEMENT.formatMessage(currentFile, importPath));
206             return null;
207         }
208         if (resolvedImportPath.startsWith("/")) {
209             resolvedImportPath = resolvedImportPath.substring(1);
210         }
211
212         return resolvedImportPath;
213     }
214
215     private boolean checkImportExists(final String filePath) {
216         return checkFileExists(filePath, Messages.MISSING_IMPORT_FILE.formatMessage(filePath));
217     }
218
219     private boolean checkMainDefinitionExists(final String filePath) {
220         return checkFileExists(filePath, Messages.MISSING_MAIN_DEFINITION_FILE.formatMessage(filePath));
221     }
222
223     /**
224      * Checks if the given file path exists inside the file structure.
225      * Reports an error if the file was not found.
226      *
227      * @param filePath  file path to check inside the file structure
228      * @param errorMsg  the error message to report
229      * @return
230      *  {@code true} if the file exists, {@code false} otherwise
231      */
232     private boolean checkFileExists(final String filePath, final String errorMsg) {
233         if (!fileMap.containsKey(filePath)) {
234             reportError(ErrorLevel.ERROR, errorMsg);
235             return false;
236         }
237
238         return true;
239     }
240
241     /**
242      * Adds an error to the validation error list.
243      *
244      * @param errorLevel        the error level
245      * @param errorMessage      the error message
246      */
247     private void reportError(final ErrorLevel errorLevel, final String errorMessage) {
248         validationErrorList.add(new ErrorMessage(errorLevel, errorMessage));
249     }
250
251     /**
252      * Gets the list of errors.
253      * @return
254      *  The import validation errors detected
255      */
256     public List<ErrorMessage> getErrors() {
257         return validationErrorList;
258     }
259
260     /**
261      * Checks if the handler detected a import error.
262      * @return
263      *  {@code true} if the handler detected any error, {@code false} otherwise.
264      */
265     public boolean hasError() {
266         return !validationErrorList.isEmpty();
267     }
268 }