9359e4ef93d5a7bd2b20009c39d7ff37067075fe
[policy/apex-pdp.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2016-2018 Ericsson. All rights reserved.
4  *  Modifications Copyright (C) 2019-2021 Nordix Foundation.
5  *  Modifications Copyright (C) 2021 Bell Canada. All rights reserved.
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.policy.apex.plugins.event.protocol.yaml;
24
25 import java.util.ArrayList;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import org.onap.policy.apex.context.impl.schema.SchemaHelperFactory;
30 import org.onap.policy.apex.model.basicmodel.service.ModelService;
31 import org.onap.policy.apex.model.eventmodel.concepts.AxEvent;
32 import org.onap.policy.apex.model.eventmodel.concepts.AxEvents;
33 import org.onap.policy.apex.model.eventmodel.concepts.AxField;
34 import org.onap.policy.apex.service.engine.event.ApexEvent;
35 import org.onap.policy.apex.service.engine.event.ApexEventException;
36 import org.onap.policy.apex.service.engine.event.ApexEventProtocolConverter;
37 import org.onap.policy.apex.service.engine.event.ApexEventRuntimeException;
38 import org.onap.policy.apex.service.parameters.eventprotocol.EventProtocolParameters;
39 import org.slf4j.ext.XLogger;
40 import org.slf4j.ext.XLoggerFactory;
41 import org.yaml.snakeyaml.DumperOptions.FlowStyle;
42 import org.yaml.snakeyaml.Yaml;
43
44 /**
45  * The Class Apex2YamlEventConverter converts {@link ApexEvent} instances to and from YAML string representations of
46  * Apex events.
47  *
48  * @author Liam Fallon (liam.fallon@ericsson.com)
49  */
50 public class Apex2YamlEventConverter implements ApexEventProtocolConverter {
51     private static final XLogger LOGGER = XLoggerFactory.getXLogger(Apex2YamlEventConverter.class);
52
53     // The parameters for the YAML event protocol
54     private YamlEventProtocolParameters yamlPars;
55
56     /**
57      * {@inheritDoc}.
58      */
59     @Override
60     public void init(final EventProtocolParameters parameters) {
61         // Check and get the YAML parameters
62         if (!(parameters instanceof YamlEventProtocolParameters)) {
63             final var errorMessage = "specified consumer properties are not applicable to the YAML event protocol";
64             throw new ApexEventRuntimeException(errorMessage);
65         }
66
67         yamlPars = (YamlEventProtocolParameters) parameters;
68     }
69
70     /**
71      * {@inheritDoc}.
72      */
73     @Override
74     public List<ApexEvent> toApexEvent(final String eventName, final Object eventObject) throws ApexEventException {
75         // Check the event eventObject
76         if (eventObject == null) {
77             throw new ApexEventException("event processing failed, event is null");
78         }
79
80         // Cast the event to a string, if our conversion is correctly configured, this cast should
81         // always work
82         if (!(eventObject instanceof String)) {
83             final String errorMessage = "error converting event \"" + eventObject + "\" to a string";
84             throw new ApexEventException(errorMessage);
85         }
86
87         final var yamlEventString = (String) eventObject;
88
89         // The list of events we will return
90         final List<ApexEvent> eventList = new ArrayList<>();
91
92         // Convert the YAML document string into an object
93         var yamlObject = new Yaml().load(yamlEventString);
94
95         // If the incoming YAML did not create a map it is a primitive type or a collection so we
96         // convert it into a map for processing
97         Map<?, ?> yamlMap;
98         if (yamlObject instanceof Map) {
99             // We already have a map so just cast the object
100             yamlMap = (Map<?, ?>) yamlObject;
101         } else {
102             // Create a single entry map, new map creation and assignment is to avoid a
103             // type checking warning
104             LinkedHashMap<String, Object> newYamlMap = new LinkedHashMap<>();
105             newYamlMap.put(yamlPars.getYamlFieldName(), yamlObject);
106             yamlMap = newYamlMap;
107         }
108
109         try {
110             eventList.add(yamlMap2ApexEvent(eventName, yamlMap));
111         } catch (final Exception e) {
112             throw new ApexEventException("Failed to unmarshal YAML event, event="
113                 + yamlEventString, e);
114         }
115
116         // Return the list of events we have unmarshalled
117         return eventList;
118     }
119
120     /**
121      * {@inheritDoc}.
122      */
123     @Override
124     public Object fromApexEvent(final ApexEvent apexEvent) throws ApexEventException {
125         // Check the Apex event
126         if (apexEvent == null) {
127             throw new ApexEventException("event processing failed, Apex event is null");
128         }
129
130         // Get the event definition for the event from the model service
131         final AxEvent eventDefinition = ModelService.getModel(AxEvents.class).get(apexEvent.getName(),
132                         apexEvent.getVersion());
133
134         // Create a map for output of the APEX event to YAML
135         LinkedHashMap<String, Object> yamlMap = new LinkedHashMap<>();
136
137         yamlMap.put(ApexEvent.NAME_HEADER_FIELD, apexEvent.getName());
138         yamlMap.put(ApexEvent.VERSION_HEADER_FIELD, apexEvent.getVersion());
139         yamlMap.put(ApexEvent.NAMESPACE_HEADER_FIELD, apexEvent.getNameSpace());
140         yamlMap.put(ApexEvent.SOURCE_HEADER_FIELD, apexEvent.getSource());
141         yamlMap.put(ApexEvent.TARGET_HEADER_FIELD, apexEvent.getTarget());
142
143         if (apexEvent.getExceptionMessage() != null) {
144             yamlMap.put(ApexEvent.EXCEPTION_MESSAGE_HEADER_FIELD, apexEvent.getExceptionMessage());
145         }
146
147         for (final AxField eventField : eventDefinition.getFields()) {
148             final String fieldName = eventField.getKey().getLocalName();
149
150             if (!apexEvent.containsKey(fieldName)) {
151                 if (!eventField.getOptional()) {
152                     final String errorMessage = "error parsing " + eventDefinition.getId() + " event to Json. "
153                                     + "Field \"" + fieldName + "\" is missing, but is mandatory. Fields: " + apexEvent;
154                     throw new ApexEventRuntimeException(errorMessage);
155                 }
156                 continue;
157             }
158
159             yamlMap.put(fieldName, apexEvent.get(fieldName));
160         }
161
162         // Use Snake YAML to convert the APEX event to YAML
163         var yaml = new Yaml();
164         return yaml.dumpAs(yamlMap, null, FlowStyle.BLOCK);
165     }
166
167     /**
168      * This method converts a YAML map into an Apex event.
169      *
170      * @param eventName the name of the event
171      * @param yamlMap the YAML map that holds the event
172      * @return the apex event that we have converted the JSON object into
173      * @throws ApexEventException thrown on unmarshaling exceptions
174      */
175     private ApexEvent yamlMap2ApexEvent(final String eventName, final Map<?, ?> yamlMap) throws ApexEventException {
176         // Process the mandatory Apex header
177         final var apexEvent = processApexEventHeader(eventName, yamlMap);
178
179         // Get the event definition for the event from the model service
180         final AxEvent eventDefinition = ModelService.getModel(AxEvents.class).get(apexEvent.getName(),
181                         apexEvent.getVersion());
182
183         // Iterate over the input fields in the event
184         for (final AxField eventField : eventDefinition.getFields()) {
185             final String fieldName = eventField.getKey().getLocalName();
186             if (!yamlMap.containsKey(fieldName)) {
187                 if (!eventField.getOptional()) {
188                     final String errorMessage = "error parsing " + eventDefinition.getId() + " event from Json. "
189                                     + "Field \"" + fieldName + "\" is missing, but is mandatory.";
190                     throw new ApexEventException(errorMessage);
191                 }
192                 continue;
193             }
194
195             final Object fieldValue = getYamlField(yamlMap, fieldName, null, !eventField.getOptional());
196
197             if (fieldValue != null) {
198                 // Get the schema helper
199                 final var fieldSchemaHelper = new SchemaHelperFactory().createSchemaHelper(eventField.getKey(),
200                                 eventField.getSchema());
201                 apexEvent.put(fieldName, fieldSchemaHelper.createNewInstance(fieldValue));
202             } else {
203                 apexEvent.put(fieldName, null);
204             }
205         }
206         return apexEvent;
207
208     }
209
210     /**
211      * This method processes the event header of an Apex event.
212      *
213      * @param eventName the name of the event
214      * @param yamlMap the YAML map that holds the event
215      * @return an apex event constructed using the header fields of the event
216      * @throws ApexEventRuntimeException the apex event runtime exception
217      * @throws ApexEventException on invalid events with missing header fields
218      */
219     private ApexEvent processApexEventHeader(final String eventName, final Map<?, ?> yamlMap)
220                     throws ApexEventException {
221         var name = getYamlStringField(yamlMap, ApexEvent.NAME_HEADER_FIELD, yamlPars.getNameAlias(),
222                         ApexEvent.NAME_REGEXP, false);
223
224         // Check that an event name has been specified
225         if (name == null && eventName == null) {
226             throw new ApexEventRuntimeException(
227                             "event received without mandatory parameter \"name\" on configuration or on event");
228         }
229
230         // Check if an event name was specified on the event parameters
231         if (eventName != null) {
232             if (name != null && !eventName.equals(name)) {
233                 LOGGER.warn("The incoming event name \"{}\" does not match the configured event name \"{}\", "
234                                 + "using configured event name", name, eventName);
235             }
236             name = eventName;
237         }
238
239         // Now, find the event definition in the model service. If version is null, the newest event
240         // definition in the model service is used
241         var version = getYamlStringField(yamlMap, ApexEvent.VERSION_HEADER_FIELD, yamlPars.getVersionAlias(),
242                         ApexEvent.VERSION_REGEXP, false);
243         final AxEvent eventDefinition = ModelService.getModel(AxEvents.class).get(name, version);
244         if (eventDefinition == null) {
245             throw new ApexEventRuntimeException("an event definition for an event named \"" + name
246                             + "\" with version \"" + version + "\" not found in Apex model");
247         }
248
249         // Use the defined event version if no version is specified on the incoming fields
250         if (version == null) {
251             version = eventDefinition.getKey().getVersion();
252         }
253
254         String namespace = getEventHeaderNamespace(yamlMap, name, eventDefinition);
255
256         String source = getEventHeaderSource(yamlMap, eventDefinition);
257
258         String target = getHeaderTarget(yamlMap, eventDefinition);
259
260         return new ApexEvent(name, version, namespace, source, target);
261     }
262
263     /**
264      * Get the event header name space.
265      *
266      * @param yamlMap the YAML map to read from
267      * @param eventDefinition the event definition
268      * @return the event header name space
269      */
270     private String getEventHeaderNamespace(final Map<?, ?> yamlMap, String name, final AxEvent eventDefinition) {
271         // Check the name space is OK if it is defined, if not, use the name space from the model
272         var namespace = getYamlStringField(yamlMap, ApexEvent.NAMESPACE_HEADER_FIELD, yamlPars.getNameSpaceAlias(),
273                         ApexEvent.NAMESPACE_REGEXP, false);
274         if (namespace != null) {
275             if (!namespace.equals(eventDefinition.getNameSpace())) {
276                 throw new ApexEventRuntimeException("namespace \"" + namespace + "\" on event \"" + name
277                                 + "\" does not match namespace \"" + eventDefinition.getNameSpace()
278                                 + "\" for that event in the Apex model");
279             }
280         } else {
281             namespace = eventDefinition.getNameSpace();
282         }
283         return namespace;
284     }
285
286     /**
287      * Get the event header source.
288      *
289      * @param yamlMap the YAML map to read from
290      * @param eventDefinition the event definition
291      * @return the event header source
292      */
293     private String getEventHeaderSource(final Map<?, ?> yamlMap, final AxEvent eventDefinition) {
294         // For source, use the defined source only if the source is not found on the incoming event
295         var source = getYamlStringField(yamlMap, ApexEvent.SOURCE_HEADER_FIELD, yamlPars.getSourceAlias(),
296                         ApexEvent.SOURCE_REGEXP, false);
297         if (source == null) {
298             source = eventDefinition.getSource();
299         }
300         return source;
301     }
302
303     /**
304      * Get the event header target.
305      *
306      * @param yamlMap the YAML map to read from
307      * @param eventDefinition the event definition
308      * @return the event header target
309      */
310     private String getHeaderTarget(final Map<?, ?> yamlMap, final AxEvent eventDefinition) {
311         // For target, use the defined source only if the source is not found on the incoming event
312         var target = getYamlStringField(yamlMap, ApexEvent.TARGET_HEADER_FIELD, yamlPars.getTargetAlias(),
313                         ApexEvent.TARGET_REGEXP, false);
314         if (target == null) {
315             target = eventDefinition.getTarget();
316         }
317         return target;
318     }
319
320     /**
321      * This method gets an event string field from a JSON object.
322      *
323      * @param yamlMap the YAML containing the YAML representation of the incoming event
324      * @param fieldName the field name to find in the event
325      * @param fieldAlias the alias for the field to find in the event, overrides the field name if it is not null
326      * @param fieldRegexp the regular expression to check the field against for validity
327      * @param mandatory true if the field is mandatory
328      * @return the value of the field in the JSON object or null if the field is optional
329      * @throws ApexEventRuntimeException the apex event runtime exception
330      */
331     private String getYamlStringField(final Map<?, ?> yamlMap, final String fieldName, final String fieldAlias,
332                     final String fieldRegexp, final boolean mandatory) {
333         // Get the YAML field for the string field
334         final Object yamlField = getYamlField(yamlMap, fieldName, fieldAlias, mandatory);
335
336         // Null strings are allowed
337         if (yamlField == null) {
338             return null;
339         }
340
341         if (!(yamlField instanceof String)) {
342             // The element is not a string so throw an error
343             throw new ApexEventRuntimeException("field \"" + fieldName + "\" with type \""
344                             + yamlField.getClass().getName() + "\" is not a string value");
345         }
346
347         final var fieldValueString = (String) yamlField;
348
349         // Is regular expression checking required
350         if (fieldRegexp == null) {
351             return fieldValueString;
352         }
353
354         // Check the event field against its regular expression
355         if (!fieldValueString.matches(fieldRegexp)) {
356             throw new ApexEventRuntimeException(
357                             "field \"" + fieldName + "\" with value \"" + fieldValueString + "\" is invalid");
358         }
359
360         return fieldValueString;
361     }
362
363     /**
364      * This method gets an event field from a YAML object.
365      *
366      * @param yamlMap the YAML containing the YAML representation of the incoming event
367      * @param fieldName the field name to find in the event
368      * @param fieldAlias the alias for the field to find in the event, overrides the field name if it is not null
369      * @param mandatory true if the field is mandatory
370      * @return the value of the field in the YAML object or null if the field is optional
371      * @throws ApexEventRuntimeException the apex event runtime exception
372      */
373     private Object getYamlField(final Map<?, ?> yamlMap, final String fieldName, final String fieldAlias,
374                     final boolean mandatory) {
375
376         // Check if we should use the alias for this field
377         String fieldToFind = fieldName;
378         if (fieldAlias != null) {
379             fieldToFind = fieldAlias;
380         }
381
382         // Get the event field
383         final Object eventElement = yamlMap.get(fieldToFind);
384         if (eventElement == null) {
385             if (!mandatory) {
386                 return null;
387             } else {
388                 throw new ApexEventRuntimeException("mandatory field \"" + fieldToFind + "\" is missing");
389             }
390         }
391
392         return eventElement;
393     }
394 }