io.opencensus.implcore.tags.propagation.SerializationUtils.java Source code

Java tutorial

Introduction

Here is the source code for io.opencensus.implcore.tags.propagation.SerializationUtils.java

Source

/*
 * Copyright 2016-17, OpenCensus Authors
 *
 * 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 io.opencensus.implcore.tags.propagation;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import io.opencensus.implcore.internal.VarInt;
import io.opencensus.implcore.tags.TagMapImpl;
import io.opencensus.implcore.tags.TagValueWithMetadata;
import io.opencensus.tags.InternalUtils;
import io.opencensus.tags.Tag;
import io.opencensus.tags.TagContext;
import io.opencensus.tags.TagKey;
import io.opencensus.tags.TagMetadata;
import io.opencensus.tags.TagMetadata.TagTtl;
import io.opencensus.tags.TagValue;
import io.opencensus.tags.propagation.TagContextDeserializationException;
import io.opencensus.tags.propagation.TagContextSerializationException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * Methods for serializing and deserializing {@link TagContext}s.
 *
 * <p>The format defined in this class is shared across all implementations of OpenCensus. It allows
 * tags to propagate across requests.
 *
 * <p>OpenCensus tag context encoding:
 *
 * <ul>
 *   <li>Tags are encoded in single byte sequence. The version 0 format is:
 *   <li>{@code <version_id><encoded_tags>}
 *   <li>{@code <version_id> == a single byte, value 0}
 *   <li>{@code <encoded_tags> == (<tag_field_id><tag_encoding>)*}
 *       <ul>
 *         <li>{@code <tag_field_id>} == a single byte, value 0
 *         <li>{@code <tag_encoding>}:
 *             <ul>
 *               <li>{@code <tag_key_len><tag_key><tag_val_len><tag_val>}
 *                   <ul>
 *                     <li>{@code <tag_key_len>} == varint encoded integer
 *                     <li>{@code <tag_key>} == tag_key_len bytes comprising tag key name
 *                     <li>{@code <tag_val_len>} == varint encoded integer
 *                     <li>{@code <tag_val>} == tag_val_len bytes comprising UTF-8 string
 *                   </ul>
 *             </ul>
 *       </ul>
 * </ul>
 */
final class SerializationUtils {

    private static final TagMetadata METADATA_UNLIMITED_PROPAGATION = TagMetadata
            .create(TagTtl.UNLIMITED_PROPAGATION);

    private SerializationUtils() {
    }

    @VisibleForTesting
    static final int VERSION_ID = 0;
    @VisibleForTesting
    static final int TAG_FIELD_ID = 0;
    // This size limit only applies to the bytes representing tag keys and values.
    @VisibleForTesting
    static final int TAGCONTEXT_SERIALIZED_SIZE_LIMIT = 8192;

    // Serializes a TagContext to the on-the-wire format.
    // Encoded tags are of the form: <version_id><encoded_tags>
    static byte[] serializeBinary(TagContext tags) throws TagContextSerializationException {
        // Use a ByteArrayDataOutput to avoid needing to handle IOExceptions.
        final ByteArrayDataOutput byteArrayDataOutput = ByteStreams.newDataOutput();
        byteArrayDataOutput.write(VERSION_ID);
        int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
        for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext();) {
            Tag tag = i.next();
            if (TagTtl.NO_PROPAGATION.equals(tag.getTagMetadata().getTagTtl())) {
                continue;
            }
            totalChars += tag.getKey().getName().length();
            totalChars += tag.getValue().asString().length();
            encodeTag(tag, byteArrayDataOutput);
        }
        if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
            throw new TagContextSerializationException(
                    "Size of TagContext exceeds the maximum serialized size " + TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
        }
        return byteArrayDataOutput.toByteArray();
    }

    // Deserializes input to TagContext based on the binary format standard.
    // The encoded tags are of the form: <version_id><encoded_tags>
    static TagMapImpl deserializeBinary(byte[] bytes) throws TagContextDeserializationException {
        try {
            if (bytes.length == 0) {
                // Does not allow empty byte array.
                throw new TagContextDeserializationException("Input byte[] can not be empty.");
            }

            ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
            int versionId = buffer.get();
            if (versionId > VERSION_ID || versionId < 0) {
                throw new TagContextDeserializationException(
                        "Wrong Version ID: " + versionId + ". Currently supports version up to: " + VERSION_ID);
            }
            return new TagMapImpl(parseTags(buffer));
        } catch (BufferUnderflowException exn) {
            throw new TagContextDeserializationException(exn.toString()); // byte array format error.
        }
    }

    private static Map<TagKey, TagValueWithMetadata> parseTags(ByteBuffer buffer)
            throws TagContextDeserializationException {
        Map<TagKey, TagValueWithMetadata> tags = new HashMap<TagKey, TagValueWithMetadata>();
        int limit = buffer.limit();
        int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
        while (buffer.position() < limit) {
            int type = buffer.get();
            if (type == TAG_FIELD_ID) {
                TagKey key = createTagKey(decodeString(buffer));
                TagValue val = createTagValue(key, decodeString(buffer));
                totalChars += key.getName().length();
                totalChars += val.asString().length();
                tags.put(key, TagValueWithMetadata.create(val, METADATA_UNLIMITED_PROPAGATION));
            } else {
                // Stop parsing at the first unknown field ID, since there is no way to know its length.
                // TODO(sebright): Consider storing the rest of the byte array in the TagContext.
                break;
            }
        }
        if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
            throw new TagContextDeserializationException(
                    "Size of TagContext exceeds the maximum serialized size " + TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
        }
        return tags;
    }

    // TODO(sebright): Consider exposing a TagKey name validation method to avoid needing to catch an
    // IllegalArgumentException here.
    private static final TagKey createTagKey(String name) throws TagContextDeserializationException {
        try {
            return TagKey.create(name);
        } catch (IllegalArgumentException e) {
            throw new TagContextDeserializationException("Invalid tag key: " + name, e);
        }
    }

    // TODO(sebright): Consider exposing a TagValue validation method to avoid needing to catch
    // an IllegalArgumentException here.
    private static final TagValue createTagValue(TagKey key, String value)
            throws TagContextDeserializationException {
        try {
            return TagValue.create(value);
        } catch (IllegalArgumentException e) {
            throw new TagContextDeserializationException("Invalid tag value for key " + key + ": " + value, e);
        }
    }

    private static final void encodeTag(Tag tag, ByteArrayDataOutput byteArrayDataOutput) {
        byteArrayDataOutput.write(TAG_FIELD_ID);
        encodeString(tag.getKey().getName(), byteArrayDataOutput);
        encodeString(tag.getValue().asString(), byteArrayDataOutput);
    }

    private static final void encodeString(String input, ByteArrayDataOutput byteArrayDataOutput) {
        putVarInt(input.length(), byteArrayDataOutput);
        byteArrayDataOutput.write(input.getBytes(Charsets.UTF_8));
    }

    private static final void putVarInt(int input, ByteArrayDataOutput byteArrayDataOutput) {
        byte[] output = new byte[VarInt.varIntSize(input)];
        VarInt.putVarInt(input, output, 0);
        byteArrayDataOutput.write(output);
    }

    private static final String decodeString(ByteBuffer buffer) {
        int length = VarInt.getVarInt(buffer);
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append((char) buffer.get());
        }
        return builder.toString();
    }
}