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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.policy.apex.plugins.context.schema.avro;
23 import com.google.gson.Gson;
24 import com.google.gson.GsonBuilder;
25 import com.google.gson.JsonElement;
27 import java.io.ByteArrayOutputStream;
29 import org.apache.avro.Schema;
30 import org.apache.avro.generic.GenericDatumReader;
31 import org.apache.avro.generic.GenericDatumWriter;
32 import org.apache.avro.generic.GenericRecord;
33 import org.apache.avro.io.DatumWriter;
34 import org.apache.avro.io.DecoderFactory;
35 import org.apache.avro.io.EncoderFactory;
36 import org.apache.avro.io.JsonDecoder;
37 import org.apache.avro.io.JsonEncoder;
38 import org.onap.policy.apex.context.ContextRuntimeException;
39 import org.onap.policy.apex.context.impl.schema.AbstractSchemaHelper;
40 import org.onap.policy.apex.model.basicmodel.concepts.AxKey;
41 import org.onap.policy.apex.model.contextmodel.concepts.AxContextSchema;
42 import org.slf4j.ext.XLogger;
43 import org.slf4j.ext.XLoggerFactory;
46 * This class is the implementation of the {@link org.onap.policy.apex.context.SchemaHelper} interface for Avro schemas.
48 * @author Liam Fallon (liam.fallon@ericsson.com)
50 public class AvroSchemaHelper extends AbstractSchemaHelper {
51 // Get a reference to the logger
52 private static final XLogger LOGGER = XLoggerFactory.getXLogger(AvroSchemaHelper.class);
54 // Recurring string constants
55 private static final String OBJECT_TAG = ": object \"";
57 // The Avro schema for this context schema
58 private Schema avroSchema;
60 // The mapper that translates between Java and Avro objects
61 private AvroObjectMapper avroObjectMapper;
64 public void init(final AxKey userKey, final AxContextSchema schema) {
65 super.init(userKey, schema);
67 // Configure the Avro schema
69 avroSchema = new Schema.Parser().parse(schema.getSchema());
70 } catch (final Exception e) {
71 final String resultSting = userKey.getId() + ": avro context schema \"" + schema.getId()
72 + "\" schema is invalid: " + e.getMessage() + ", schema: " + schema.getSchema();
73 LOGGER.warn(resultSting, e);
74 throw new ContextRuntimeException(resultSting);
77 // Get the object mapper for the schema type to a Java class
78 avroObjectMapper = new AvroObjectMapperFactory().get(userKey, avroSchema);
80 // Get the Java type for this schema, if it is a primitive type then we can do direct
82 setSchemaClass(avroObjectMapper.getJavaClass());
86 * Getter to get the Avro schema.
88 * @return the Avro schema
90 public Schema getAvroSchema() {
95 public Object getSchemaObject() {
96 return getAvroSchema();
100 public Object createNewInstance() {
101 // Create a new instance using the Avro object mapper
102 final Object newInstance = avroObjectMapper.createNewInstance(avroSchema);
104 // If no new instance is created, use default schema handler behavior
105 if (newInstance != null) {
108 return super.createNewInstance();
113 public Object createNewInstance(final String stringValue) {
114 return unmarshal(stringValue);
118 public Object createNewInstance(final Object incomingObject) {
119 if (incomingObject instanceof JsonElement) {
120 final Gson gson = new GsonBuilder().serializeNulls().create();
121 final String elementJsonString = gson.toJson((JsonElement) incomingObject);
123 return createNewInstance(elementJsonString);
125 final String returnString = getUserKey().getId() + ": the object \"" + incomingObject
126 + "\" is not an instance of JsonObject";
127 LOGGER.warn(returnString);
128 throw new ContextRuntimeException(returnString);
133 public Object unmarshal(final Object object) {
134 // If an object is already in the correct format, just carry on
135 if (passThroughObject(object)) {
139 String objectString = getStringObject(object);
141 // Translate illegal characters in incoming JSON keys to legal Avro values
142 objectString = AvroSchemaKeyTranslationUtilities.translateIllegalKeys(objectString, false);
145 Object decodedObject;
147 final JsonDecoder jsonDecoder = DecoderFactory.get().jsonDecoder(avroSchema, objectString);
148 decodedObject = new GenericDatumReader<GenericRecord>(avroSchema).read(null, jsonDecoder);
149 } catch (final Exception e) {
150 final String returnString = getUserKey().getId() + OBJECT_TAG + objectString
151 + "\" Avro unmarshalling failed: " + e.getMessage();
152 LOGGER.warn(returnString, e);
153 throw new ContextRuntimeException(returnString, e);
156 // Now map the decoded object into something we can handle
157 return avroObjectMapper.mapFromAvro(decodedObject);
161 * Check that the incoming object is a string, the incoming object must be a string containing Json.
163 * @param object incoming object
164 * @return object as String
166 private String getStringObject(final Object object) {
168 if (isObjectString(object)) {
169 return getObjectString(object);
171 return (String) object;
173 } catch (final ClassCastException e) {
174 final String returnString = getUserKey().getId() + OBJECT_TAG + object + "\" of type \""
175 + (object != null ? object.getClass().getCanonicalName() : "null")
176 + "\" must be assignable to \"" + getSchemaClass().getCanonicalName()
177 + "\" or be a Json string representation of it for Avro unmarshalling";
178 LOGGER.warn(returnString, e);
179 throw new ContextRuntimeException(returnString);
184 * Get a string object.
186 * @param object the string object
189 private String getObjectString(final Object object) {
190 String objectString = object.toString().trim();
191 if (objectString.length() == 0) {
193 } else if (objectString.length() == 1) {
194 return "\"" + objectString + "\"";
196 // All strings must be quoted for decoding
197 if (objectString.charAt(0) != '"') {
198 objectString = '"' + objectString;
200 if (objectString.charAt(objectString.length() - 1) != '"') {
207 private boolean isObjectString(final Object object) {
208 return object != null && avroSchema.getType().equals(Schema.Type.STRING);
212 public String marshal2String(final Object object) {
213 // Condition the object for Avro encoding
214 final Object conditionedObject = avroObjectMapper.mapToAvro(object);
216 final String jsonString = getJsonString(object, conditionedObject);
218 return AvroSchemaKeyTranslationUtilities.translateIllegalKeys(jsonString, true);
221 private String getJsonString(final Object object, final Object conditionedObject) {
223 try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) {
224 final DatumWriter<Object> writer = new GenericDatumWriter<>(avroSchema);
225 final JsonEncoder jsonEncoder = EncoderFactory.get().jsonEncoder(avroSchema, output, true);
226 writer.write(conditionedObject, jsonEncoder);
228 return new String(output.toByteArray());
229 } catch (final Exception e) {
230 final String returnString = getUserKey().getId() + OBJECT_TAG + object + "\" Avro marshalling failed: "
232 LOGGER.warn(returnString);
233 throw new ContextRuntimeException(returnString, e);
238 public JsonElement marshal2Object(final Object schemaObject) {
239 // Get the object as a Json string
240 final String schemaObjectAsString = marshal2String(schemaObject);
242 // Get a Gson instance to convert the Json string to an object created by Json
243 final Gson gson = new Gson();
245 // Convert the Json string into an object
246 final Object schemaObjectAsObject = gson.fromJson(schemaObjectAsString, Object.class);
248 return gson.toJsonTree(schemaObjectAsObject);
252 * Check if we can pass this object straight through encoding or decoding, is it an object native to the schema.
254 * @param object the object to check
255 * @return true if it's a straight pass through
257 private boolean passThroughObject(final Object object) {
258 if (object == null || getSchemaClass() == null) {
262 // All strings must be mapped
263 if (object instanceof String) {
267 // Now, check if the object is native
268 return getSchemaClass().isAssignableFrom(object.getClass());