blob: 39873325d291b4fe91708aeb2eb0f35f471f57d5 [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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
//
// 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.google.devtools.build.lib.skyframe.serialization;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
/**
* Wrapper for the minutiae of serializing and deserializing objects using {@link ObjectCodec}s,
* serving as a layer between the streaming-oriented {@link ObjectCodec} interface and users.
* Handles the mapping and selection of custom serialization implementations, falling back on less
* performant java serialization by default when no better option is available and it is allowed by
* the configuration.
*
* <p>To use, create a {@link ObjectCodecs.Builder} and add custom classifier to {@link ObjectCodec}
* mappings using {@link ObjectCodecs.Builder#add} directly or by using one of the convenience
* builders returned by {@link ObjectCodecs.Builder#asSkyFunctionNameKeyedBuilder()} or
* {@link ObjectCodecs.Builder#asClassKeyedBuilder()}. The provided mappings are then used to
* determine serialization/deserialization logic. For example:
*
* <pre>{@code
* // Create an instance for which anything identified as "foo" will use FooCodec.
* ObjectCodecs objectCodecs = ObjectCodecs.newBuilder()
* .add("foo", new FooCodec())
* .build();
*
* // This will use the custom supplied FooCodec to serialize obj:
* ByteString serialized = objectCodecs.serialize("foo", obj);
* Object deserialized = objectCodecs.deserialize(ByteString.copyFromUtf8("foo"), serialized);
*
* // This will use default java object serialization to serialize obj:
* ByteString serialized = objectCodecs.serialize("bar", obj);
* Object deserialized = objectCodecs.deserialize(ByteString.copyFromUtf8("bar"), serialized);
* }</pre>
*
* <p>Classifiers will typically be class names or SkyFunction names.
*/
public class ObjectCodecs {
private static final ObjectCodec<Object> DEFAULT_CODEC = new JavaSerializableCodec();
/** Create new ObjectCodecs.Builder, the preferred instantiation method. */
// TODO(janakr,michajlo): Specialize builders into ones keyed by class (even if the class isn't
// the one specified by the codec) and ones keyed by string, and expose a getClassifier() method
// for ObjectCodecs keyed by class.
public static ObjectCodecs.Builder newBuilder() {
return new Builder();
}
private final Map<String, ObjectCodec<?>> stringMappedCodecs;
private final Map<ByteString, ObjectCodec<?>> byteStringMappedCodecs;
private final boolean allowDefaultCodec;
private ObjectCodecs(Map<String, ObjectCodec<?>> codecs, boolean allowDefaultCodec) {
this.stringMappedCodecs = codecs;
this.byteStringMappedCodecs = makeByteStringMappedCodecs(codecs);
this.allowDefaultCodec = allowDefaultCodec;
}
/**
* Serialize {@code subject}, using the serialization strategy determined by {@code classifier},
* returning a {@link ByteString} containing the serialized representation.
*/
public ByteString serialize(String classifier, Object subject) throws SerializationException {
ByteString.Output resultOut = ByteString.newOutput();
CodedOutputStream codedOut = CodedOutputStream.newInstance(resultOut);
ObjectCodec<?> codec = getCodec(classifier);
try {
doSerialize(classifier, codec, subject, codedOut);
codedOut.flush();
return resultOut.toByteString();
} catch (IOException e) {
throw new SerializationException(
"Failed to serialize " + subject + " using " + codec + " for " + classifier, e);
}
}
/**
* Similar to {@link #serialize(String, Object)}, except allows the caller to specify a {@link
* CodedOutputStream} to serialize {@code subject} to. Has less object overhead than {@link
* #serialize(String, Object)} and as such is preferrable when serializing objects in bulk.
*
* <p>{@code codedOut} is not flushed by this method.
*/
public void serialize(String classifier, Object subject, CodedOutputStream codedOut)
throws SerializationException {
ObjectCodec<?> codec = getCodec(classifier);
try {
doSerialize(classifier, codec, subject, codedOut);
} catch (IOException e) {
throw new SerializationException(
"Failed to serialize " + subject + " using " + codec + " for " + classifier, e);
}
}
/**
* Deserialize {@code data} using the serialization strategy determined by {@code classifier}.
* {@code classifier} should be the utf-8 encoded {@link ByteString} representation of the {@link
* String} classifier used to serialize {@code data}. This is preferred since callers typically
* have parsed {@code classifier} from a protocol buffer, for which {@link ByteString}s are
* cheaper to use.
*/
public Object deserialize(ByteString classifier, ByteString data) throws SerializationException {
return deserialize(classifier, data.newCodedInput());
}
/**
* Similar to {@link #deserialize(ByteString, ByteString)}, except allows the caller to specify a
* {@link CodedInputStream} to deserialize data from. This is useful for decoding objects
* serialized in bulk by {@link #serialize(String, Object, CodedOutputStream)}.
*/
public Object deserialize(ByteString classifier, CodedInputStream codedIn)
throws SerializationException {
ObjectCodec<?> codec = getCodec(classifier);
// If safe, this will allow CodedInputStream to return a direct view of the underlying bytes
// in some situations, bypassing a copy.
codedIn.enableAliasing(true);
try {
Object result = codec.deserialize(codedIn);
if (result == null) {
throw new NullPointerException(
"ObjectCodec " + codec + " for " + classifier.toStringUtf8() + " returned null");
}
return result;
} catch (IOException e) {
throw new SerializationException(
"Failed to deserialize data using " + codec + " for " + classifier.toStringUtf8(), e);
}
}
private ObjectCodec<?> getCodec(String classifier)
throws SerializationException.NoCodecException {
ObjectCodec<?> result = stringMappedCodecs.get(classifier);
if (result != null) {
return result;
} else if (allowDefaultCodec) {
return DEFAULT_CODEC;
} else {
throw new SerializationException.NoCodecException(
"No codec available for " + classifier + " and default fallback disabled");
}
}
private ObjectCodec<?> getCodec(ByteString classifier) throws SerializationException {
ObjectCodec<?> result = byteStringMappedCodecs.get(classifier);
if (result != null) {
return result;
} else if (allowDefaultCodec) {
return DEFAULT_CODEC;
} else {
throw new SerializationException(
"No codec available for " + classifier.toStringUtf8() + " and default fallback disabled");
}
}
private static <T> void doSerialize(
String classifier, ObjectCodec<T> codec, Object subject, CodedOutputStream codedOut)
throws SerializationException, IOException {
try {
codec.serialize(codec.getEncodedClass().cast(subject), codedOut);
} catch (ClassCastException e) {
throw new SerializationException(
"Codec "
+ codec
+ " for "
+ classifier
+ " is incompatible with "
+ subject
+ " (of type "
+ subject.getClass().getName()
+ ")",
e);
}
}
/** Builder for {@link ObjectCodecs}. */
static class Builder {
private final ImmutableMap.Builder<String, ObjectCodec<?>> codecsBuilder =
ImmutableMap.builder();
private boolean allowDefaultCodec = true;
private Builder() {}
/**
* Add custom serialization strategy ({@code codec}) for {@code classifier}.
*
* <p>Intended for package-internal usage only. Consider using the specialized build types
* returned by {@link #asClassKeyedBuilder()} or {@link #asSkyFunctionNameKeyedBuilder()}
* before using this method.
*/
Builder add(String classifier, ObjectCodec<?> codec) {
codecsBuilder.put(classifier, codec);
return this;
}
/** Set whether or not we allow fallback to the default codec, java serialization. */
public Builder setAllowDefaultCodec(boolean allowDefaultCodec) {
this.allowDefaultCodec = allowDefaultCodec;
return this;
}
/** Wrap this builder with a {@link ClassKeyedBuilder}. */
public ClassKeyedBuilder asClassKeyedBuilder() {
return new ClassKeyedBuilder(this);
}
/** Wrap this builder with a {@link SkyFunctionNameKeyedBuilder}. */
public SkyFunctionNameKeyedBuilder asSkyFunctionNameKeyedBuilder() {
return new SkyFunctionNameKeyedBuilder(this);
}
public ObjectCodecs build() {
return new ObjectCodecs(codecsBuilder.build(), allowDefaultCodec);
}
}
/** Convenience builder for adding codecs classified by class name. */
static class ClassKeyedBuilder {
private final Builder underlying;
private ClassKeyedBuilder(Builder underlying) {
this.underlying = underlying;
}
public <T> ClassKeyedBuilder add(Class<? extends T> clazz, ObjectCodec<T> codec) {
underlying.add(clazz.getName(), codec);
return this;
}
public ObjectCodecs build() {
return underlying.build();
}
}
/** Convenience builder for adding codecs classified by SkyFunctionName. */
static class SkyFunctionNameKeyedBuilder {
private final Builder underlying;
private SkyFunctionNameKeyedBuilder(Builder underlying) {
this.underlying = underlying;
}
public SkyFunctionNameKeyedBuilder add(SkyFunctionName skyFuncName, ObjectCodec<?> codec) {
underlying.add(skyFuncName.getName(), codec);
return this;
}
public ObjectCodecs build() {
return underlying.build();
}
}
private static Map<ByteString, ObjectCodec<?>> makeByteStringMappedCodecs(
Map<String, ObjectCodec<?>> stringMappedCodecs) {
ImmutableMap.Builder<ByteString, ObjectCodec<?>> result = ImmutableMap.builder();
for (Entry<String, ObjectCodec<?>> entry : stringMappedCodecs.entrySet()) {
result.put(ByteString.copyFromUtf8(entry.getKey()), entry.getValue());
}
return result.build();
}
}