BinaryData.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.util;

import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.serializer.JsonSerializerProviders;
import com.azure.core.util.serializer.ObjectSerializer;
import com.azure.core.util.serializer.JsonSerializer;
import com.azure.core.util.serializer.TypeReference;
import static com.azure.core.util.FluxUtil.monoError;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;

/**
 * This class is an abstraction over many different ways that binary data can be represented. The {@link BinaryData}
 * can be created from {@link InputStream}, {@link Flux} of {@link ByteBuffer}, {@link String}, {@link Object}, or byte
 * array.
 * <p><strong>Immutable data</strong></p>
 * {@link BinaryData} is constructed by copying the given data. Once {@link BinaryData} is instantiated, it can not be
 * changed. It provides various convenient APIs to get data out of {@link BinaryData}, they all start with the 'to'
 * prefix, for example {@link BinaryData#toBytes()}.
 * <p>
 * Code samples are presented below.
 *
 * <p><strong>Create an instance from Bytes</strong></p>
 * {@codesnippet com.azure.core.util.BinaryData.from#bytes}
 *
 * <p><strong>Create an instance from String</strong></p>
 * {@codesnippet com.azure.core.util.BinaryData.from#String}
 *
 * <p><strong>Create an instance from InputStream</strong></p>
 * {@codesnippet com.azure.core.util.BinaryData.from#Stream}
 *
 * <p><strong>Create an instance from Object</strong></p>
 * {@codesnippet com.azure.core.util.BinaryData.fromObject}
 *
 * @see ObjectSerializer
 * @see JsonSerializer
 * @see <a href="https://aka.ms/azsdk/java/docs/serialization" target="_blank">More about serialization</a>
 */
public final class  BinaryData {
    private static final ClientLogger LOGGER = new ClientLogger(BinaryData.class);
    private static final BinaryData EMPTY_DATA = new BinaryData(new byte[0]);

    private static final Object LOCK = new Object();

    private final byte[] data;

    private static volatile JsonSerializer defaultJsonSerializer;

    /**
     * Create an instance of {@link BinaryData} from the given data.
     *
     * @param data to represent as bytes.
     */
    BinaryData(byte[] data) {
        this.data = data;
    }

