/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hortonworks.registries.schemaregistry.serdes.avro;
import com.hortonworks.registries.schemaregistry.SchemaMetadata;
import com.hortonworks.registries.schemaregistry.SchemaVersionKey;
import com.hortonworks.registries.schemaregistry.avro.AvroSchemaResolver;
import com.hortonworks.registries.schemaregistry.errors.InvalidSchemaException;
import com.hortonworks.registries.schemaregistry.errors.SchemaNotFoundException;
import com.hortonworks.registries.schemaregistry.serde.AbstractSnapshotDeserializer;
import com.hortonworks.registries.schemaregistry.serde.SerDesException;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.specific.SpecificDatumReader;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
/**
* This class implements most of the required functionality for an avro deserializer by extending {@link AbstractSnapshotDeserializer}
* and implementing the required methods.
*
* <p>
* The below example describes how to extend this deserializer with user supplied representation like MessageContext.
* Default deserialization of avro payload is implemented in {@link #buildDeserializedObject(InputStream, SchemaMetadata, Integer, Integer)}
* and it can be used while implementing {@link #doDeserialize(Object, byte, SchemaMetadata, Integer, Integer)} as given
* below.
* </p>
*
* <pre>{@code
public class MessageContext {
final Map<String, Object> headers;
final InputStream payloadEntity;
public MessageContext(Map<String, Object> headers, InputStream payloadEntity) {
this.headers = headers;
this.payloadEntity = payloadEntity;
}
}
public class MessageContextBasedDeserializer extends AbstractAvroSnapshotDeserializer<MessageContext> {
{@literal @}Override
protected Object doDeserialize(MessageContext input,
byte protocolId,
SchemaMetadata schemaMetadata,
Integer writerSchemaVersion,
Integer readerSchemaVersion) throws SerDesException {
return buildDeserializedObject(input.payloadEntity,
schemaMetadata,
writerSchemaVersion,
readerSchemaVersion);
}
{@literal @}Override
protected byte retrieveProtocolId(MessageContext input) throws SerDesException {
return (byte) input.headers.get("protocol.id");
}
{@literal @}Override
protected SchemaIdVersion retrieveSchemaIdVersion(byte protocolId, MessageContext input) throws SerDesException {
Long id = (Long) input.headers.get("schema.metadata.id");
Integer version = (Integer) input.headers.get("schema.version");
return new SchemaIdVersion(id, version);
}
}
}</pre>
*
* @param <I> representation of the received input payload
*/
public abstract class AbstractAvroSnapshotDeserializer<I> extends AbstractSnapshotDeserializer<I, Object, Schema> {
private static final Logger LOG = LoggerFactory.getLogger(AbstractAvroSnapshotDeserializer.class);
private AvroSchemaResolver avroSchemaResolver;
@Override
public void init(Map<String, ?> config) {
super.init(config);
avroSchemaResolver = new AvroSchemaResolver(key -> schemaRegistryClient.getSchemaVersionInfo(key));
}
@Override
protected Schema getParsedSchema(SchemaVersionKey schemaVersionKey) throws InvalidSchemaException, SchemaNotFoundException {
return new Schema.Parser().parse(avroSchemaResolver.resolveSchema(schemaVersionKey));
}
/**
* Builds the deserialized object from the given {@code payloadInputStream} and applying writer and reader schemas
* from the respective given versions.
*
* @param payloadInputStream payload
* @param schemaMetadata metadata about schema
* @param writerSchemaVersion schema version of the writer
* @param readerSchemaVersion schema version to be applied for reading or projection
* @return
* @throws SerDesException when any ser/des error occurs
*/
protected Object buildDeserializedObject(InputStream payloadInputStream,
SchemaMetadata schemaMetadata,
Integer writerSchemaVersion,
Integer readerSchemaVersion) throws SerDesException {
Object deserializedObj;
String schemaName = schemaMetadata.getName();
SchemaVersionKey writerSchemaVersionKey = new SchemaVersionKey(schemaName, writerSchemaVersion);
LOG.debug("SchemaKey: [{}] for the received payload", writerSchemaVersionKey);
try {
Schema writerSchema = getSchema(writerSchemaVersionKey);
if (writerSchema == null) {
throw new SerDesException("No schema exists with metadata-key: " + schemaMetadata + " and writerSchemaVersion: " + writerSchemaVersion);
}
Schema.Type writerSchemaType = writerSchema.getType();
if (Schema.Type.BYTES.equals(writerSchemaType)) {
// serializer writes byte array directly without going through avro encoder layers.
deserializedObj = IOUtils.toByteArray(payloadInputStream);
} else if (Schema.Type.STRING.equals(writerSchemaType)) {
// generate UTF-8 string object from the received bytes.
deserializedObj = new String(IOUtils.toByteArray(payloadInputStream), AvroUtils.UTF_8);
} else {
int recordType = payloadInputStream.read();
LOG.debug("Received record type: [{}]", recordType);
GenericDatumReader datumReader = null;
Schema readerSchema = readerSchemaVersion != null
? getSchema(new SchemaVersionKey(schemaName, readerSchemaVersion)) : null;
if (recordType == AvroUtils.GENERIC_RECORD) {
datumReader = readerSchema != null ? new GenericDatumReader(writerSchema, readerSchema)
: new GenericDatumReader(writerSchema);
} else {
datumReader = readerSchema != null ? new SpecificDatumReader(writerSchema, readerSchema)
: new SpecificDatumReader(writerSchema);
}
deserializedObj = datumReader.read(null, DecoderFactory.get().binaryDecoder(payloadInputStream, null));
}
} catch (IOException e) {
throw new SerDesException(e);
}
return deserializedObj;
}
}