// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
/**
* A codec that composes two codecs: a primary and a compatibility codec. It always serializes with
* the primary codec, but can make a decision on deserialization based on the first few bytes of the
* serialized format whether to use the compatibility codec. This allows for easier transition
* between storage formats as the codec remains able to read the old serialized format.
*
* @author Attila Szegedi
*
* @param <T> the type of objects this codec is for.
*/
public class CompatibilityCodec<T> implements Codec<T> {
private final Codec<T> primaryCodec;
private final Codec<T> secondaryCodec;
private final int prefixLength;
private final Predicate<byte[]> discriminator;
private CompatibilityCodec(Codec<T> primaryCodec, Codec<T> secondaryCodec, int prefixLength,
Predicate<byte[]> discriminator) {
Preconditions.checkNotNull(primaryCodec);
Preconditions.checkNotNull(secondaryCodec);
this.primaryCodec = primaryCodec;
this.secondaryCodec = secondaryCodec;
this.prefixLength = prefixLength;
this.discriminator = discriminator;
}
/**
* Creates a new compatibility codec instance.
*
* @param primaryCodec the codec used to serialize objects, as well as deserialize them when the
* first byte of the serialized format matches the discriminator.
* @param secondaryCodec the codec used to deserialize objects when the first byte of the
* serialized format does not match the discriminator.
* @param prefixLength the length, in bytes, of the prefix of the message that is inspected for
* determining the format.
* @param discriminator a predicate that will receive an array of at most prefixLength bytes
* (it can receive less if the serialized format is shorter) and has to return true
* if the primary codec should be used for deserialization, otherwise false.
*/
public static <T> CompatibilityCodec<T> create(Codec<T> primaryCodec, Codec<T> secondaryCodec,
int prefixLength, Predicate<byte[]> discriminator) {
return new CompatibilityCodec<T>(primaryCodec, secondaryCodec, prefixLength, discriminator);
}
@Override
public T deserialize(InputStream source) throws IOException {
final PushbackInputStream in = new PushbackInputStream(source, prefixLength);
final byte[] prefix = readAtMostPrefix(in);
in.unread(prefix);
return (discriminator.apply(prefix) ? primaryCodec : secondaryCodec).deserialize(in);
}
private byte[] readAtMostPrefix(InputStream in) throws IOException {
final byte[] prefix = new byte[prefixLength];
int read = 0;
do {
final int readNow = in.read(prefix, read, prefixLength - read);
if (readNow == -1) {
byte[] newprefix = new byte[read];
System.arraycopy(prefix, 0, newprefix, 0, read);
return newprefix;
}
read += readNow;
} while (read < prefixLength);
return prefix;
}
@Override
public void serialize(T item, OutputStream sink) throws IOException {
primaryCodec.serialize(item, sink);
}
}