/** * 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 org.apache.hadoop.io; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public abstract class CompatibleWritable implements Writable { /** Initial version number */ public static final int INITIAL_VERSION = 0; /** Default size of buffer which we use as a temporary storage for serialization */ private static final int SERIALIZER_BUFFER_SIZE = 1024; /** * Encoded length of a message if it's initial version. For initial version we don't need to * write actual length, since the reader cannot be older than initial version. */ private static final int INITIAL_VERSION_LENGTH = -1; private static final ThreadLocal<byte[]> serializerBuffers = new ThreadLocal<byte[]>() { @Override protected byte[] initialValue() { return new byte[SERIALIZER_BUFFER_SIZE]; } }; /** * Returns current version of an object, initial version is {@link #INITIAL_VERSION} (this is * important for optimizations). If we implement new, extended object (with new fields), * we have to increment version (that means add EXACTLY one). */ protected int getVersion() { return INITIAL_VERSION; } /** * Returns good estimate (in the perfect scenario a tight upper bound) on the size of * serialized structure. Default implementation ensures that default sized buffer can be * retrieved from cache. */ protected int getSizeEstimate() { return SERIALIZER_BUFFER_SIZE; } /** * Returns temporary buffer for serialization, note that if your {@link CompatibleWritable} * contains another {@link CompatibleWritable} you have to ensure that returned buffers are * unique. Default implementation is safe (maintains above property). */ protected byte[] getBuffer(int size) { byte[] buffer; if (size > SERIALIZER_BUFFER_SIZE) { buffer = new byte[size]; } else { buffer = serializerBuffers.get(); serializerBuffers.remove(); } return buffer; } /** Frees acquired buffer. */ protected void freeBuffer(byte[] buffer) { if (buffer.length <= SERIALIZER_BUFFER_SIZE) { serializerBuffers.set(buffer); } } /** * Actual implementation of {@link Writable#write(DataOutput)} for this type. * Must call super#writeCompatible(DataOutputBuffer). */ protected abstract void writeCompatible(DataOutput out) throws IOException; /** * Actual implementation of {@link Writable#readFields(DataInput)} for this type, * must check if {@link DataInputBuffer#canReadNextVersion()} before reading to ensure * compatibility. * Must call super#readCompatible(DataInputBuffer). */ protected abstract void readCompatible(CompatibleDataInput in) throws IOException; @Override public final void write(DataOutput realOut) throws IOException { final int localVersion = getVersion(); WritableUtils.writeVInt(realOut, localVersion); if (localVersion == INITIAL_VERSION) { WritableUtils.writeVInt(realOut, INITIAL_VERSION_LENGTH); writeCompatible(realOut); } else { final byte[] buffer = getBuffer(getSizeEstimate()); try { DataOutputBuffer out = new DataOutputBuffer(buffer); // Write object writeCompatible(out); WritableUtils.writeVInt(realOut, out.getLength()); realOut.write(out.getData(), 0, out.getLength()); } finally { freeBuffer(buffer); } } } @Override public final void readFields(DataInput realIn) throws IOException { final int localVersion = getVersion(); final int remoteVersion = WritableUtils.readVInt(realIn); if (remoteVersion <= localVersion) { // Discard length WritableUtils.readVInt(realIn); CompatibleDataInput in = new CompatibleDataInputWrapper(realIn, remoteVersion); readCompatible(in); } else { final int length = WritableUtils.readVInt(realIn); final byte[] buffer = getBuffer(length); try { realIn.readFully(buffer, 0, length); DataInputBuffer in = new DataInputBuffer(); in.reset(buffer, length); // Read object readCompatible(in); } finally { freeBuffer(buffer); } } } /** * Provides calls to read data and check for possible compatibility problems. * Used to wrap {@link DataInput} in the case when the reader version is newer than the writer * version for object deserialization. This class provides information when the deserialization * needs to be stopped in such case, and the default values need to be assigned for the fields * that could not be deserialized. */ public static interface CompatibleDataInput extends DataInput { /** * This function needs to be used when the reader is newer that the writer. * The reader reads fields, and for every version increment that occurred between the writer * and the reader, the reader needs to call this function to see if there is more data * available to be deserialized. * Please see {@link TestCompatibleWritable} for example. */ boolean canReadNextVersion(); } private static class CompatibleDataInputWrapper implements CompatibleDataInput { private final DataInput input; private final int serializedObjectVersion; private int currentVersion; public CompatibleDataInputWrapper(DataInput input, int serializedObjectVersion) { this.input = input; this.serializedObjectVersion = serializedObjectVersion; this.currentVersion = INITIAL_VERSION; } @Override public boolean canReadNextVersion() { if (currentVersion < serializedObjectVersion) { currentVersion++; return true; } else { return false; } } //////////////////////////////////////// // DataInput //////////////////////////////////////// @Override public void readFully(byte[] bytes) throws IOException { input.readFully(bytes); } @Override public void readFully(byte[] bytes, int i, int i2) throws IOException { input.readFully(bytes, i, i2); } @Override public int skipBytes(int i) throws IOException { return input.skipBytes(i); } @Override public boolean readBoolean() throws IOException { return input.readBoolean(); } @Override public byte readByte() throws IOException { return input.readByte(); } @Override public int readUnsignedByte() throws IOException { return input.readUnsignedByte(); } @Override public short readShort() throws IOException { return input.readShort(); } @Override public int readUnsignedShort() throws IOException { return input.readUnsignedShort(); } @Override public char readChar() throws IOException { return input.readChar(); } @Override public int readInt() throws IOException { return input.readInt(); } @Override public long readLong() throws IOException { return input.readLong(); } @Override public float readFloat() throws IOException { return input.readFloat(); } @Override public double readDouble() throws IOException { return input.readDouble(); } @Override public String readLine() throws IOException { return input.readLine(); } @Override public String readUTF() throws IOException { return input.readUTF(); } } }