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