com.couchbase.client.java.transcoder.TranscoderUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.couchbase.client.java.transcoder.TranscoderUtils.java

Source

/*
 * Copyright (c) 2016 Couchbase, Inc.
 *
 * 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.couchbase.client.java.transcoder;

import com.couchbase.client.core.endpoint.util.WhitespaceSkipper;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.ResponseStatus;
import com.couchbase.client.deps.com.fasterxml.jackson.databind.ObjectMapper;
import com.couchbase.client.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.deps.io.netty.buffer.ByteBufUtil;
import com.couchbase.client.deps.io.netty.buffer.Unpooled;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;

import java.io.*;
import java.util.List;
import java.util.Map;

/**
 * Helper methods and flags for the shipped {@link Transcoder}s.
 *
 * @author Michael Nitschinger
 * @author Simon Basl
 * @since 2.0
 */
public class TranscoderUtils {

    private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(TranscoderUtils.class);

    /**
     * 32bit flag is composed of:
     *  - 3 compression bits
     *  - 1 bit reserved for future use
     *  - 4 format flags bits. those 8 upper bits make up the common flags
     *  - 8 bits reserved for future use
     *  - 16 bits for legacy flags
     *
     * This mask allows to compare a 32 bits flags with the 4 common flag format bits
     * ("00001111 00000000 00000000 00000000").
     *
     * @see #extractCommonFlags(int)
     * @see #hasCommonFlags(int)
     * @see #hasCompressionFlags(int)
     */
    public static final int COMMON_FORMAT_MASK = 0x0F000000;

    public static final int PRIVATE_COMMON_FLAGS = createCommonFlags(CommonFlags.PRIVATE.ordinal());
    public static final int JSON_COMMON_FLAGS = createCommonFlags(CommonFlags.JSON.ordinal());
    public static final int BINARY_COMMON_FLAGS = createCommonFlags(CommonFlags.BINARY.ordinal());
    public static final int STRING_COMMON_FLAGS = createCommonFlags(CommonFlags.STRING.ordinal());

    public static final int SERIALIZED_LEGACY_FLAGS = 1;
    public static final int BINARY_LEGACY_FLAGS = (8 << 8);
    public static final int STRING_LEGACY_FLAGS = 0;
    public static final int JSON_LEGACY_FLAGS = STRING_LEGACY_FLAGS;
    public static final int BOOLEAN_LEGACY_FLAGS = STRING_LEGACY_FLAGS;
    public static final int LONG_LEGACY_FLAGS = STRING_LEGACY_FLAGS;
    public static final int DOUBLE_LEGACY_FLAGS = STRING_LEGACY_FLAGS;

    public static final int SERIALIZED_COMPAT_FLAGS = PRIVATE_COMMON_FLAGS | SERIALIZED_LEGACY_FLAGS;
    public static final int JSON_COMPAT_FLAGS = JSON_COMMON_FLAGS | JSON_LEGACY_FLAGS;
    public static final int BINARY_COMPAT_FLAGS = BINARY_COMMON_FLAGS | BINARY_LEGACY_FLAGS;
    public static final int BOOLEAN_COMPAT_FLAGS = JSON_COMMON_FLAGS | BOOLEAN_LEGACY_FLAGS;
    public static final int LONG_COMPAT_FLAGS = JSON_COMMON_FLAGS | LONG_LEGACY_FLAGS;
    public static final int DOUBLE_COMPAT_FLAGS = JSON_COMMON_FLAGS | DOUBLE_LEGACY_FLAGS;
    public static final int STRING_COMPAT_FLAGS = STRING_COMMON_FLAGS | STRING_LEGACY_FLAGS;

    private TranscoderUtils() {
    }

    /**
     * Checks whether the upper 8 bits are set, indicating common flags presence.
     *
     * It does this by shifting bits to the right until only the most significant
     * bits are remaining and then checks if one of them is set.
     *
     * @param flags the flags to check.
     * @return true if set, false otherwise.
     */
    public static boolean hasCommonFlags(final int flags) {
        return (flags >> 24) > 0;
    }

    /**
     * Checks whether the upper 3 bits are set, indicating compression presence.
     *
     * It does this by shifting bits to the right until only the most significant
     * bits are remaining and then checks if one of them is set.
     *
     * @param flags the flags to check.
     * @return true if compression set, false otherwise.
     */
    public static boolean hasCompressionFlags(final int flags) {
        return (flags >> 29) > 0;
    }

