Return latest entity on null versions
[policy/models.git] / models-base / src / main / java / org / onap / policy / models / base / PfConceptContainer.java
1 /*-
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2019-2020 Nordix Foundation.
4  *  Modifications Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
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  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  *
18  * SPDX-License-Identifier: Apache-2.0
19  * ============LICENSE_END=========================================================
20  */
21
22 package org.onap.policy.models.base;
23
24 import java.lang.reflect.ParameterizedType;
25 import java.util.ArrayList;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.NavigableMap;
31 import java.util.Set;
32 import java.util.TreeMap;
33 import java.util.function.Function;
34
35 import javax.persistence.CascadeType;
36 import javax.persistence.EmbeddedId;
37 import javax.persistence.Entity;
38 import javax.persistence.JoinColumn;
39 import javax.persistence.JoinTable;
40 import javax.persistence.ManyToMany;
41 import javax.persistence.MappedSuperclass;
42 import javax.persistence.Table;
43 import javax.ws.rs.core.Response;
44
45 import lombok.Data;
46 import lombok.EqualsAndHashCode;
47 import lombok.NonNull;
48
49 import org.apache.commons.lang3.StringUtils;
50 import org.onap.policy.models.base.PfValidationResult.ValidationResult;
51
52 // @formatter:off
53 /**
54  * This class is a concept container and holds a map of concepts. The {@link PfConceptContainer} class implements the
55  * helper methods of the {@link PfConceptGetter} interface to allow {@link PfConceptContainer} instances to be retrieved
56  * by calling methods directly on this class without referencing the contained map.
57  *
58  * <p>Validation checks that a container key is not null. An error is issued if no concepts are defined in a container.
59  * Each concept entry is checked to ensure that its key and value are not null and that the key matches the key in the
60  * map value. Each concept entry is then validated individually.
61  *
62  * @param C the concept being contained
63  */
64 //@formatter:on
65 @MappedSuperclass
66 @Entity
67 @Table(name = "PfConceptContainer")
68 @Data
69 @EqualsAndHashCode(callSuper = false)
70
71 public class PfConceptContainer<C extends PfConcept, A extends PfNameVersion> extends PfConcept
72     implements PfConceptGetter<C>, PfAuthorative<List<Map<String, A>>> {
73     private static final long serialVersionUID = -324211738823208318L;
74
75     @EmbeddedId
76     private PfConceptKey key;
77
78     @ManyToMany(cascade = CascadeType.ALL)
79     // @formatter:off
80     @JoinTable(
81             joinColumns = {
82                 @JoinColumn(name = "conceptContainerMapName",    referencedColumnName = "name"),
83                 @JoinColumn(name = "concpetContainerMapVersion", referencedColumnName = "version")
84             },
85             inverseJoinColumns = {
86                 @JoinColumn(name = "conceptContainerName",    referencedColumnName = "name"),
87                 @JoinColumn(name = "conceptContainerVersion", referencedColumnName = "version")
88             }
89         )
90     // @formatter:on
91     private Map<PfConceptKey, C> conceptMap;
92
93     /**
94      * The Default Constructor creates a {@link PfConceptContainer} object with a null artifact key and creates an empty
95      * concept map.
96      */
97     public PfConceptContainer() {
98         this(new PfConceptKey());
99     }
100
101     /**
102      * The Key Constructor creates a {@link PfConceptContainer} object with the given artifact key and creates an empty
103      * concept map.
104      *
105      * @param key the concept key
106      */
107     public PfConceptContainer(@NonNull final PfConceptKey key) {
108         this(key, new TreeMap<PfConceptKey, C>());
109     }
110
111     /**
112      * This Constructor creates an concept container with all of its fields defined.
113      *
114      * @param key the concept container key
115      * @param conceptMap the concepts to be stored in the concept container
116      */
117     public PfConceptContainer(@NonNull final PfConceptKey key, @NonNull final Map<PfConceptKey, C> conceptMap) {
118         super();
119
120         this.key = key;
121         this.conceptMap = new TreeMap<>(conceptMap);
122     }
123
124     /**
125      * Copy constructor.
126      *
127      * @param copyConcept the concept to copy from
128      */
129     public PfConceptContainer(@NonNull final PfConceptContainer<C, A> copyConcept) {
130         super(copyConcept);
131         this.key = new PfConceptKey(copyConcept.key);
132
133         this.conceptMap = new TreeMap<>();
134         for (final Entry<PfConceptKey, C> conceptMapEntry : copyConcept.conceptMap.entrySet()) {
135             PfConceptKey newK = new PfConceptKey(conceptMapEntry.getKey());
136             C newC = PfUtils.makeCopy(conceptMapEntry.getValue());
137             this.conceptMap.put(newK, newC);
138         }
139     }
140
141     @Override
142     public List<PfKey> getKeys() {
143         final List<PfKey> keyList = key.getKeys();
144
145         for (final C concept : conceptMap.values()) {
146             keyList.addAll(concept.getKeys());
147         }
148
149         return keyList;
150     }
151
152     @Override
153     public List<Map<String, A>> toAuthorative() {
154         // The returned list is a list of map singletons with one map for each map
155         // entry in the concept container
156         List<Map<String, A>> toscaPolicyMapList = new ArrayList<>();
157
158         for (Entry<PfConceptKey, C> conceptEntry : getConceptMap().entrySet()) {
159             // Create a map to hold this entry
160             Map<String, A> toscaPolicyMap = new LinkedHashMap<>(1);
161
162             // Add the concept container entry to the singleton map
163             @SuppressWarnings("unchecked")
164             PfAuthorative<A> authoritiveImpl = (PfAuthorative<A>) conceptEntry.getValue();
165             toscaPolicyMap.put(conceptEntry.getKey().getName(), authoritiveImpl.toAuthorative());
166
167             // Add the map to the returned list
168             toscaPolicyMapList.add(toscaPolicyMap);
169         }
170
171         return toscaPolicyMapList;
172     }
173
174     @Override
175     public void fromAuthorative(List<Map<String, A>> authorativeList) {
176         // Clear any existing map entries
177         conceptMap.clear();
178
179         // Concepts are in lists of maps
180         for (Map<String, A> incomingConceptMap : authorativeList) {
181             // Add the map entries one by one
182             for (Entry<String, A> incomingConceptEntry : incomingConceptMap.entrySet()) {
183
184                 PfConceptKey conceptKey = new PfConceptKey();
185                 if (incomingConceptEntry.getKey().matches(PfKey.KEY_ID_REGEXP)) {
186                     conceptKey = new PfConceptKey(incomingConceptEntry.getKey());
187                 } else {
188                     conceptKey.setName(incomingConceptEntry.getKey());
189                     if (incomingConceptEntry.getValue().getVersion() != null) {
190                         conceptKey.setVersion(incomingConceptEntry.getValue().getVersion());
191                     } else {
192                         conceptKey.setVersion(PfKey.NULL_KEY_VERSION);
193                     }
194                 }
195
196                 incomingConceptEntry.getValue().setName(findConceptField(conceptKey, conceptKey.getName(),
197                     incomingConceptEntry.getValue(), PfNameVersion::getName));
198                 incomingConceptEntry.getValue().setVersion(findConceptField(conceptKey, conceptKey.getVersion(),
199                     incomingConceptEntry.getValue(), PfNameVersion::getVersion));
200
201                 C jpaConcept = getConceptNewInstance();
202                 // This cast allows us to call the fromAuthorative method
203                 @SuppressWarnings("unchecked")
204                 PfAuthorative<A> authoritiveImpl = (PfAuthorative<A>) jpaConcept;
205
206                 // Set the key name and the rest of the values on the concept
207                 authoritiveImpl.fromAuthorative(incomingConceptEntry.getValue());
208
209                 // After all that, save the map entry
210                 conceptMap.put(conceptKey, jpaConcept);
211             }
212         }
213
214         if (conceptMap.isEmpty()) {
215             throw new PfModelRuntimeException(Response.Status.BAD_REQUEST,
216                 "An incoming list of concepts must have at least one entry");
217         }
218     }
219
220     @Override
221     public void clean() {
222         key.clean();
223         for (final Entry<PfConceptKey, C> conceptEntry : conceptMap.entrySet()) {
224             conceptEntry.getKey().clean();
225             conceptEntry.getValue().clean();
226         }
227     }
228
229     @Override
230     public PfValidationResult validate(@NonNull final PfValidationResult resultIn) {
231         PfValidationResult result = resultIn;
232
233         if (key.equals(PfConceptKey.getNullKey())) {
234             result.addValidationMessage(
235                 new PfValidationMessage(key, this.getClass(), ValidationResult.INVALID, "key is a null key"));
236         }
237
238         result = key.validate(result);
239
240         if (!conceptMap.isEmpty()) {
241             result = validateConceptMap(result);
242         }
243
244         return result;
245     }
246
247     /**
248      * Validate the concept map of the container.
249      *
250      * @param resultIn the incoming validation results so far
251      * @return the validation results with the results of this validation added
252      */
253     private PfValidationResult validateConceptMap(final PfValidationResult resultIn) {
254         PfValidationResult result = resultIn;
255
256         for (final Entry<PfConceptKey, C> conceptEntry : conceptMap.entrySet()) {
257             if (conceptEntry.getKey().equals(PfConceptKey.getNullKey())) {
258                 result.addValidationMessage(new PfValidationMessage(key, this.getClass(), ValidationResult.INVALID,
259                     "key on concept entry " + conceptEntry.getKey() + " may not be the null key"));
260             } else if (conceptEntry.getValue() == null) {
261                 result.addValidationMessage(new PfValidationMessage(key, this.getClass(), ValidationResult.INVALID,
262                     "value on concept entry " + conceptEntry.getKey() + " may not be null"));
263             } else if (!conceptEntry.getKey().equals(conceptEntry.getValue().getKey())) {
264                 result.addValidationMessage(new PfValidationMessage(key, this.getClass(), ValidationResult.INVALID,
265                     "key on concept entry key " + conceptEntry.getKey() + " does not equal concept value key "
266                         + conceptEntry.getValue().getKey()));
267                 result = conceptEntry.getValue().validate(result);
268             } else {
269                 result = conceptEntry.getValue().validate(result);
270             }
271         }
272         return result;
273     }
274
275     @Override
276     public int compareTo(final PfConcept otherConcept) {
277         if (otherConcept == null) {
278             return -1;
279         }
280         if (this == otherConcept) {
281             return 0;
282         }
283         if (getClass() != otherConcept.getClass()) {
284             return getClass().getName().compareTo(otherConcept.getClass().getName());
285         }
286
287         @SuppressWarnings("unchecked")
288         final PfConceptContainer<C, A> other = (PfConceptContainer<C, A>) otherConcept;
289         int retVal = key.compareTo(other.key);
290         if (retVal != 0) {
291             return retVal;
292         }
293
294         if (!conceptMap.equals(other.conceptMap)) {
295             return (conceptMap.hashCode() - other.conceptMap.hashCode());
296         }
297
298         return 0;
299     }
300
301     @Override
302     public C get(final PfConceptKey conceptKey) {
303         if (conceptKey.isNullVersion()) {
304             return get(conceptKey.getName());
305         } else {
306             return new PfConceptGetterImpl<>((NavigableMap<PfConceptKey, C>) conceptMap).get(conceptKey);
307         }
308     }
309
310     @Override
311     public C get(final String conceptKeyName) {
312         return new PfConceptGetterImpl<>((NavigableMap<PfConceptKey, C>) conceptMap).get(conceptKeyName);
313     }
314
315     @Override
316     public C get(final String conceptKeyName, final String conceptKeyVersion) {
317         return new PfConceptGetterImpl<>((NavigableMap<PfConceptKey, C>) conceptMap).get(conceptKeyName,
318             conceptKeyVersion);
319     }
320
321     @Override
322     public Set<C> getAll(final String conceptKeyName) {
323         return new PfConceptGetterImpl<>((NavigableMap<PfConceptKey, C>) conceptMap).getAll(conceptKeyName);
324     }
325
326     @Override
327     public Set<C> getAll(final String conceptKeyName, final String conceptKeyVersion) {
328         return new PfConceptGetterImpl<>((NavigableMap<PfConceptKey, C>) conceptMap).getAll(conceptKeyName,
329             conceptKeyVersion);
330     }
331
332     /**
333      * Get a new empty instance of a concept for this concept map.
334      *
335      * @return the new instance
336      */
337     @SuppressWarnings("unchecked")
338     private C getConceptNewInstance() {
339         try {
340             String conceptClassName =
341                 ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0].getTypeName();
342             return (C) Class.forName(conceptClassName).getDeclaredConstructor().newInstance();
343         } catch (Exception ex) {
344             throw new PfModelRuntimeException(Response.Status.INTERNAL_SERVER_ERROR,
345                 "failed to instantiate instance of container concept class", ex);
346         }
347     }
348
349     private String findConceptField(final PfConceptKey conceptKey, final String keyFieldValue,
350         final PfNameVersion concept, final Function<PfNameVersion, String> fieldGetterFunction) {
351
352         String conceptField = fieldGetterFunction.apply(concept);
353
354         if (StringUtils.isBlank(conceptField) || keyFieldValue.equals(conceptField)) {
355             return keyFieldValue;
356         } else {
357             throw new PfModelRuntimeException(Response.Status.BAD_REQUEST, "Key " + conceptKey.getId() + " field "
358                 + keyFieldValue + " does not match the value " + conceptField + " in the concept field");
359         }
360     }
361 }