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