// 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. * *

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: * *

{@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);
 * }
* *

Classifiers will typically be class names or SkyFunction names. */ public class ObjectCodecs { private static final ObjectCodec 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> stringMappedCodecs; private final Map> byteStringMappedCodecs; private final boolean allowDefaultCodec; private ObjectCodecs(Map> 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. * *

{@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 void doSerialize( String classifier, ObjectCodec 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> codecsBuilder = ImmutableMap.builder(); private boolean allowDefaultCodec = true; private Builder() {} /** * Add custom serialization strategy ({@code codec}) for {@code classifier}. * *

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 ClassKeyedBuilder add(Class clazz, ObjectCodec 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> makeByteStringMappedCodecs( Map> stringMappedCodecs) { ImmutableMap.Builder> result = ImmutableMap.builder(); for (Entry> entry : stringMappedCodecs.entrySet()) { result.put(ByteString.copyFromUtf8(entry.getKey()), entry.getValue()); } return result.build(); } }