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