    /**
     * Checks that flags has common flags bits set and that they correspond to expected common flags format.
     *
     * @param flags the 32 bits flags to check
     * @param expectedCommonFlag the expected common flags format bits
     * @return true if common flags bits are set and correspond to expectedCommonFlag format
     */
    public static boolean hasCommonFormat(final int flags, final int expectedCommonFlag) {
        return hasCommonFlags(flags) && (flags & COMMON_FORMAT_MASK) == expectedCommonFlag;
    }

    /**
     * Returns only the common flags from the full flags.
     *
     * @param flags the flags to check.
     * @return only the common flags simple representation (8 bits).
     */
    public static int extractCommonFlags(final int flags) {
        return flags >> 24;
    }

    /**
     * Takes a integer representation of flags and moves them to the common flags MSBs.
     *
     * @param flags the flags to shift.
     * @return an integer having the common flags set.
     */
    public static int createCommonFlags(final int flags) {
        return flags << 24;
    }

    /**
     * Utility method to correctly check a flag has a certain type, by checking
     * that either the corresponding flags are set in the common flags bits or
     * the flag is a legacy flag of the correct type.
     *
     * @param flags the flags to be checked.
     * @param expectedCommonFlags the common flags for the expected type
     * @param expectedLegacyFlag the legacy flags for the expected type
     * @return true if flags conform to the correct common flags or legacy flags
     */
    private static boolean hasFlags(final int flags, final int expectedCommonFlags, final int expectedLegacyFlag) {
        return hasCommonFormat(flags, expectedCommonFlags) || flags == expectedLegacyFlag;
    }

    /**
     * Checks if the flags identify a JSON document.
     *
     * This method is strict if it finds common flags set, and if not falls back
     * to a check of legacy JSON string (identified by 0 flags and no compression).
     *
     * @param flags the flags to check.
     * @return true if JSON, false otherwise.
     */
    public static boolean hasJsonFlags(final int flags) {
        return hasFlags(flags, JSON_COMMON_FLAGS, JSON_LEGACY_FLAGS);
    }

    /**
     * Checks if the flags identify a String document.
     *
     * This method is strict if it finds common flags set, and if not falls back
     * to a check of legacy String (identified by 0 flags and no compression).
     *
     * @param flags the flags to check.
     * @return true if String, false otherwise.
     */
    public static boolean hasStringFlags(final int flags) {
        return hasFlags(flags, STRING_COMMON_FLAGS, STRING_LEGACY_FLAGS);
    }

    /**
     * Checks if the flags identify a serialized document.
     *
     * @param flags the flags to check.
     * @return true if serializable, false otherwise.
     */
    public static boolean hasSerializableFlags(final int flags) {
        return hasFlags(flags, PRIVATE_COMMON_FLAGS, SERIALIZED_LEGACY_FLAGS);
    }

    /**
     * Checks if the flags identify a binary document.
     *
     * @param flags the flags to check.
     * @return true if binary, false otherwise.
     */
    public static boolean hasBinaryFlags(final int flags) {
        return hasFlags(flags, BINARY_COMMON_FLAGS, BINARY_LEGACY_FLAGS);
    }

    /**
     * Takes the input content and deserializes it.
     *
     * @param content the content to deserialize.
     * @return the serializable object.
     * @throws Exception if something goes wrong during deserialization.
     */
    public static Serializable deserialize(final ByteBuf content) throws Exception {
        byte[] serialized = new byte[content.readableBytes()];
        content.getBytes(0, serialized);
        ByteArrayInputStream bis = new ByteArrayInputStream(serialized);
        ObjectInputStream is = new ObjectInputStream(bis);
        Serializable deserialized = (Serializable) is.readObject();
        is.close();
        bis.close();
        return deserialized;
    }

    /**
     * Serializes the input into a ByteBuf.
     *
     * @param serializable the object to serialize.
     * @return the serialized object.
     * @throws Exception if something goes wrong during serialization.
     */
    public static ByteBuf serialize(final Serializable serializable) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ;
        ObjectOutputStream os = new ObjectOutputStream(bos);
        os.writeObject(serializable);

