Editing of Nordix Licenses to ONAP guidelines
[cps.git] / cps-ri / src / main / java / org / onap / cps / spi / impl / CpsModulePersistenceServiceImpl.java
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2020 Nordix Foundation
4  *  Modifications Copyright (C) 2020-2021 Bell Canada.
5  *  Modifications Copyright (C) 2021 Pantheon.tech
6  *  ================================================================================
7  *  Licensed under the Apache License, Version 2.0 (the "License");
8  *  you may not use this file except in compliance with the License.
9  *  You may obtain a copy of the License at
10  *
11  *        http://www.apache.org/licenses/LICENSE-2.0
12  *
13  *  Unless required by applicable law or agreed to in writing, software
14  *  distributed under the License is distributed on an "AS IS" BASIS,
15  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  *  See the License for the specific language governing permissions and
17  *  limitations under the License.
18  *
19  *  SPDX-License-Identifier: Apache-2.0
20  *  ============LICENSE_END=========================================================
21  */
22
23 package org.onap.cps.spi.impl;
24
25 import com.google.common.collect.ImmutableSet;
26 import java.nio.charset.StandardCharsets;
27 import java.util.Collection;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31 import java.util.Set;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34 import java.util.stream.Collectors;
35 import javax.transaction.Transactional;
36 import lombok.extern.slf4j.Slf4j;
37 import org.apache.commons.codec.digest.DigestUtils;
38 import org.apache.commons.lang3.StringUtils;
39 import org.hibernate.exception.ConstraintViolationException;
40 import org.onap.cps.spi.CascadeDeleteAllowed;
41 import org.onap.cps.spi.CpsAdminPersistenceService;
42 import org.onap.cps.spi.CpsModulePersistenceService;
43 import org.onap.cps.spi.entities.AnchorEntity;
44 import org.onap.cps.spi.entities.SchemaSetEntity;
45 import org.onap.cps.spi.entities.YangResourceEntity;
46 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
47 import org.onap.cps.spi.exceptions.DuplicatedYangResourceException;
48 import org.onap.cps.spi.exceptions.SchemaSetInUseException;
49 import org.onap.cps.spi.repository.AnchorRepository;
50 import org.onap.cps.spi.repository.DataspaceRepository;
51 import org.onap.cps.spi.repository.FragmentRepository;
52 import org.onap.cps.spi.repository.SchemaSetRepository;
53 import org.onap.cps.spi.repository.YangResourceRepository;
54 import org.springframework.beans.factory.annotation.Autowired;
55 import org.springframework.dao.DataIntegrityViolationException;
56 import org.springframework.retry.annotation.Backoff;
57 import org.springframework.retry.annotation.Retryable;
58 import org.springframework.stereotype.Component;
59
60
61 @Component
62 @Slf4j
63 public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceService {
64
65     private static final String YANG_RESOURCE_CHECKSUM_CONSTRAINT_NAME = "yang_resource_checksum_key";
66     private static final Pattern CHECKSUM_EXCEPTION_PATTERN = Pattern.compile(".*\\(checksum\\)=\\((\\w+)\\).*");
67
68     @Autowired
69     private YangResourceRepository yangResourceRepository;
70
71     @Autowired
72     private SchemaSetRepository schemaSetRepository;
73
74     @Autowired
75     private DataspaceRepository dataspaceRepository;
76
77     @Autowired
78     private AnchorRepository anchorRepository;
79
80     @Autowired
81     private FragmentRepository fragmentRepository;
82
83     @Autowired
84     private CpsAdminPersistenceService cpsAdminPersistenceService;
85
86     @Override
87     @Transactional
88     // A retry is made to store the schema set if it fails because of duplicated yang resource exception that
89     // can occur in case of specific concurrent requests.
90     @Retryable(value = DuplicatedYangResourceException.class, maxAttempts = 2, backoff = @Backoff(delay = 500))
91     public void storeSchemaSet(final String dataspaceName, final String schemaSetName,
92         final Map<String, String> yangResourcesNameToContentMap) {
93
94         final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
95         final Set<YangResourceEntity> yangResourceEntities = synchronizeYangResources(yangResourcesNameToContentMap);
96         final var schemaSetEntity = new SchemaSetEntity();
97         schemaSetEntity.setName(schemaSetName);
98         schemaSetEntity.setDataspace(dataspaceEntity);
99         schemaSetEntity.setYangResources(yangResourceEntities);
100         try {
101             schemaSetRepository.save(schemaSetEntity);
102         } catch (final DataIntegrityViolationException e) {
103             throw AlreadyDefinedException.forSchemaSet(schemaSetName, dataspaceName, e);
104         }
105     }
106
107     private Set<YangResourceEntity> synchronizeYangResources(final Map<String, String> yangResourcesNameToContentMap) {
108         final Map<String, YangResourceEntity> checksumToEntityMap = yangResourcesNameToContentMap.entrySet().stream()
109             .map(entry -> {
110                 final String checksum = DigestUtils.sha256Hex(entry.getValue().getBytes(StandardCharsets.UTF_8));
111                 final var yangResourceEntity = new YangResourceEntity();
112                 yangResourceEntity.setName(entry.getKey());
113                 yangResourceEntity.setContent(entry.getValue());
114                 yangResourceEntity.setChecksum(checksum);
115                 return yangResourceEntity;
116             })
117             .collect(Collectors.toMap(
118                 YangResourceEntity::getChecksum,
119                 entity -> entity
120             ));
121
122         final List<YangResourceEntity> existingYangResourceEntities =
123             yangResourceRepository.findAllByChecksumIn(checksumToEntityMap.keySet());
124         existingYangResourceEntities.forEach(yangFile -> checksumToEntityMap.remove(yangFile.getChecksum()));
125
126         final Collection<YangResourceEntity> newYangResourceEntities = checksumToEntityMap.values();
127         if (!newYangResourceEntities.isEmpty()) {
128             try {
129                 yangResourceRepository.saveAll(newYangResourceEntities);
130             } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
131                 // Throw a CPS duplicated Yang resource exception if the cause of the error is a yang checksum
132                 // database constraint violation.
133                 // If it is not, then throw the original exception
134                 final Optional<DuplicatedYangResourceException> convertedException =
135                         convertToDuplicatedYangResourceException(
136                                 dataIntegrityViolationException, newYangResourceEntities);
137                 convertedException.ifPresent(
138                     e ->  log.warn(
139                                 "Cannot persist duplicated yang resource. "
140                                         + "A total of 2 attempts to store the schema set are planned.", e));
141                 throw convertedException.isPresent() ? convertedException.get() : dataIntegrityViolationException;
142             }
143         }
144
145         return ImmutableSet.<YangResourceEntity>builder()
146             .addAll(existingYangResourceEntities)
147             .addAll(newYangResourceEntities)
148             .build();
149     }
150
151     /**
152      * Convert the specified data integrity violation exception into a CPS duplicated Yang resource exception
153      * if the cause of the error is a yang checksum database constraint violation.
154      * @param originalException the original db exception.
155      * @param yangResourceEntities the collection of Yang resources involved in the db failure.
156      * @return an optional converted CPS duplicated Yang resource exception. The optional is empty if the original
157      *      cause of the error is not a yang checksum database constraint violation.
158      */
159     private Optional<DuplicatedYangResourceException> convertToDuplicatedYangResourceException(
160             final DataIntegrityViolationException originalException,
161             final Collection<YangResourceEntity> yangResourceEntities) {
162
163         // The exception result
164         DuplicatedYangResourceException duplicatedYangResourceException = null;
165
166         final Throwable cause = originalException.getCause();
167         if (cause instanceof ConstraintViolationException) {
168             final ConstraintViolationException constraintException = (ConstraintViolationException) cause;
169             if (YANG_RESOURCE_CHECKSUM_CONSTRAINT_NAME.equals(constraintException.getConstraintName())) {
170                 // Db constraint related to yang resource checksum uniqueness is not respected
171                 final String checksumInError = getDuplicatedChecksumFromException(constraintException);
172                 final String nameInError = getNameForChecksum(checksumInError, yangResourceEntities);
173                 duplicatedYangResourceException =
174                         new DuplicatedYangResourceException(nameInError, checksumInError, constraintException);
175             }
176         }
177
178         return Optional.ofNullable(duplicatedYangResourceException);
179
180     }
181
182     /**
183      * Get the checksum that caused the constraint violation exception.
184      * @param exception the exception having the checksum in error.
185      * @return the checksum in error or null if not found.
186      */
187     private String getDuplicatedChecksumFromException(final ConstraintViolationException exception) {
188         String checksum = null;
189         final Matcher matcher = CHECKSUM_EXCEPTION_PATTERN.matcher(exception.getSQLException().getMessage());
190         if (matcher.find() && matcher.groupCount() == 1) {
191             checksum = matcher.group(1);
192         }
193         return checksum;
194     }
195
196     /**
197      * Get the name of the yang resource having the specified checksum.
198      * @param checksum the checksum. Null is supported.
199      * @param yangResourceEntities the list of yang resources to search among.
200      * @return the name found or null if none.
201      */
202     private String getNameForChecksum(
203             final String checksum, final Collection<YangResourceEntity> yangResourceEntities) {
204         return
205                 yangResourceEntities.stream()
206                         .filter(entity -> StringUtils.equals(checksum, (entity.getChecksum())))
207                         .findFirst()
208                         .map(YangResourceEntity::getName)
209                         .orElse(null);
210     }
211
212     @Override
213     public Map<String, String> getYangSchemaResources(final String dataspaceName, final String schemaSetName) {
214         final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
215         final var schemaSetEntity =
216             schemaSetRepository.getByDataspaceAndName(dataspaceEntity, schemaSetName);
217         return schemaSetEntity.getYangResources().stream().collect(
218             Collectors.toMap(YangResourceEntity::getName, YangResourceEntity::getContent));
219     }
220
221     @Override
222     public Map<String, String> getYangSchemaSetResources(final String dataspaceName, final String anchorName) {
223         final var anchor = cpsAdminPersistenceService.getAnchor(dataspaceName, anchorName);
224         return getYangSchemaResources(dataspaceName, anchor.getSchemaSetName());
225     }
226
227     @Override
228     @Transactional
229     public void deleteSchemaSet(final String dataspaceName, final String schemaSetName,
230         final CascadeDeleteAllowed cascadeDeleteAllowed) {
231         final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
232         final var schemaSetEntity =
233             schemaSetRepository.getByDataspaceAndName(dataspaceEntity, schemaSetName);
234
235         final Collection<AnchorEntity> anchorEntities = anchorRepository.findAllBySchemaSet(schemaSetEntity);
236         if (!anchorEntities.isEmpty()) {
237             if (cascadeDeleteAllowed != CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED) {
238                 throw new SchemaSetInUseException(dataspaceName, schemaSetName);
239             }
240             fragmentRepository.deleteByAnchorIn(anchorEntities);
241             anchorRepository.deleteAll(anchorEntities);
242         }
243         schemaSetRepository.delete(schemaSetEntity);
244         yangResourceRepository.deleteOrphans();
245     }
246 }