    /**
     * Creates a {@link BinaryData} instance with given {@link InputStream} as source of data. The {@link InputStream}
     * is not closed by this function.
     *
     * <p><strong>Create an instance from InputStream</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.from#Stream}
     *
     * @param inputStream The {@link InputStream} to use as data backing the instance of {@link BinaryData}.
     * @throws UncheckedIOException If any error in reading from {@link InputStream}.
     * @throws NullPointerException If {@code inputStream} is null.
     * @return {@link BinaryData} representing the binary data.
     */
    public static BinaryData fromStream(InputStream inputStream) {
        if (Objects.isNull(inputStream)) {
            return EMPTY_DATA;
        }

        final int bufferSize = 1024;
        try {
            ByteArrayOutputStream dataOutputBuffer = new ByteArrayOutputStream();
            int nRead;
            byte[] data = new byte[bufferSize];
            while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                dataOutputBuffer.write(data, 0, nRead);
            }
            dataOutputBuffer.flush();

            return new BinaryData(dataOutputBuffer.toByteArray());

        } catch (IOException ex) {
            throw LOGGER.logExceptionAsError(new UncheckedIOException(ex));
        }
    }

    /**
     * Asynchronously creates a {@link BinaryData} instance with the given {@link InputStream} as source of data. The
     * {@link InputStream} is not closed by this function. If the {@link InputStream} is {@code null}, an empty
     * {@link BinaryData} will be returned.
     *
     * @param inputStream The {@link InputStream} to use as data backing the instance of {@link BinaryData}.
     * @return {@link Mono} of {@link BinaryData} representing the binary data.
     */
    public static Mono<BinaryData> fromStreamAsync(InputStream inputStream) {
        return Mono.fromCallable(() -> fromStream(inputStream));
    }

    /**
     * Creates a {@link BinaryData} instance with given {@link Flux} of {@link ByteBuffer} as source of data. It will
     * collect all the bytes from {@link ByteBuffer} into {@link BinaryData}. If the {@link Flux} is {@code null}, an
     * empty {@link BinaryData} will be returned.
     *
     * <p><strong>Create an instance from String</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.from#Flux}
     *
     * @param data The byte buffer stream to use as data backing the instance of {@link BinaryData}.
     * @return {@link Mono} of {@link BinaryData} representing binary data.
     */
    public static Mono<BinaryData> fromFlux(Flux<ByteBuffer> data) {
        if (Objects.isNull(data)) {
            return Mono.just(EMPTY_DATA);
        }

        return FluxUtil.collectBytesInByteBufferStream(data)
            .flatMap(bytes -> Mono.just(new BinaryData(bytes)));
    }

    /**
     * Creates a {@link BinaryData} instance with given data. The {@link String} is converted into bytes using UTF_8
     * character set. If the String is {@code null}, an empty {@link BinaryData} will be returned.
     *
     * @param data The string to use as data backing the instance of {@link BinaryData}.
     * @return {@link BinaryData} representing binary data.
     */
    public static BinaryData fromString(String data) {
        if (Objects.isNull(data) || data.length() == 0) {
            return EMPTY_DATA;
        }

        return new BinaryData(data.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * Creates a {@link BinaryData} instance with given byte array data. If the byte array is {@code null}, an empty
     * {@link BinaryData} will be returned.
     *
     * @param data The byte array to use as data backing the instance of {@link BinaryData}.
     * @return {@link BinaryData} representing the binary data.
     */
    public static BinaryData fromBytes(byte[] data) {
        if (Objects.isNull(data) || data.length == 0) {
            return EMPTY_DATA;
        }

        return new BinaryData(Arrays.copyOf(data, data.length));
    }

    /**
     * Serialize the given {@link Object} into {@link BinaryData} using json serializer which is available on classpath.
     * The serializer on classpath must implement {@link JsonSerializer} interface. If the given Object is {@code null},
     * an empty {@link BinaryData} will be returned.
     * <p><strong>Code sample</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.fromObject}

     * @param data The object to use as data backing the instance of {@link BinaryData}.
     * @throws IllegalStateException If a {@link JsonSerializer} cannot be found on the classpath.
     * @return {@link BinaryData} representing the JSON serialized object.
     *
     * @see JsonSerializer
     * @see <a href="ObjectSerializer" target="_blank">More about serialization</a>
     */
    public static BinaryData fromObject(Object data) {
        if (Objects.isNull(data)) {
            return EMPTY_DATA;
        }
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        getDefaultSerializer().serialize(outputStream, data);

        return new BinaryData(outputStream.toByteArray());
    }

    /**
     * Serialize the given {@link Object} into {@link BinaryData} using json serializer which is available on classpath.
     * The serializer on classpath must implement {@link JsonSerializer} interface. If the given Object is {@code null},
     * an empty {@link BinaryData} will be returned.
     * <p><strong>Code sample</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.fromObjectAsync}

     * @param data The object to use as data backing the instance of {@link BinaryData}.
     * @throws IllegalStateException If a {@link JsonSerializer} cannot be found on the classpath.
     * @return {@link BinaryData} representing the JSON serialized object.
     *
     * @see JsonSerializer
     * @see <a href="ObjectSerializer" target="_blank">More about serialization</a>
     */
    public static Mono<BinaryData> fromObjectAsync(Object data) {
        return Mono.fromCallable(() -> fromObject(data));
    }

    /**
     * Serialize the given {@link Object} into {@link BinaryData} using the provided {@link ObjectSerializer}.
     * If the Object is {@code null}, an empty {@link BinaryData} will be returned.
     * <p>You can provide your custom implementation of {@link ObjectSerializer} interface or use one provided in Azure
     * SDK by adding them as dependency.
     * <ul>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-jackson" target="_blank">Jackson serializer</a></li>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-gson" target="_blank">Gson serializer</a>.</li>
     * </ul>
     *
     * <p><strong>Create an instance from Object</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.fromObject#Object-ObjectSerializer}
     *
     * @param data The object to use as data backing the instance of {@link BinaryData}.
     * @param serializer to use for serializing the object.
     * @throws NullPointerException If {@code serializer} is null.
     * @return {@link BinaryData} representing binary data.
     * @see ObjectSerializer
     * @see JsonSerializer
     * @see <a href="https://aka.ms/azsdk/java/docs/serialization" target="_blank">More about serialization</a>
     */
    public static BinaryData fromObject(Object data, ObjectSerializer serializer) {
        if (Objects.isNull(data)) {
            return EMPTY_DATA;
        }

        Objects.requireNonNull(serializer, "'serializer' cannot be null.");

        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        serializer.serialize(outputStream, data);
        return new BinaryData(outputStream.toByteArray());
    }

    /**
     * Serialize the given {@link Object} into {@link Mono} {@link BinaryData} using the provided
     * {@link ObjectSerializer}. If the Object is {@code null}, an empty {@link BinaryData} will be returned.
     *
     * <p>You can provide your custom implementation of {@link ObjectSerializer} interface or use one provided in zure
     * SDK by adding them as dependency.
     * <ul>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-jackson" target="_blank">Jackson serializer</a></li>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-gson" target="_blank">Gson serializer</a>.</li>
     * </ul>
     *
     * @param data The object to use as data backing the instance of {@link BinaryData}.
     * @param serializer to use for serializing the object.
     * @throws NullPointerException If {@code serializer} is null.
     * @return {@link Mono} of {@link BinaryData} representing the binary data.
     * @see ObjectSerializer
     * @see <a href="https://aka.ms/azsdk/java/docs/serialization" target="_blank">More about serialization</a>
     */
    public static Mono<BinaryData> fromObjectAsync(Object data, ObjectSerializer serializer) {
        if (Objects.isNull(serializer)) {
            return monoError(LOGGER, new NullPointerException("'serializer' cannot be null."));
        }
        return Mono.fromCallable(() -> fromObject(data, serializer));
    }

    /**
     * Provides byte array representation of this {@link BinaryData} object.
     *
     * @return byte array representation of the the data.
     */
    public byte[] toBytes() {
        return Arrays.copyOf(this.data, this.data.length);
    }

    /**
     * Provides {@link String} representation of this {@link BinaryData} object. The bytes are converted into
     * {@link String} using the UTF-8 character set.
     *
     * @return {@link String} representation of the data.
     */
    public String toString() {
        return new String(this.data, StandardCharsets.UTF_8);
    }

    /**
     * Deserialize the bytes into the {@link Object} of given type by applying the provided {@link ObjectSerializer} on
     * the data. The type, represented by {@link TypeReference}, can either be a regular class or a generic class that
     * retains the type information.
     *
     * <p>You can provide your custom implementation of {@link ObjectSerializer} interface or use one provided in zure
     * SDK by adding them as dependency.
     * <ul>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-jackson" target="_blank">Jackson serializer</a></li>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-gson" target="_blank">Gson serializer</a>.</li>
     * </ul>
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing a regular class</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObject#TypeReference-ObjectSerializer}
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing generic types</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObject#TypeReference-ObjectSerializer-generic}
     *
     * @param typeReference representing the {@link TypeReference type} of the Object.
     * @param serializer to use deserialize data into type.
     * @param <T> Generic type that the data is deserialized into.
     * @throws NullPointerException If {@code serializer} or {@code typeReference} is null.
     * @return The {@link Object} of given type after deserializing the bytes.
     */
    public <T> T toObject(TypeReference<T> typeReference, ObjectSerializer serializer) {
        Objects.requireNonNull(typeReference, "'typeReference' cannot be null.");
        Objects.requireNonNull(serializer, "'serializer' cannot be null.");

        InputStream jsonStream = new ByteArrayInputStream(this.data);
        return serializer.deserialize(jsonStream, typeReference);
    }

    /**
     * Return a {@link Mono} by deserializing the bytes into the {@link Object} of given type after applying the
     * provided {@link ObjectSerializer} on the {@link BinaryData}. The type, represented by {@link TypeReference},
     * can either be a regular class or a generic class that retains the type information.
     *
     * <p>You can provide your custom implementation of {@link ObjectSerializer} interface or use one provided in zure
     * SDK by adding them as dependency.
     * <ul>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-jackson" target="_blank">Jackson serializer</a></li>
     * <li><a href="https://mvnrepository.com/artifact/com.azure/azure-core-serializer-json-gson" target="_blank">Gson serializer</a>.</li>
     * </ul>
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing a regular class</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObjectAsync#TypeReference-ObjectSerializer}
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing generic types</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObjectAsync#TypeReference-ObjectSerializer-generic}
     *
     * @param typeReference representing the {@link TypeReference type} of the Object.
     * @param serializer to use deserialize data into type.
     * @param <T> Generic type that the data is deserialized into.
     * @throws NullPointerException If {@code typeReference} or {@code serializer} is null.
     * @return The {@link Object} of given type after deserializing the bytes.
     */
    public <T> Mono<T> toObjectAsync(TypeReference<T> typeReference, ObjectSerializer serializer) {

        if (Objects.isNull(typeReference)) {
            return monoError(LOGGER, new NullPointerException("'typeReference' cannot be null."));
        } else if (Objects.isNull(serializer)) {
            return monoError(LOGGER, new NullPointerException("'serializer' cannot be null."));
        }
        return Mono.fromCallable(() -> toObject(typeReference, serializer));
    }

    /**
     * Deserialize the bytes into the {@link Object} of given type by using json serializer which is available in
     * classpath. The type, represented by {@link TypeReference}, can either be a regular class or a generic class that
     * retains the type information. This method assumes the data to be in JSON format and will use a default
     * implementation of {@link JsonSerializer}.
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing a regular class</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObject#TypeReference}
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing generic types</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObject#TypeReference-generic}
     *
     * @param typeReference representing the {@link TypeReference type} of the Object.
     * @param <T> Generic type that the data is deserialized into.
     * @throws NullPointerException If {@code typeReference} is null.
     * @return The {@link Object} of given type after deserializing the bytes.
     */
    public <T> T toObject(TypeReference<T> typeReference) {
        Objects.requireNonNull(typeReference, "'typeReference' cannot be null.");

        InputStream jsonStream = new ByteArrayInputStream(this.data);
        return getDefaultSerializer().deserialize(jsonStream, typeReference);
    }

    /**
     * Return a {@link Mono} by deserializing the bytes into the {@link Object} of given type after applying the Json
     * serializer found on classpath. The type, represented by {@link TypeReference}, can either be a regular class
     * or a generic class that retains the type information. This method assumes the data to be in JSON format and will
     * use a default implementation of {@link JsonSerializer}.
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing a regular class</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObjectAsync#TypeReference}
     *
     * <p><strong>Code sample to demonstrate serializing and deserializing generic types</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.toObjectAsync#TypeReference-generic}
     *
     * @param typeReference representing the {@link TypeReference type} of the Object.
     * @param <T> Generic type that the data is deserialized into.
     * @throws NullPointerException If {@code typeReference} is null.
     * @return The {@link Object} of given type after deserializing the bytes.
     */
    public <T> Mono<T> toObjectAsync(TypeReference<T> typeReference) {
        if (Objects.isNull(typeReference)) {
            return monoError(LOGGER, new NullPointerException("'typeReference' cannot be null."));
        }
        return Mono.fromCallable(() -> toObject(typeReference));
    }

    /**
     * Provides {@link InputStream} for the data represented by this {@link BinaryData} object.
     *
     * <p><strong>Get InputStream from BinaryData</strong></p>
     * {@codesnippet com.azure.core.util.BinaryData.to#Stream}
     *
     * @return {@link InputStream} representing the binary data.
     */
    public InputStream toStream() {
        return new ByteArrayInputStream(this.data);
    }

    /* This will ensure lazy instantiation to avoid hard dependency on Json Serializer. */
    private static JsonSerializer getDefaultSerializer() {
        if (defaultJsonSerializer ==  null) {
            synchronized (LOCK) {
                if (defaultJsonSerializer == null) {
                    defaultJsonSerializer = JsonSerializerProviders.createInstance();
                }
            }
        }
        return defaultJsonSerializer;
    }
}