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