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