public record Blob()

in api/applib/src/main/java/org/apache/causeway/applib/value/Blob.java [83:397]


public record Blob(
    String name,
    MimeType mimeType,
    byte[] bytes
    ) implements NamedWithMimeType {

    // -- FACTORIES

    /**
     * Returns a new {@link Blob} of given {@code name}, {@code mimeType} and {@code content}.
     * <p>
     * {@code name} may or may not include the desired filename extension, as it
     * is guaranteed, that the resulting {@link Blob} has the appropriate extension
     * as constraint by the given {@code mimeType}.
     * <p>
     * For more fine-grained control use one of the {@link Blob} constructors directly.
     * @param name - may or may not include the desired filename extension
     * @param mimeType
     * @param content - bytes
     * @return new {@link Blob}
     */
    public static Blob of(final String name, final CommonMimeType mimeType, final byte[] content) {
        var fileName = _Strings.asFileNameWithExtension(name, mimeType.getProposedFileExtensions());
        return new Blob(fileName, mimeType.getMimeType(), content);
    }

    /**
     * Returns a new {@link Blob} of given {@code name}, {@code mimeType} and content from {@code dataSource},
     * wrapped with a {@link Try}.
     * <p>
     * {@code name} may or may not include the desired filename extension, as it
     * is guaranteed, that the resulting {@link Blob} has the appropriate extension
     * as constraint by the given {@code mimeType}.
     * <p>
     * For more fine-grained control use one of the {@link Blob} factories directly.
     * @param name - may or may not include the desired filename extension
     * @param mimeType
     * @param dataSource - the {@link DataSource} to be opened for reading
     * @return new {@link Blob}
     */
    public static Try<Blob> tryRead(final String name, final CommonMimeType mimeType, final DataSource dataSource) {
        return dataSource.tryReadAsBytes()
                .mapSuccess(bytes->Blob.of(name, mimeType, bytes.orElse(null)));
    }

    /**
     * Shortcut for {@code tryRead(name, mimeType, DataSource.ofFile(file))}
     * @see #tryRead(String, org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType, DataSource)
     */
    public static Try<Blob> tryRead(final String name, final CommonMimeType mimeType, final File file) {
        return tryRead(name, mimeType, DataSource.ofFile(file));
    }

     // --

    public Blob(final String name, final String primaryType, final String subtype, final byte[] bytes) {
        this(name, CommonMimeType.newMimeType(primaryType, subtype), bytes);
    }

    public Blob(final String name, final String mimeTypeBase, final byte[] bytes) {
        this(name, CommonMimeType.newMimeType(mimeTypeBase), bytes);
    }

    // canonical constructor
    public Blob(final String name, final MimeType mimeType, final byte[] bytes) {
        if(name == null) {
            throw new IllegalArgumentException("Name cannot be null");
        }
        if(mimeType == null) {
            throw new IllegalArgumentException("MimeType cannot be null");
        }
        if(name.contains(":")) {
            throw new IllegalArgumentException("Name cannot contain ':'");
        }
        if(bytes == null) {
            throw new IllegalArgumentException("Bytes cannot be null");
        }
        this.name = name;
        this.mimeType = mimeType;
        this.bytes = bytes;
    }

    /**
     * @deprecated use {@link #bytes()} instead
     */
    public byte[] getBytes() { return bytes(); }

    // -- UTILITIES

    /**
     * Converts to a {@link Clob}, using given {@link Charset}
     * for the underlying byte[] to String conversion.
     */
    public Clob toClob(final @NonNull Charset charset) {
        return new Clob(name(), mimeType(), _Strings.ofBytes(bytes(), charset));
    }

    /**
     * Does not close the OutputStream.
     */
    @SneakyThrows
    public void writeBytesTo(final @Nullable OutputStream os) {
        if(os==null) return;
        
        if(bytes!=null) {
            os.write(bytes);
        }
    }

    /**
     * Writes this {@link Blob} to the file represented by
     * the specified <code>File</code> object.
     * <p>
     * If the file exists but is a directory rather than a regular file, does
     * not exist but cannot be created, or cannot be opened for any other
     * reason then a <code>FileNotFoundException</code> is thrown.
     *
     * @param      file the file to be opened for writing; if <code>null</code> this method does nothing
     * @see        java.io.FileOutputStream
     */
    @SneakyThrows
    public void writeTo(final @Nullable File file) {
        if(file==null) return; // just ignore
        
        try(var os = new FileOutputStream(file)){
            writeBytesTo(os);
        }
    }

    /**
     * Returns a new {@link DataSource} for underlying byte array.
     * @see DataSource
     */
    public DataSource asDataSource() {
        return DataSource.ofBytes(_NullSafe.toNonNull(getBytes()));
    }

    /**
     * Returns a new {@link Blob} that has this Blob's underlying byte array
     * zipped into a zip-entry using this Blob's name.
     */
    public Blob zip() {
        return zip(name());
    }

    /**
     * Returns a new {@link Blob} that has this Blob's underlying byte array
     * zipped into a zip-entry with given zip-entry name.
     * @param zipEntryNameIfAny - if null or empty this Blob's name is used
     */
    public Blob zip(final @Nullable String zipEntryNameIfAny) {
        var zipEntryName = _Strings.nonEmpty(zipEntryNameIfAny)
            .orElseGet(this::name);
        var zipBuilder = ZipUtils.zipEntryBuilder();
        zipBuilder.add(zipEntryName, getBytes());
        return Blob.of(name()+".zip", CommonMimeType.ZIP, zipBuilder.toBytes());
    }

    public Blob unZip(final @NonNull CommonMimeType resultingMimeType) {
        return unZip(resultingMimeType, ZipOptions.builder().build());
    }

    public Blob unZip(final @NonNull CommonMimeType resultingMimeType, final @NonNull ZipOptions zipOptions) {
        return ZipUtils.firstZipEntry(asDataSource(), zipOptions) // assuming first entry is the one we want
                .map(zipEntryDataSource->Blob.of(
                        zipEntryDataSource.zipEntry().getName(),
                        resultingMimeType,
                        zipEntryDataSource.bytes()))
                .orElseThrow(()->_Exceptions
                      .unrecoverable("failed to unzip blob, no entry found %s", name()));
    }

    // -- HASHING

    public Try<HashUtils.Hash> tryHash(final @NonNull HashAlgorithm hashAlgorithm) {
        return HashUtils.tryDigest(hashAlgorithm, bytes, 4*1024); // 4k default
    }

    public String md5Hex() {
        return tryHash(HashAlgorithm.MD5)
                .valueAsNonNullElseFail()
                .asHexString();
    }

    public String sha256Hex() {
        return tryHash(HashAlgorithm.SHA256)
                .valueAsNonNullElseFail()
                .asHexString();
    }

    // -- OBJECT CONTRACT

    @Override
    public boolean equals(final Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        final Blob blob = (Blob) o;
        return Objects.equals(mimeType.toString(), blob.mimeType.toString()) &&
                Arrays.equals(bytes, blob.bytes) &&
                Objects.equals(name, blob.name);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(mimeType.toString(), name);
        result = 31 * result + Arrays.hashCode(bytes);
        return result;
    }

    @Override
    public String toString() {
        return name() + " [" + mimeType().getBaseType() + "]: " + bytes().length + " bytes";
    }

    /**
     * (thread-safe)
     * @implNote see also BlobValueSemanticsProvider
     */
    public static final class JaxbToStringAdapter extends XmlAdapter<String, Blob> {

        private final PrimitiveJaxbAdapters.BytesAdapter bytesAdapter = new PrimitiveJaxbAdapters.BytesAdapter(); // thread-safe

        @Override
        public Blob unmarshal(final String data) throws Exception {
            if(data==null) {
                return null;
            }
            final int colonIdx = data.indexOf(':');
            final String name  = data.substring(0, colonIdx);
            final int colon2Idx  = data.indexOf(":", colonIdx+1);
            final String mimeTypeBase = data.substring(colonIdx+1, colon2Idx);
            final String payload = data.substring(colon2Idx+1);
            final byte[] bytes = bytesAdapter.unmarshal(payload);
            try {
                return new Blob(name, new MimeType(mimeTypeBase), bytes);
            } catch (MimeTypeParseException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public String marshal(final Blob blob) throws Exception {
            if(blob==null) {
                return null;
            }
            String s = blob.name() +
                    ':' +
                    blob.mimeType().getBaseType() +
                    ':' +
                    bytesAdapter.marshal(blob.bytes());
            return s;
        }

    }

    /**
     * @return optionally the payload as a {@link BufferedImage} based on whether
     * this Blob's MIME type identifies as image and whether the payload is not empty
     */
    public Optional<BufferedImage> asImage() {

        var bytes = getBytes();
        if(bytes == null) {
            return Optional.empty();
        }

        var mimeType = mimeType();
        if(mimeType == null || !mimeType.getPrimaryType().equals("image")) {
            return Optional.empty();
        }

        try {
            var img = _Images.fromBytes(bytes());
            return Optional.ofNullable(img);
        } catch (Exception e) {
            log.error("failed to read image data", e);
            return Optional.empty();
        }

    }

    // -- SERIALIZATION PROXY

    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    private void readObject(final ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }

    private static class SerializationProxy implements Serializable {
        /**
         * Generated, based on String, String, bytes[]
         */
        private static final long serialVersionUID = -950845631214162726L;
        private final String name;
        private final String mimeTypeBase;
        private final byte[] bytes;

        private SerializationProxy(final Blob blob) {
            this.name = blob.name();
            this.mimeTypeBase = blob.mimeType().getBaseType();
            this.bytes = blob.bytes();
        }

        private Object readResolve() {
            return new Blob(name, mimeTypeBase, bytes);
        }

    }

}