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