/**
* Copyright 2016 Hortonworks.
* <p>
* Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.serde;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.hortonworks.registries.schemaregistry.SchemaIdVersion;
import com.hortonworks.registries.schemaregistry.SchemaMetadata;
import com.hortonworks.registries.schemaregistry.SchemaVersionKey;
import com.hortonworks.registries.schemaregistry.client.SchemaRegistryClient;
import com.hortonworks.registries.schemaregistry.errors.InvalidSchemaException;
import com.hortonworks.registries.schemaregistry.errors.SchemaNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* This class implements {@link SnapshotDeserializer} and provides common functionality like
* <ul>
* <li>connecting to given schema registry with schema registry client(internally provides caching for multiple types)</li>
* <li>caching of parsed schemas</li>
* <li>providing life cycle to deserialization as mentioned below.</li>
* </ul>
*
* Deserialization involves
* <ol>
* <li>getting ser/des protocol id by implementing {@link #retrieveProtocolId(Object)} </li>
* <li>getting schema version id by implementing {@link #retrieveSchemaIdVersion(byte, Object)} </li>
* <li>implement the actual deserialization with {@link #doDeserialize(Object, byte, SchemaMetadata, Integer, Integer)} </li>
* </ol>
*
* Extensions to this class may need to implement the above life cycle methods.
*
* @param <I> representation of the received input payload
* @param <O> deserialized representation of the received payload
* @param <S> parsed schema representation to be stored in local cache
*/
public abstract class AbstractSnapshotDeserializer<I, O, S> implements SnapshotDeserializer<I, O, Integer> {
private static final Logger LOG = LoggerFactory.getLogger(AbstractSnapshotDeserializer.class);
/**
* Maximum inmemory cache size maintained in deserializer instance.
*/
public static final String DESERIALIZER_SCHEMA_CACHE_MAX_SIZE = "schemaregistry.deserializer.schema.cache.size";
/**
* Default schema cache max size.
*/
public static final Integer DEFAULT_SCHEMA_CACHE_SIZE = 1024;
/**
* Expiry interval(in milli seconds) after an access for an entry in schema cache
*/
public static final String DESERIALIZER_SCHEMA_CACHE_EXPIRY_IN_SECS = "schemaregistry.deserializer.schema.cache.expiry.secs";
/**
* Default schema cache entry access expiration interval
*/
public static final Long DEFAULT_DESERIALIZER_SCHEMA_CACHE_EXPIRY_IN_SECS = 60 * 5L;
private LoadingCache<SchemaVersionKey, S> schemaCache;
protected SchemaRegistryClient schemaRegistryClient;
@Override
public void init(Map<String, ?> config) {
LOG.debug("Initialized with config: [{}]", config);
schemaRegistryClient = new SchemaRegistryClient(config);
schemaCache = CacheBuilder.newBuilder()
.maximumSize(getCacheMaxSize(config))
.expireAfterAccess(getCacheExpiryInMillis(config), TimeUnit.MILLISECONDS)
.build(new CacheLoader<SchemaVersionKey, S>() {
@Override
public S load(SchemaVersionKey schemaVersionKey) throws Exception {
return getParsedSchema(schemaVersionKey);
}
});
}
private Long getCacheExpiryInMillis(Map<String, ?> config) {
Long value = (Long) getValue(config, DESERIALIZER_SCHEMA_CACHE_EXPIRY_IN_SECS, DEFAULT_DESERIALIZER_SCHEMA_CACHE_EXPIRY_IN_SECS);
if (value < 0) {
throw new IllegalArgumentException("Property: " + DESERIALIZER_SCHEMA_CACHE_EXPIRY_IN_SECS + "must be non negative.");
}
return value;
}
private Integer getCacheMaxSize(Map<String, ?> config) {
Integer value = (Integer) getValue(config, DESERIALIZER_SCHEMA_CACHE_MAX_SIZE, DEFAULT_SCHEMA_CACHE_SIZE);
if (value < 0) {
throw new IllegalArgumentException("Property: " + DESERIALIZER_SCHEMA_CACHE_MAX_SIZE + "must be non negative.");
}
return value;
}
private Object getValue(Map<String, ?> config, String key, Object defaultValue) {
Object value = config.get(key);
if (value == null) {
value = defaultValue;
}
return value;
}
/**
* Returns the parsed schema representation of the schema associated with the given {@code schemaVersionInfo}
* @param schemaVersionInfo
* @throws InvalidSchemaException when the associated schema is not valid.
* @throws SchemaNotFoundException when there is no schema for the given {@code schemaVersionInfo}
*/
protected abstract S getParsedSchema(SchemaVersionKey schemaVersionInfo) throws InvalidSchemaException, SchemaNotFoundException;
@Override
public O deserialize(I input,
Integer readerSchemaVersion) throws SerDesException {
// it can be enhanced to have respective protocol handlers for different versions
byte protocolId = retrieveProtocolId(input);
SchemaIdVersion schemaIdVersion = retrieveSchemaIdVersion(protocolId, input);
SchemaMetadata schemaMetadata = schemaRegistryClient.getSchemaMetadataInfo(schemaIdVersion.getSchemaMetadataId()).getSchemaMetadata();
return doDeserialize(input, protocolId, schemaMetadata, schemaIdVersion.getVersion(), readerSchemaVersion);
}
/**
* Returns the deserialized object for the given input according to the schema information provided.
*
* @param input payload to be deserialized into.
* @param protocolId protocol id for deserializtion.
* @param schemaMetadata metadata about the schema
* @param writerSchemaVersion schema version of writer used in building the serialized payload.
* @param readerSchemaVersion schema version for reading/projection.
* @throws SerDesException when any ser/des error occurs
*/
protected abstract O doDeserialize(I input,
byte protocolId,
SchemaMetadata schemaMetadata,
Integer writerSchemaVersion,
Integer readerSchemaVersion) throws SerDesException;
/**
* Returns protocol id of ser/des.
* @param input input from which protocol id can be retrieved
* @throws SerDesException when any ser/des error occurs
*/
protected abstract byte retrieveProtocolId(I input) throws SerDesException;
/**
* Retrieve the writer schema version and id from the given {@code input} for the given {@code protocolId}
*
* @param protocolId protocol id
* @param input input from which version and id are to be retrieved
* @throws SerDesException when any ser/des error occurs
*/
protected abstract SchemaIdVersion retrieveSchemaIdVersion(byte protocolId, I input) throws SerDesException;
/**
* Returns Schema for the given {@code schemaVersionKey} from loadable cache.
* @param schemaVersionKey schema version key
*/
protected S getSchema(SchemaVersionKey schemaVersionKey) {
try {
return schemaCache.get(schemaVersionKey);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public void close() throws Exception {
schemaRegistryClient.close();
}
}