        byte[] serialized = bos.toByteArray();
        os.close();
        bos.close();
        return Unpooled.buffer().writeBytes(serialized);
    }

    /**
     * Helper method to encode a String into UTF8 via fast-path methods.
     *
     * @param source the source document.
     * @return the encoded byte buffer.
     */
    public static ByteBuf encodeStringAsUtf8(String source) {
        ByteBuf target = Unpooled.buffer(source.length());
        ByteBufUtil.writeUtf8(target, source);
        return target;
    }

    /**
     * A class that holds information from a {@link ByteBuf} that allows to
     * read its corresponding byte array. Offset and length are needed in case
     * the ByteBuf is directly backed by a byte[] but the size of the byte isn't
     * representative of the actual size of the current content.
     */
    public static class ByteBufToArray {
        public final byte[] byteArray;
        public final int offset;
        public final int length;

        public ByteBufToArray(byte[] byteArray, int offset, int length) {
            this.byteArray = byteArray;
            this.offset = offset;
            this.length = length;
        }
    }

    /**
     * Converts a {@link ByteBuf} to a byte[] in the most straightforward manner available.
     * @param input the ByteBuf to convert.
     * @return a {@link ByteBufToArray} containing the byte[]array, as well as the offset and length to use (in case
     * the actual array is longer than the data the ByteBuf represents for instance).
     */
    public static ByteBufToArray byteBufToByteArray(ByteBuf input) {
        byte[] inputBytes;
        int offset = 0;
        int length = input.readableBytes();
        if (input.hasArray()) {
            inputBytes = input.array();
            offset = input.arrayOffset() + input.readerIndex();
        } else {
            inputBytes = new byte[length];
            input.getBytes(input.readerIndex(), inputBytes);
        }
        return new ByteBufToArray(inputBytes, offset, length);
    }

    /**
     * Decode a {@link ByteBuf} representing a valid JSON entity to the requested target class,
     * using the {@link ObjectMapper} provided and without releasing the buffer.
     *
     * Mapper uses the byte[], performing the most straightforward conversion from ByteBuf to byte[] available. (see
     * {@link #byteBufToByteArray(ByteBuf)}).
     *
     * @param input the ByteBuf to decode.
     * @param clazz the class to decode to.
     * @param mapper the mapper to use for decoding.
     * @param <T> the decoded type.
     * @return the decoded value.
     * @throws IOException in case decoding failed.
     */
    public static <T> T byteBufToClass(ByteBuf input, Class<? extends T> clazz, ObjectMapper mapper)
            throws IOException {
        ByteBufToArray toArray = byteBufToByteArray(input);
        return mapper.readValue(toArray.byteArray, toArray.offset, toArray.length, clazz);
    }

    /**
     * Converts a {@link ByteBuf} representing a valid JSON entity to a generic {@link Object},
     * <b>without releasing the buffer</b>. The entity can either be a JSON object, array or scalar value,
     * potentially with leading whitespace (which gets ignored). JSON objects are converted to a {@link JsonObject}
     * and JSON arrays to a {@link JsonArray}.
     *
     * Detection of JSON objects and arrays is attempted in order not to incur an
     * additional conversion step (JSON to Map to JsonObject for example), but if a
     * Map or List is produced, it will be transformed to {@link JsonObject} or
     * {@link JsonArray} (with a warning logged).
     *
     * @param input the buffer to convert. It won't be released.
     * @return a Object decoded from the buffer
     * @throws IOException if the decoding fails.
     */
    public static Object byteBufToGenericObject(ByteBuf input, ObjectMapper mapper) throws IOException {
        //skip leading whitespaces
        int toSkip = input.forEachByte(new WhitespaceSkipper());
        if (toSkip > 0) {
            input.skipBytes(toSkip);
        }
        //peek into the buffer for quick detection of objects and arrays
        input.markReaderIndex();
        byte first = input.readByte();
        input.resetReaderIndex();

        switch (first) {
        case '{':
            return byteBufToClass(input, JsonObject.class, mapper);
        case '[':
            return byteBufToClass(input, JsonArray.class, mapper);
        }

        //we couldn't fast detect the type, we'll have to unmarshall to object and make sure maps and lists
        //are converted to JsonObject/JsonArray.
        Object value = byteBufToClass(input, Object.class, mapper);
        if (value instanceof Map) {
            LOGGER.warn("A JSON object could not be fast detected (first byte '" + (char) first + "')");
            return JsonObject.from((Map<String, ?>) value);
        } else if (value instanceof List) {
            LOGGER.warn("A JSON array could not be fast detected (first byte '" + (char) first + "')");
            return JsonArray.from((List<?>) value);
        } else {
            return value;
        }
    }

    /**
     * The common flags enum.
     */
    public static enum CommonFlags {
        RESERVED, PRIVATE, JSON, BINARY, STRING
    }

}