Sonar Issues in Apex plugins-context
[policy/apex-pdp.git] / plugins / plugins-context / plugins-context-schema / plugins-context-schema-avro / src / main / java / org / onap / policy / apex / plugins / context / schema / avro / AvroSchemaHelper.java
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.context.schema.avro;
24
25 import com.google.gson.Gson;
26 import com.google.gson.GsonBuilder;
27 import com.google.gson.JsonElement;
28 import java.io.ByteArrayOutputStream;
29 import java.util.LinkedHashSet;
30 import java.util.Set;
31 import org.apache.avro.Schema;
32 import org.apache.avro.Schema.Field;
33 import org.apache.avro.Schema.Type;
34 import org.apache.avro.generic.GenericDatumReader;
35 import org.apache.avro.generic.GenericDatumWriter;
36 import org.apache.avro.generic.GenericRecord;
37 import org.apache.avro.io.DatumWriter;
38 import org.apache.avro.io.DecoderFactory;
39 import org.apache.avro.io.EncoderFactory;
40 import org.onap.policy.apex.context.ContextRuntimeException;
41 import org.onap.policy.apex.context.impl.schema.AbstractSchemaHelper;
42 import org.onap.policy.apex.model.basicmodel.concepts.AxArtifactKey;
43 import org.onap.policy.apex.model.basicmodel.concepts.AxKey;
44 import org.onap.policy.apex.model.contextmodel.concepts.AxContextSchema;
45
46 /**
47  * This class is the implementation of the {@link org.onap.policy.apex.context.SchemaHelper} interface for Avro schemas.
48  *
49  * @author Liam Fallon (liam.fallon@ericsson.com)
50  */
51 public class AvroSchemaHelper extends AbstractSchemaHelper {
52     // Get a reference to the logger
53
54     // Recurring string constants
55     private static final String OBJECT_TAG = ": object \"";
56
57     // The Avro schema for this context schema
58     private Schema avroSchema;
59
60     // The mapper that translates between Java and Avro objects
61     private AvroObjectMapper avroObjectMapper;
62
63     @Override
64     public void init(final AxKey userKey, final AxContextSchema schema) {
65         super.init(userKey, schema);
66
67         // Configure the Avro schema
68         try {
69             avroSchema = new Schema.Parser().parse(schema.getSchema());
70         } catch (final Exception e) {
71             final String resultSting = userKey.getId() + ": avro context schema \"" + schema.getId()
72                 + "\" schema is invalid, schema: " + schema.getSchema();
73             throw new ContextRuntimeException(resultSting, e);
74         }
75
76         // Get the object mapper for the schema type to a Java class
77         avroObjectMapper = new AvroObjectMapperFactory().get(userKey, avroSchema);
78
79         // Get the Java type for this schema, if it is a primitive type then we can do direct
80         // conversion to JAva
81         setSchemaClass(avroObjectMapper.getJavaClass());
82     }
83
84     /**
85      * Getter to get the Avro schema.
86      *
87      * @return the Avro schema
88      */
89     public Schema getAvroSchema() {
90         return avroSchema;
91     }
92
93     @Override
94     public Object getSchemaObject() {
95         return getAvroSchema();
96     }
97
98     @Override
99     public Object createNewInstance() {
100         // Create a new instance using the Avro object mapper
101         final var newInstance = avroObjectMapper.createNewInstance(avroSchema);
102
103         // If no new instance is created, use default schema handler behaviour
104         if (newInstance != null) {
105             return newInstance;
106         } else {
107             return super.createNewInstance();
108         }
109     }
110
111     @Override
112     public Object createNewInstance(final String stringValue) {
113         return unmarshal(stringValue);
114     }
115
116     @Override
117     public Object createNewInstance(final Object incomingObject) {
118         if (incomingObject instanceof JsonElement) {
119             final var gson = new GsonBuilder().serializeNulls().create();
120             final var elementJsonString = gson.toJson((JsonElement) incomingObject);
121
122             return createNewInstance(elementJsonString);
123         } else {
124             final var returnString =
125                     getUserKey().getId() + ": the object \"" + incomingObject + "\" is not an instance of JsonObject";
126             throw new ContextRuntimeException(returnString);
127         }
128     }
129
130     @Override
131     public Object createNewSubInstance(final String subInstanceType) {
132         final Set<String> foundTypes = new LinkedHashSet<>();
133
134         Object subInstance = createNewSubInstance(avroSchema, subInstanceType, foundTypes);
135
136         if (subInstance != null) {
137             return subInstance;
138         } else {
139             final var returnString = getUserKey().getId() + ": the schema \"" + avroSchema.getName()
140                     + "\" does not have a subtype of type \"" + subInstanceType + "\"";
141             throw new ContextRuntimeException(returnString);
142         }
143     }
144
145     /**
146      * Create an instance of a sub type of this type.
147      *
148      * @param schema the Avro schema of the the type
149      * @param subInstanceType the sub type
150      * @param foundTypes types we have already found
151      * @return the sub type schema or null if it is not created
152      */
153     private Object createNewSubInstance(Schema schema, String subInstanceType, final Set<String> foundTypes) {
154         // Try Array element types
155         if (Type.ARRAY == schema.getType()) {
156             Object newInstance = instantiateSubInstance(subInstanceType, schema.getElementType(), foundTypes);
157             if (newInstance != null) {
158                 return newInstance;
159             }
160         }
161
162         if (Type.MAP == schema.getType()) {
163             Object newInstance = instantiateSubInstance(subInstanceType, schema.getValueType(), foundTypes);
164             if (newInstance != null) {
165                 return newInstance;
166             }
167         }
168
169         if (Type.RECORD == schema.getType()) {
170             for (Field field : schema.getFields()) {
171                 Object newInstance = instantiateSubInstance(subInstanceType, field.schema(), foundTypes);
172                 if (newInstance != null) {
173                     return newInstance;
174                 }
175             }
176         }
177
178         return null;
179     }
180
181     /**
182      * Instantiate a sub instance of a type.
183      *
184      * @param subInstanceType the type of the sub instance to create
185      * @param subSchema the sub schema we have received
186      * @param foundTypes types we have already found
187      * @return an instance of the type or null if it is the incorrect type
188      */
189     private Object instantiateSubInstance(final String subInstanceType, final Schema subSchema,
190             final Set<String> foundTypes) {
191         if (subSchema == null) {
192             return null;
193         }
194
195         // Check for recursive use of field names in records, if we have already checked a field name
196         // skip it this time.
197         if (foundTypes.contains(subSchema.getName())) {
198             return null;
199         }
200
201         foundTypes.add(subSchema.getName());
202
203         if (subSchema.getName().equals(subInstanceType)) {
204             return new AvroObjectMapperFactory().get(AxArtifactKey.getNullKey(), subSchema)
205                     .createNewInstance(subSchema);
206         }
207         return createNewSubInstance(subSchema, subInstanceType, foundTypes);
208     }
209
210     @Override
211     public Object unmarshal(final Object object) {
212         // If an object is already in the correct format, just carry on
213         if (passThroughObject(object)) {
214             return object;
215         }
216
217         var objectString = getStringObject(object);
218
219         // Translate illegal characters in incoming JSON keys to legal Avro values
220         objectString = AvroSchemaKeyTranslationUtilities.translateIllegalKeys(objectString, false);
221
222         // Decode the object
223         Object decodedObject;
224         try {
225             final var jsonDecoder = DecoderFactory.get().jsonDecoder(avroSchema, objectString);
226             decodedObject = new GenericDatumReader<GenericRecord>(avroSchema).read(null, jsonDecoder);
227         } catch (final Exception e) {
228             final var returnString =
229                 getUserKey().getId() + OBJECT_TAG + objectString + "\" Avro unmarshalling failed.";
230             throw new ContextRuntimeException(returnString, e);
231         }
232
233         // Now map the decoded object into something we can handle
234         return avroObjectMapper.mapFromAvro(decodedObject);
235     }
236
237     /**
238      * Check that the incoming object is a string, the incoming object must be a string containing Json.
239      *
240      * @param object incoming object
241      * @return object as String
242      */
243     private String getStringObject(final Object object) {
244         try {
245             if (isObjectString(object)) {
246                 return getObjectString(object);
247             } else {
248                 return (String) object;
249             }
250         } catch (final ClassCastException e) {
251             final var returnString = getUserKey().getId() + OBJECT_TAG + object + "\" of type \""
252                     + (object != null ? object.getClass().getName() : "null") + "\" must be assignable to \""
253                     + getSchemaClass().getName() + "\" or be a Json string representation of it for Avro unmarshalling";
254             throw new ContextRuntimeException(returnString, e);
255         }
256     }
257
258     /**
259      * Get a string object.
260      *
261      * @param object the string object
262      * @return the string
263      */
264     private String getObjectString(final Object object) {
265         var objectString = object.toString().trim();
266         if (objectString.length() == 0) {
267             return "\"\"";
268         } else if (objectString.length() == 1) {
269             return "\"" + objectString + "\"";
270         } else {
271             // All strings must be quoted for decoding
272             if (objectString.charAt(0) != '"') {
273                 objectString = '"' + objectString;
274             }
275             if (objectString.charAt(objectString.length() - 1) != '"') {
276                 objectString += '"';
277             }
278         }
279         return objectString;
280     }
281
282     private boolean isObjectString(final Object object) {
283         return object != null && avroSchema.getType().equals(Schema.Type.STRING);
284     }
285
286     @Override
287     public String marshal2String(final Object object) {
288         // Condition the object for Avro encoding
289         final var conditionedObject = avroObjectMapper.mapToAvro(object);
290
291         final var jsonString = getJsonString(object, conditionedObject);
292
293         return AvroSchemaKeyTranslationUtilities.translateIllegalKeys(jsonString, true);
294     }
295
296     private String getJsonString(final Object object, final Object conditionedObject) {
297
298         try (final var output = new ByteArrayOutputStream()) {
299             final DatumWriter<Object> writer = new GenericDatumWriter<>(avroSchema);
300             final var jsonEncoder = EncoderFactory.get().jsonEncoder(avroSchema, output, true);
301             writer.write(conditionedObject, jsonEncoder);
302             jsonEncoder.flush();
303             return new String(output.toByteArray());
304         } catch (final Exception e) {
305             final var returnString = getUserKey().getId() + OBJECT_TAG + object + "\" Avro marshalling failed.";
306             throw new ContextRuntimeException(returnString, e);
307         }
308     }
309
310     @Override
311     public JsonElement marshal2Object(final Object schemaObject) {
312         // Get the object as a Json string
313         final var schemaObjectAsString = marshal2String(schemaObject);
314
315         // Get a Gson instance to convert the Json string to an object created by Json
316         final var gson = new Gson();
317
318         // Convert the Json string into an object
319         final var schemaObjectAsObject = gson.fromJson(schemaObjectAsString, Object.class);
320
321         return gson.toJsonTree(schemaObjectAsObject);
322     }
323
324     /**
325      * Check if we can pass this object straight through encoding or decoding, is it an object native to the schema.
326      *
327      * @param object the object to check
328      * @return true if it's a straight pass through
329      */
330     private boolean passThroughObject(final Object object) {
331         if (object == null || getSchemaClass() == null) {
332             return false;
333         }
334
335         // All strings must be mapped
336         if (object instanceof String) {
337             return false;
338         }
339
340         // Now, check if the object is native
341         return getSchemaClass().isAssignableFrom(object.getClass());
342     }
343 }