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