30e9db7221a3abe861ace8acb7a97efd34dece7a
[policy/apex-pdp.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2016-2018 Ericsson. All rights reserved.
4  * ================================================================================
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  * 
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  * 
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.policy.apex.service.engine.event.impl.jsonprotocolplugin;
22
23 import java.util.ArrayList;
24 import java.util.List;
25
26 import org.onap.policy.apex.context.SchemaHelper;
27 import org.onap.policy.apex.context.impl.schema.SchemaHelperFactory;
28 import org.onap.policy.apex.model.basicmodel.service.ModelService;
29 import org.onap.policy.apex.model.eventmodel.concepts.AxEvent;
30 import org.onap.policy.apex.model.eventmodel.concepts.AxEvents;
31 import org.onap.policy.apex.model.eventmodel.concepts.AxField;
32 import org.onap.policy.apex.service.engine.event.ApexEvent;
33 import org.onap.policy.apex.service.engine.event.ApexEventException;
34 import org.onap.policy.apex.service.engine.event.ApexEventProtocolConverter;
35 import org.onap.policy.apex.service.engine.event.ApexEventRuntimeException;
36 import org.onap.policy.apex.service.parameters.eventprotocol.EventProtocolParameters;
37 import org.slf4j.ext.XLogger;
38 import org.slf4j.ext.XLoggerFactory;
39
40 import com.google.gson.Gson;
41 import com.google.gson.GsonBuilder;
42 import com.google.gson.JsonElement;
43 import com.google.gson.JsonObject;
44
45 /**
46  * The Class Apex2JSONEventConverter converts {@link ApexEvent} instances to and from JSON string
47  * representations of Apex events.
48  *
49  * @author Liam Fallon (liam.fallon@ericsson.com)
50  */
51 public class Apex2JSONEventConverter implements ApexEventProtocolConverter {
52     private static final XLogger LOGGER = XLoggerFactory.getXLogger(Apex2JSONEventConverter.class);
53
54     // The parameters for the JSON event protocol
55     private JSONEventProtocolParameters jsonPars;
56
57     /*
58      * (non-Javadoc)
59      * 
60      * @see
61      * org.onap.policy.apex.service.engine.event.ApexEventProtocolConverter#init(org.onap.policy.
62      * apex.service.parameters.eventprotocol.EventProtocolParameters)
63      */
64     @Override
65     public void init(final EventProtocolParameters parameters) {
66         // Check and get the JSON parameters
67         if (!(parameters instanceof JSONEventProtocolParameters)) {
68             final String errorMessage = "specified consumer properties are not applicable to the JSON event protocol";
69             LOGGER.warn(errorMessage);
70             throw new ApexEventRuntimeException(errorMessage);
71         }
72
73         jsonPars = (JSONEventProtocolParameters) parameters;
74     }
75
76     /*
77      * (non-Javadoc)
78      *
79      * @see
80      * org.onap.policy.apex.service.engine.event.ApexEventConverter#toApexEvent(java.lang.String,
81      * java.lang.Object)
82      */
83     @Override
84     public List<ApexEvent> toApexEvent(final String eventName, final Object eventObject) throws ApexEventException {
85         // Check the event eventObject
86         if (eventObject == null) {
87             LOGGER.warn("event processing failed, event is null");
88             throw new ApexEventException("event processing failed, event is null");
89         }
90
91         // Cast the event to a string, if our conversion is correctly configured, this cast should
92         // always work
93         String jsonEventString = null;
94         try {
95             jsonEventString = (String) eventObject;
96         } catch (final Exception e) {
97             final String errorMessage = "error converting event \"" + eventObject + "\" to a string";
98             LOGGER.debug(errorMessage, e);
99             throw new ApexEventRuntimeException(errorMessage, e);
100         }
101
102         // The list of events we will return
103         final List<ApexEvent> eventList = new ArrayList<>();
104
105         try {
106             // We may have a single JSON object with a single event or an array of JSON objects
107             final Object decodedJsonObject =
108                     new GsonBuilder().serializeNulls().create().fromJson(jsonEventString, Object.class);
109
110             // Check if we have a list of objects
111             if (decodedJsonObject instanceof List) {
112                 // Check if it's a list of JSON objects or a list of strings
113                 @SuppressWarnings("unchecked")
114                 final List<Object> decodedJsonList = (List<Object>) decodedJsonObject;
115
116                 // Decode each of the list elements in sequence
117                 for (final Object jsonListObject : decodedJsonList) {
118                     if (jsonListObject instanceof String) {
119                         eventList.add(jsonStringApexEvent(eventName, (String) jsonListObject));
120                     } else if (jsonListObject instanceof JsonObject) {
121                         eventList.add(jsonObject2ApexEvent(eventName, (JsonObject) jsonListObject));
122                     } else {
123                         throw new ApexEventException("incoming event (" + jsonEventString
124                                 + ") is a JSON object array containing an invalid object " + jsonListObject);
125                     }
126                 }
127             } else {
128                 eventList.add(jsonStringApexEvent(eventName, jsonEventString));
129             }
130         } catch (final Exception e) {
131             final String errorString =
132                     "Failed to unmarshal JSON event: " + e.getMessage() + ", event=" + jsonEventString;
133             LOGGER.warn(errorString, e);
134             throw new ApexEventException(errorString, e);
135         }
136
137         // Return the list of events we have unmarshalled
138         return eventList;
139     }
140
141     /*
142      * (non-Javadoc)
143      *
144      * @see
145      * org.onap.policy.apex.service.engine.event.ApexEventConverter#fromApexEvent(org.onap.policy.
146      * apex.service.engine.event.ApexEvent)
147      */
148     @Override
149     public Object fromApexEvent(final ApexEvent apexEvent) throws ApexEventException {
150         // Check the Apex event
151         if (apexEvent == null) {
152             LOGGER.warn("event processing failed, Apex event is null");
153             throw new ApexEventException("event processing failed, Apex event is null");
154         }
155
156         // Get the event definition for the event from the model service
157         final AxEvent eventDefinition =
158                 ModelService.getModel(AxEvents.class).get(apexEvent.getName(), apexEvent.getVersion());
159
160         // Use a GSON Json object to marshal the Apex event to JSON
161         final Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
162         final JsonObject jsonObject = new JsonObject();
163
164         jsonObject.addProperty(ApexEvent.NAME_HEADER_FIELD, apexEvent.getName());
165         jsonObject.addProperty(ApexEvent.VERSION_HEADER_FIELD, apexEvent.getVersion());
166         jsonObject.addProperty(ApexEvent.NAMESPACE_HEADER_FIELD, apexEvent.getNameSpace());
167         jsonObject.addProperty(ApexEvent.SOURCE_HEADER_FIELD, apexEvent.getSource());
168         jsonObject.addProperty(ApexEvent.TARGET_HEADER_FIELD, apexEvent.getTarget());
169
170         if (apexEvent.getExceptionMessage() != null) {
171             jsonObject.addProperty(ApexEvent.EXCEPTION_MESSAGE_HEADER_FIELD, apexEvent.getExceptionMessage());
172         }
173
174         for (final AxField eventField : eventDefinition.getFields()) {
175             final String fieldName = eventField.getKey().getLocalName();
176
177             if (!apexEvent.containsKey(fieldName)) {
178                 if (!eventField.getOptional()) {
179                     final String errorMessage = "error parsing " + eventDefinition.getId() + " event to Json. "
180                             + "Field \"" + fieldName + "\" is missing, but is mandatory. Fields: " + apexEvent;
181                     LOGGER.debug(errorMessage);
182                     throw new ApexEventRuntimeException(errorMessage);
183                 }
184                 continue;
185             }
186
187             final Object fieldValue = apexEvent.get(fieldName);
188
189             // Get the schema helper
190             final SchemaHelper fieldSchemaHelper =
191                     new SchemaHelperFactory().createSchemaHelper(eventField.getKey(), eventField.getSchema());
192             jsonObject.add(fieldName, (JsonElement) fieldSchemaHelper.marshal2Object(fieldValue));
193         }
194
195         // Output JSON string in a pretty format
196         return gson.toJson(jsonObject);
197     }
198
199     /**
200      * This method converts a JSON object into an Apex event.
201      *
202      * @param eventName the name of the event
203      * @param jsonEventString the JSON string that holds the event
204      * @return the apex event that we have converted the JSON object into
205      * @throws ApexEventException thrown on unmarshaling exceptions
206      */
207     private ApexEvent jsonStringApexEvent(final String eventName, final String jsonEventString)
208             throws ApexEventException {
209         // Use GSON to read the event string
210         final JsonObject jsonObject =
211                 new GsonBuilder().serializeNulls().create().fromJson(jsonEventString, JsonObject.class);
212
213         if (jsonObject == null || !jsonObject.isJsonObject()) {
214             throw new ApexEventException(
215                     "incoming event (" + jsonEventString + ") is not a JSON object or an JSON object array");
216         }
217
218         return jsonObject2ApexEvent(eventName, jsonObject);
219     }
220
221     /**
222      * This method converts a JSON object into an Apex event.
223      *
224      * @param eventName the name of the event
225      * @param jsonObject the JSON object that holds the event
226      * @return the apex event that we have converted the JSON object into
227      * @throws ApexEventException thrown on unmarshaling exceptions
228      */
229     private ApexEvent jsonObject2ApexEvent(final String eventName, final JsonObject jsonObject)
230             throws ApexEventException {
231         // Process the mandatory Apex header
232         final ApexEvent apexEvent = processApexEventHeader(eventName, jsonObject);
233
234         // Get the event definition for the event from the model service
235         final AxEvent eventDefinition =
236                 ModelService.getModel(AxEvents.class).get(apexEvent.getName(), apexEvent.getVersion());
237
238         // Iterate over the input fields in the event
239         for (final AxField eventField : eventDefinition.getFields()) {
240             final String fieldName = eventField.getKey().getLocalName();
241             if (!hasJSONField(jsonObject, fieldName)) {
242                 if (!eventField.getOptional()) {
243                     final String errorMessage = "error parsing " + eventDefinition.getId() + " event from Json. "
244                             + "Field \"" + fieldName + "\" is missing, but is mandatory.";
245                     LOGGER.debug(errorMessage);
246                     throw new ApexEventException(errorMessage);
247                 }
248                 continue;
249             }
250
251             final JsonElement fieldValue = getJSONField(jsonObject, fieldName, null, !eventField.getOptional());
252
253             if (fieldValue != null && !fieldValue.isJsonNull()) {
254                 // Get the schema helper
255                 final SchemaHelper fieldSchemaHelper =
256                         new SchemaHelperFactory().createSchemaHelper(eventField.getKey(), eventField.getSchema());
257                 apexEvent.put(fieldName, fieldSchemaHelper.createNewInstance(fieldValue));
258             } else {
259                 apexEvent.put(fieldName, null);
260             }
261         }
262         return apexEvent;
263
264     }
265
266     /**
267      * This method processes the event header of an Apex event.
268      *
269      * @param eventName the name of the event
270      * @param jsonObject the JSON object containing the JSON representation of the incoming event
271      * @return an apex event constructed using the header fields of the event
272      * @throws ApexEventRuntimeException the apex event runtime exception
273      * @throws ApexEventException on invalid events with missing header fields
274      */
275     private ApexEvent processApexEventHeader(final String eventName, final JsonObject jsonObject)
276             throws ApexEventException {
277         // Get the event header fields
278         // @formatter:off
279         String name      = getJSONStringField(jsonObject, ApexEvent.NAME_HEADER_FIELD,      
280                 jsonPars.getNameAlias(), ApexEvent.NAME_REGEXP, false);
281         String version   = getJSONStringField(jsonObject, ApexEvent.VERSION_HEADER_FIELD,   
282                 jsonPars.getVersionAlias(), ApexEvent.VERSION_REGEXP, false);
283         String namespace = getJSONStringField(jsonObject, ApexEvent.NAMESPACE_HEADER_FIELD,
284                 jsonPars.getNameSpaceAlias(), ApexEvent.NAMESPACE_REGEXP, false);
285         String source    = getJSONStringField(jsonObject, ApexEvent.SOURCE_HEADER_FIELD,
286                 jsonPars.getSourceAlias(),    ApexEvent.SOURCE_REGEXP, false);
287         String target    = getJSONStringField(jsonObject, ApexEvent.TARGET_HEADER_FIELD,
288                 jsonPars.getTargetAlias(),    ApexEvent.TARGET_REGEXP, false);
289         // @formatter:on
290
291         // Check that an event name has been specified
292         if (name == null && eventName == null) {
293             throw new ApexEventRuntimeException(
294                     "event received without mandatory parameter \"name\" on configuration or on event");
295         }
296
297         // Check if an event name was specified on the event parameters
298         if (eventName != null) {
299             if (name != null && !eventName.equals(name)) {
300                 LOGGER.warn("The incoming event name \"{}\" does not match the configured event name \"{}\","
301                         + " using configured event name", name, eventName);
302             }
303             name = eventName;
304         }
305
306         // Now, find the event definition in the model service. If version is null, the newest event
307         // definition in the model service is used
308         final AxEvent eventDefinition = ModelService.getModel(AxEvents.class).get(name, version);
309         if (eventDefinition == null) {
310             if (version == null) {
311                 throw new ApexEventRuntimeException(
312                         "an event definition for an event named \"" + name + "\" not found in Apex model");
313             }
314             throw new ApexEventRuntimeException("an event definition for an event named \"" + name
315                     + "\" with version \"" + version + "\" not found in Apex model");
316         }
317
318         // Use the defined event version if no version is specified on the incoming fields
319         if (version == null) {
320             version = eventDefinition.getKey().getVersion();
321         }
322
323         // Check the name space is OK if it is defined, if not, use the name space from the model
324         if (namespace != null) {
325             if (!namespace.equals(eventDefinition.getNameSpace())) {
326                 throw new ApexEventRuntimeException(
327                         "namespace \"" + namespace + "\" on event \"" + name + "\" does not match namespace \""
328                                 + eventDefinition.getNameSpace() + "\" for that event in the Apex model");
329             }
330         } else {
331             namespace = eventDefinition.getNameSpace();
332         }
333
334         // For source, use the defined source only if the source is not found on the incoming event
335         if (source == null) {
336             source = eventDefinition.getSource();
337         }
338
339         // For target, use the defined source only if the source is not found on the incoming event
340         if (target == null) {
341             target = eventDefinition.getTarget();
342         }
343
344         return new ApexEvent(name, version, namespace, source, target);
345     }
346
347     /**
348      * This method gets an event string field from a JSON object.
349      *
350      * @param jsonObject the JSON object containing the JSON representation of the incoming event
351      * @param fieldName the field name to find in the event
352      * @param fieldAlias the alias for the field to find in the event, overrides the field name if
353      *        it is not null
354      * @param fieldRE the regular expression to check the field against for validity
355      * @param mandatory true if the field is mandatory
356      * @return the value of the field in the JSON object or null if the field is optional
357      * @throws ApexEventRuntimeException the apex event runtime exception
358      */
359     private String getJSONStringField(final JsonObject jsonObject, final String fieldName, final String fieldAlias,
360             final String fieldRE, final boolean mandatory) {
361         // Get the JSON field for the string field
362         final JsonElement jsonField = getJSONField(jsonObject, fieldName, fieldAlias, mandatory);
363
364         // Null strings are allowed
365         if (jsonField == null || jsonField.isJsonNull()) {
366             return null;
367         }
368
369         // Check if this is a string field
370         String fieldValueString = null;
371         try {
372             fieldValueString = jsonField.getAsString();
373         } catch (final Exception e) {
374             // The element is not a string so throw an error
375             throw new ApexEventRuntimeException("field \"" + fieldName + "\" with type \""
376                     + jsonField.getClass().getCanonicalName() + "\" is not a string value");
377         }
378
379         // Is regular expression checking required
380         if (fieldRE == null) {
381             return fieldValueString;
382         }
383
384         // Check the event field against its regular expression
385         if (!fieldValueString.matches(fieldRE)) {
386             throw new ApexEventRuntimeException(
387                     "field \"" + fieldName + "\" with value \"" + fieldValueString + "\" is invalid");
388         }
389
390         return fieldValueString;
391     }
392
393     /**
394      * This method gets an event field from a JSON object.
395      *
396      * @param jsonObject the JSON object containing the JSON representation of the incoming event
397      * @param fieldName the field name to find in the event
398      * @param fieldAlias the alias for the field to find in the event, overrides the field name if
399      *        it is not null
400      * @param mandatory true if the field is mandatory
401      * @return the value of the field in the JSON object or null if the field is optional
402      * @throws ApexEventRuntimeException the apex event runtime exception
403      */
404     private JsonElement getJSONField(final JsonObject jsonObject, final String fieldName, final String fieldAlias,
405             final boolean mandatory) {
406
407         // Check if we should use the alias for this field
408         String fieldToFind = fieldName;
409         if (fieldAlias != null) {
410             fieldToFind = fieldAlias;
411         }
412
413         // Get the event field
414         final JsonElement eventElement = jsonObject.get(fieldToFind);
415         if (eventElement == null) {
416             if (!mandatory) {
417                 return null;
418             } else {
419                 throw new ApexEventRuntimeException("mandatory field \"" + fieldToFind + "\" is missing");
420             }
421         }
422
423         return eventElement;
424     }
425
426     /**
427      * This method if a JSON object has a named field.
428      *
429      * @param jsonObject the JSON object containing the JSON representation of the incoming event
430      * @param fieldName the field name to find in the event
431      * @return true if the field is present
432      * @throws ApexEventRuntimeException the apex event runtime exception
433      */
434     private boolean hasJSONField(final JsonObject jsonObject, final String fieldName) {
435         // check for the field
436         return jsonObject.has(fieldName);
437     }
438 }