df430b6836d21276d49f55e0373ea875f4626004
[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 java.io.ByteArrayOutputStream;
24
25 import org.apache.avro.Schema;
26 import org.apache.avro.generic.GenericDatumReader;
27 import org.apache.avro.generic.GenericDatumWriter;
28 import org.apache.avro.generic.GenericRecord;
29 import org.apache.avro.io.DatumWriter;
30 import org.apache.avro.io.DecoderFactory;
31 import org.apache.avro.io.EncoderFactory;
32 import org.apache.avro.io.JsonDecoder;
33 import org.apache.avro.io.JsonEncoder;
34 import org.onap.policy.apex.context.ContextRuntimeException;
35 import org.onap.policy.apex.context.impl.schema.AbstractSchemaHelper;
36 import org.onap.policy.apex.model.basicmodel.concepts.AxKey;
37 import org.onap.policy.apex.model.contextmodel.concepts.AxContextSchema;
38 import org.slf4j.ext.XLogger;
39 import org.slf4j.ext.XLoggerFactory;
40
41 import com.google.gson.Gson;
42 import com.google.gson.GsonBuilder;
43 import com.google.gson.JsonElement;
44
45 /**
46  * This class is the implementation of the {@link org.onap.policy.apex.context.SchemaHelper}
47  * 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     private static final XLogger LOGGER = XLoggerFactory.getXLogger(AvroSchemaHelper.class);
54
55     // The Avro schema for this context schema
56     private Schema avroSchema;
57
58     // The mapper that translates between Java and Avro objects
59     private AvroObjectMapper avroObjectMapper;
60
61     @Override
62     public void init(final AxKey userKey, final AxContextSchema schema) throws ContextRuntimeException {
63         super.init(userKey, schema);
64
65         // Configure the Avro schema
66         try {
67             avroSchema = new Schema.Parser().parse(schema.getSchema());
68         } catch (final Exception e) {
69             final String resultSting = userKey.getId() + ": avro context schema \"" + schema.getId()
70                     + "\" schema is invalid: " + e.getMessage() + ", schema: " + schema.getSchema();
71             LOGGER.warn(resultSting);
72             throw new ContextRuntimeException(resultSting);
73         }
74
75         // Get the object mapper for the schema type to a Java class
76         avroObjectMapper = new AvroObjectMapperFactory().get(userKey, avroSchema);
77
78         // Get the Java type for this schema, if it is a primitive type then we can do direct
79         // conversion to JAva
80         setSchemaClass(avroObjectMapper.getJavaClass());
81     }
82
83     /**
84      * Getter to get the Avro schema.
85      *
86      * @return the Avro schema
87      */
88     public Schema getAvroSchema() {
89         return avroSchema;
90     }
91
92     @Override
93     public Object getSchemaObject() {
94         return avroSchema;
95     }
96
97     @Override
98     public Object createNewInstance() {
99         // Create a new instance using the Avro object mapper
100         final Object newInstance = avroObjectMapper.createNewInstance(avroSchema);
101
102         // If no new instance is created, use default schema handler behavior
103         if (newInstance != null) {
104             return newInstance;
105         } else {
106             return super.createNewInstance();
107         }
108     }
109
110     @Override
111     public Object createNewInstance(final String stringValue) {
112         return unmarshal(stringValue);
113     }
114
115     @Override
116     public Object createNewInstance(final Object incomingObject) {
117         if (incomingObject instanceof JsonElement) {
118             final Gson gson = new GsonBuilder().serializeNulls().create();
119             final String elementJsonString = gson.toJson((JsonElement) incomingObject);
120
121             return createNewInstance(elementJsonString);
122         }
123         else {
124             final String returnString = getUserKey().getId() + ": the object \"" + incomingObject
125             + "\" is not an instance of JsonObject";
126             LOGGER.warn(returnString);
127             throw new ContextRuntimeException(returnString);
128         }
129     }
130
131     @Override
132     public Object unmarshal(final Object object) {
133         // If an object is already in the correct format, just carry on
134         if (passThroughObject(object)) {
135             return object;
136         }
137
138         String objectString = getStringObject(object);
139
140         // Translate illegal characters in incoming JSON keys to legal Avro values
141         objectString = AvroSchemaKeyTranslationUtilities.translateIllegalKeys(objectString, false);
142
143         // Decode the object
144         Object decodedObject;
145         try {
146             final JsonDecoder jsonDecoder = DecoderFactory.get().jsonDecoder(avroSchema, objectString);
147             decodedObject = new GenericDatumReader<GenericRecord>(avroSchema).read(null, jsonDecoder);
148         } catch (final Exception e) {
149             final String returnString = getUserKey().getId() + ": object \"" + objectString
150                     + "\" Avro unmarshalling failed: " + e.getMessage();
151             LOGGER.warn(returnString, e);
152             throw new ContextRuntimeException(returnString, e);
153         }
154
155         // Now map the decoded object into something we can handle
156         return avroObjectMapper.mapFromAvro(decodedObject);
157     }
158
159     /**
160      * Check that the incoming object is a string, the incoming object must be a string containing
161      * Json
162      * 
163      * @param object incoming object
164      * @return object as String
165      */
166     private String getStringObject(final Object object) {
167         try {
168             if (isObjectString(object)) {
169                 String objectString = object.toString().trim();
170                 if (objectString.length() == 0) {
171                     return "\"\"";
172                 } else if (objectString.length() == 1) {
173                     return "\"" + objectString + "\"";
174                 } else {
175                     // All strings must be quoted for decoding
176                     if (objectString.charAt(0) != '"') {
177                         objectString = '"' + objectString;
178                     }
179                     if (objectString.charAt(objectString.length() - 1) != '"') {
180                         objectString += '"';
181                     }
182                 }
183                 return objectString;
184             } else {
185                 return (String) object;
186             }
187         } catch (final ClassCastException e) {
188             final String returnString = getUserKey().getId() + ": object \"" + object + "\" of type \""
189                     + (object != null ? object.getClass().getCanonicalName() : "null") + "\" must be assignable to \""
190                     + getSchemaClass().getCanonicalName()
191                     + "\" or be a Json string representation of it for Avro unmarshalling";
192             LOGGER.warn(returnString);
193             throw new ContextRuntimeException(returnString);
194         }
195     }
196
197     private boolean isObjectString(final Object object) {
198         return object != null && avroSchema.getType().equals(Schema.Type.STRING);
199     }
200
201     @Override
202     public String marshal2String(final Object object) {
203         // Condition the object for Avro encoding
204         final Object conditionedObject = avroObjectMapper.mapToAvro(object);
205
206         final String jsonString = getJsonString(object, conditionedObject);
207
208         return AvroSchemaKeyTranslationUtilities.translateIllegalKeys(jsonString, true);
209     }
210
211     private String getJsonString(final Object object, final Object conditionedObject) {
212
213         try (final ByteArrayOutputStream output = new ByteArrayOutputStream();) {
214             final DatumWriter<Object> writer = new GenericDatumWriter<>(avroSchema);
215             final JsonEncoder jsonEncoder = EncoderFactory.get().jsonEncoder(avroSchema, output, true);
216             writer.write(conditionedObject, jsonEncoder);
217             jsonEncoder.flush();
218             return new String(output.toByteArray());
219         } catch (final Exception e) {
220             final String returnString =
221                     getUserKey().getId() + ": object \"" + object + "\" Avro marshalling failed: " + e.getMessage();
222             LOGGER.warn(returnString);
223             throw new ContextRuntimeException(returnString, e);
224         }
225     }
226
227     @Override
228     public JsonElement marshal2Object(final Object schemaObject) {
229         // Get the object as a Json string
230         final String schemaObjectAsString = marshal2String(schemaObject);
231
232         // Get a Gson instance to convert the Json string to an object created by Json
233         final Gson gson = new Gson();
234
235         // Convert the Json string into an object
236         final Object schemaObjectAsObject = gson.fromJson(schemaObjectAsString, Object.class);
237
238         return gson.toJsonTree(schemaObjectAsObject);
239     }
240
241     /**
242      * Check if we can pass this object straight through encoding or decoding, is it an object
243      * native to the schema.
244      *
245      * @param object the object to check
246      * @return true if it's a straight pass through
247      */
248     private boolean passThroughObject(final Object object) {
249         if (object == null || getSchemaClass() == null) {
250             return false;
251         }
252
253         // All strings must be mapped
254         if (object instanceof String) {
255             return false;
256         }
257
258         // Now, check if the object is native
259         return getSchemaClass().isAssignableFrom(object.getClass());
260     }
261 }