015d9ea30ae36d5244695b6abcfabf6464aa160c
[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
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;
44
45 /**
46  * This class is the implementation of the {@link org.onap.policy.apex.context.SchemaHelper} interface for Avro schemas.
47  *
48  * @author Liam Fallon (liam.fallon@ericsson.com)
49  */
50 public class AvroSchemaHelper extends AbstractSchemaHelper {
51     // Get a reference to the logger
52     private static final XLogger LOGGER = XLoggerFactory.getXLogger(AvroSchemaHelper.class);
53
54     // Recurring string constants
55     private static final String OBJECT_TAG = ": object \"";
56
57     // The Avro schema for this context schema
58     private Schema avroSchema;
59
60     // The mapper that translates between Java and Avro objects
61     private AvroObjectMapper avroObjectMapper;
62
63     @Override
64     public void init(final AxKey userKey, final AxContextSchema schema) {
65         super.init(userKey, schema);
66
67         // Configure the Avro schema
68         try {
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);
75         }
76
77         // Get the object mapper for the schema type to a Java class
78         avroObjectMapper = new AvroObjectMapperFactory().get(userKey, avroSchema);
79
80         // Get the Java type for this schema, if it is a primitive type then we can do direct
81         // conversion to JAva
82         setSchemaClass(avroObjectMapper.getJavaClass());
83     }
84
85     /**
86      * Getter to get the Avro schema.
87      *
88      * @return the Avro schema
89      */
90     public Schema getAvroSchema() {
91         return avroSchema;
92     }
93
94     @Override
95     public Object getSchemaObject() {
96         return getAvroSchema();
97     }
98
99     @Override
100     public Object createNewInstance() {
101         // Create a new instance using the Avro object mapper
102         final Object newInstance = avroObjectMapper.createNewInstance(avroSchema);
103
104         // If no new instance is created, use default schema handler behavior
105         if (newInstance != null) {
106             return newInstance;
107         } else {
108             return super.createNewInstance();
109         }
110     }
111
112     @Override
113     public Object createNewInstance(final String stringValue) {
114         return unmarshal(stringValue);
115     }
116
117     @Override
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);
122
123             return createNewInstance(elementJsonString);
124         } else {
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);
129         }
130     }
131
132     @Override
133     public Object unmarshal(final Object object) {
134         // If an object is already in the correct format, just carry on
135         if (passThroughObject(object)) {
136             return object;
137         }
138
139         String objectString = getStringObject(object);
140
141         // Translate illegal characters in incoming JSON keys to legal Avro values
142         objectString = AvroSchemaKeyTranslationUtilities.translateIllegalKeys(objectString, false);
143
144         // Decode the object
145         Object decodedObject;
146         try {
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);
154         }
155
156         // Now map the decoded object into something we can handle
157         return avroObjectMapper.mapFromAvro(decodedObject);
158     }
159
160     /**
161      * Check that the incoming object is a string, the incoming object must be a string containing 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                 return getObjectString(object);
170             } else {
171                 return (String) object;
172             }
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);
180         }
181     }
182
183     /**
184      * Get a string object.
185      * 
186      * @param object the string object
187      * @return the string
188      */
189     private String getObjectString(final Object object) {
190         String objectString = object.toString().trim();
191         if (objectString.length() == 0) {
192             return "\"\"";
193         } else if (objectString.length() == 1) {
194             return "\"" + objectString + "\"";
195         } else {
196             // All strings must be quoted for decoding
197             if (objectString.charAt(0) != '"') {
198                 objectString = '"' + objectString;
199             }
200             if (objectString.charAt(objectString.length() - 1) != '"') {
201                 objectString += '"';
202             }
203         }
204         return objectString;
205     }
206
207     private boolean isObjectString(final Object object) {
208         return object != null && avroSchema.getType().equals(Schema.Type.STRING);
209     }
210
211     @Override
212     public String marshal2String(final Object object) {
213         // Condition the object for Avro encoding
214         final Object conditionedObject = avroObjectMapper.mapToAvro(object);
215
216         final String jsonString = getJsonString(object, conditionedObject);
217
218         return AvroSchemaKeyTranslationUtilities.translateIllegalKeys(jsonString, true);
219     }
220
221     private String getJsonString(final Object object, final Object conditionedObject) {
222
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);
227             jsonEncoder.flush();
228             return new String(output.toByteArray());
229         } catch (final Exception e) {
230             final String returnString = getUserKey().getId() + OBJECT_TAG + object + "\" Avro marshalling failed: "
231                             + e.getMessage();
232             LOGGER.warn(returnString);
233             throw new ContextRuntimeException(returnString, e);
234         }
235     }
236
237     @Override
238     public JsonElement marshal2Object(final Object schemaObject) {
239         // Get the object as a Json string
240         final String schemaObjectAsString = marshal2String(schemaObject);
241
242         // Get a Gson instance to convert the Json string to an object created by Json
243         final Gson gson = new Gson();
244
245         // Convert the Json string into an object
246         final Object schemaObjectAsObject = gson.fromJson(schemaObjectAsString, Object.class);
247
248         return gson.toJsonTree(schemaObjectAsObject);
249     }
250
251     /**
252      * Check if we can pass this object straight through encoding or decoding, is it an object native to the schema.
253      *
254      * @param object the object to check
255      * @return true if it's a straight pass through
256      */
257     private boolean passThroughObject(final Object object) {
258         if (object == null || getSchemaClass() == null) {
259             return false;
260         }
261
262         // All strings must be mapped
263         if (object instanceof String) {
264             return false;
265         }
266
267         // Now, check if the object is native
268         return getSchemaClass().isAssignableFrom(object.getClass());
269     }
270 }