blob.java
/*
** Module : blob.java
** Abstract : Data wrapper for SQL BLOB data.
**
** Copyright (c) 2018-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20181214 Created initial version.
** 002 CA 20190516 Added blob(BDT) constructor.
** 003 IAS 20190617 Changed the signature of the writeByteRange method
** 004 CA 20190628 Added write/readExternal.
** 005 ECF 20190628 Implemented COPY-LOB runtime.
** 006 SVL 20191002 Added assign(blob) in order to work around an issue when a dynamic record with
** a blob field cannot be created.
** 007 CA 20191118 blob initial state is unknown.
** 008 CA 20191203 Fixed overlay and unknown COPY-LOB related bugs.
** 009 IAS 20200908 Rework (de)serialization.
** 010 IAS 20201007 Added Type enum
** OM 20210312 Added parent FieldReference accessors in case the LOB was obtained by dereferenciation
** of a buffer field. Improved COPY-LOB engine and validations.
** 011 AL2 20220328 Added proxy checks for parameter BDT.
** EVL 20221222 Adding runtime support for LOB fields export.
** 012 CA 20230215 'instantiateDefault' now is implemented by each sub-class.
** CA 20230215 'duplicate()' method returns the real type instead of BDT.
** 013 IAS 20230225 Avoid NPE in <code>writeExternal</code>.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.util;
import static com.goldencode.util.NativeTypeSerializer.*;
import java.io.*;
import java.sql.*;
import com.goldencode.p2j.persist.*;
/**
* Data wrapper type for SQL BLOB data.
*/
public class blob
extends BinaryData
implements LargeObject
{
/** Maximum size of the blob */
private static final int MAX_SIZE = 1024 * 1024 * 1024;
/** Backing data */
private byte[] value = null;
/** SQL BLOB object */
private Blob sqlBlob = null;
/** The parent buffer field where this object was obtained by dereferenciation, if this is the case. */
private FieldReference fRef;
/** The field name used with current field reference. */
private String fieldName = null;
/**
* Default constructor.
*/
public blob()
{
setUnknown();
}
/**
* Constructor used for creating initial field from default {@code String}.
* @param def
* default value
*/
public blob(String def)
{
assign((byte[]) null);
}
/**
* Copy constructor.
*
* @param value
* Blob to be copied.
*/
public blob(blob value)
{
if (value == null)
{
assign((byte[]) null);
}
else
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
if (value.val() instanceof blob)
{
value = (blob) value.val();
}
else
{
assign(value.val());
return;
}
}
assign(value.asByteArray());
this.sqlBlob = value.sqlBlob;
}
}
/**
* Constructor which is initialized from a database record.
*
* @param sqlBlob
* SQL BLOB from which data is read.
*
* @throws SQLException
* if there is an error reading data.
*/
public blob(Blob sqlBlob)
throws SQLException
{
this.sqlBlob = sqlBlob;
// TODO: enable optimization to skip this step in certain cases (e.g., query field list
// exclusion)
readData();
}
/**
* Constructor which initializes an instance from the given data.
*
* @param value
* Data.
*/
public blob(byte[] value)
{
assign(value);
}
/**
* Constructor which initializes an instance from a memptr object.
*
* @param value
* Pointer from which data is copied.
*/
public blob(memptr value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
assign(value.val());
}
else
{
assign(value.asByteArray());
}
}
/**
* Constructor which initializes an instance from a raw object.
*
* @param value
* Raw object from which data is copied.
*/
public blob(raw value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
assign(value.val());
}
else
{
assign(value.asByteArray());
}
}
/**
* Constructor which initializes an instance from a POLY value.
*
* @param value
* POLY value from which data is copied.
*/
public blob(BaseDataType value)
{
if (value.val() instanceof BinaryData)
{
assign(((BinaryData) value.val()).asByteArray());
}
else
{
UnimplementedFeature.missing("blob(POLY) constructor is not implemented!");
}
}
/**
* Get the type
* @return type
*/
public Type getType()
{
return Type.BLOB;
}
/**
* Read data from the SQL BLOB used to initialize this object.
*
* @throws SQLException
* if there is an error reading data.
*/
public void readData()
throws SQLException
{
if (this.value != null)
{
// already initialized
return;
}
if (sqlBlob == null)
{
setUnknown();
return;
}
int len = (int) sqlBlob.length();
if (len > 0)
{
assign(sqlBlob.getBytes(1, len));
}
sqlBlob.free();
sqlBlob = null;
}
/**
* Indicate whether this object manages character data (as opposed to binary data).
*
* @return {@code false}.
*/
public boolean isCharacterData()
{
return false;
}
/**
* Sets the length of the buffer/array to a given length. Depending on the sub-class, this
* may result in the allocation of memory, or the extending/truncating of the backing array.
* It is up to the sub-class to determine if an already allocated buffer/array will be
* resized.
* <p>
* This implementation is a no-op.
*
* @param len
* New length of the buffer/array.
*
* @return The current instance is returned.
*/
@Override
public BinaryData setLength(long len)
{
return this;
}
/**
* Reports if this instance represents an uninitialized buffer. Being uninitialized is not the
* same as being of zero length. Rather it means that the buffer was never allocated. Being
* <code>unknown</code> is also different from being uninitialized.
*
* @return <code>true</code> if this instance is uninitialized.
*/
@Override
public boolean isUninitialized()
{
return value == null;
}
/**
* @see com.goldencode.p2j.util.BaseDataType#duplicate()
*
* This class is not undoable, so return a reference to this object.
*
* @return This object.
*/
@Override
public blob duplicate()
{
return this;
}
/**
* Creates a new instance of the same type that represents the {@code unknown value}.
*
* @return An instance that represents the {@code unknown value}.
*/
@Override
public BaseDataType instantiateUnknown()
{
blob blb = new blob();
blb.setUnknown();
return blb;
}
/**
* Creates a new instance of the same type that represents the default initialized value.
*
* @return An instance that represents the default value.
*/
@Override
public BaseDataType instantiateDefault()
{
return new blob();
}
/**
* The function {@code STRING(blob-value, format)} always generates error 11382 in OE.
*
* @param fmt
* The format string to use.
*
* @return The formatted string.
*/
@Override
public String toString(String fmt)
{
ErrorManager.recordOrThrowError(11382);
// INPUT/OUTPUT operations are not allowed with RAW, ROWID, MEMPTR, BLOB, CLOB or LONGCHAR type variables. (11382)
return "";
}
/**
* Creates a string representation of the instance data in a form that is compatible with the
* {@code MESSAGE} language statement. Regardless the instance represents the {@code unknown} value or not,
* a "?" character value will always be returned.
*
* @return The 'message' formatted string.
*/
@Override
public String toStringMessage()
{
return "?"; // in 4GL STRING(tt1.blobField) will return the "?" value!
// NOTE: the following code is only used for FWD debugging
// if (isUnknown())
// {
// return null;
// }
//
// int len = value.length;
// StringBuilder buf = new StringBuilder("[");
// for (int i = 0; i < len; i++)
// {
// if (i > 0)
// {
// buf.append(',');
// }
// buf.append(Integer.toString(value[i], 16));
// }
// buf.append(']');
// return buf.toString();
}
/**
* Creates a string representation of the instance data using the 'export'
* format. If the instance represents the <code>unknown value</code>, a
* '?' will be returned.
*
* @return The 'export' formatted string.
*/
@Override
public String toStringExport()
{
// TODO: implement
return null;
}
/**
* Return all or some of the contents of the current buffer as an array. This will be a copy
* of the data if the given parameters indicate a subset of the full data; otherwise, it will
* be the original, backing buffer.
*
* @param pos
* Starting offset position.
* @param len
* Length of range to return.
*
* @return The contents of the instance (possibly a copy). If the instance is uninitialized
* the returned value will be a 0 length array. If the instance is {@code unknown}
* or has an undefined size, then {@code null} will be returned.
*/
@Override
public byte[] asByteArray(long pos, long len)
{
if (isUnknown())
{
return null;
}
if (pos == 0 && len == value.length)
{
return value;
}
int offset = (int) Math.max(0, pos);
int length = (int) Math.min(len, value.length);
byte[] copy = new byte[length];
System.arraycopy(value, offset, copy, 0, length);
return copy;
}
/**
* Replacement for the default object writing method. The latest
* state is written to the output destination.
*
* @param out
* The output destination to which fields will be saved.
*
* @throws IOException
* In case of I/O errors.
*/
@Override
public void writeExternal(ObjectOutput out)
throws IOException
{
super.writeExternal(out);
writeByteArray(out, value);
writeString(out, fieldName);
}
/**
* Replacement for the default object reading method. The latest
* state is read from the input source.
*
* @param in
* The input source from which fields will be restored.
*
* @throws IOException
* In case of I/O errors.
* @throws ClassNotFoundException
* If payload can't be instantiated.
*/
@Override
public void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
super.readExternal(in);
value = readByteArray(in);
fieldName = readString(in);
}
/**
* Release any resources associated with the instance.
*/
@Override
protected void deallocate()
{
value = new byte[0];
}
/**
* Determines if the array of binary data stored in an instance of this
* class must be limited in length.
*
* @return <code>true</code> if the length must be limited.
*/
@Override
protected boolean isLengthLimited()
{
return true;
}
/**
* Determines the maximum length of the array of binary data stored in an
* instance of this class.
*
* @return The length limit for binary data or 0 if this class has no limit.
*/
@Override
protected long lengthLimit()
{
return MAX_SIZE;
}
/**
* Determines if the array of binary data stored in an instance of this
* class must be automatically extended in length when operations
* write past the current length. Such extensions will pad with
* <code>null</code> bytes.
*
* @return <code>true</code> if the length must automatically extended.
*/
@Override
protected boolean isAutoExtend()
{
return false;
}
/**
* Helper to create a new byte array with the same contents as the given
* byte array and the specified length. If the given target length
* is smaller than the source array length, the output array is truncated
* to the specified size. If the target length is larger than the
* source array length, the output array is extended to the given size
* and any additional bytes are initialized to '\0'.
* <p>
* This is safe to use on 0 length arrays.
* <p>
* This implementation is a no-op.
*
* @param len
* The output array length.
*/
@Override
protected void extendBytes(long len)
{
}
/**
* Replace the entire contents of the current buffer with the given array.
*
* @param value
* The new contents.
*/
@Override
protected void replaceContents(byte[] value)
{
this.value = value;
clearUnknown();
}
/**
* Return the entire contents of the current buffer as an array. This may be a copy of the
* data, depending on the subclass' implementation. DO NOT MODIFY the data that is returned,
* since it is undefined as to whether the changes will or will not be reflected in the
* actual storage.
*
* @return The contents of the instance (possibly a copy). If the instance is uninitialized
* the returned value will be a 0 length array. If the instance is
* <code>unknown</code>, then <code>null</code> will be returned.
*/
@Override
protected byte[] asByteArray()
{
if (isUnknown())
{
return null;
}
if (value == null)
{
return new byte[0];
}
return value;
}
/**
* The actual size of the buffer (in bytes).
*
* @return The length or 0 if uninitialized.
*/
@Override
protected long internalLength()
{
return isUnknown() ? 0 : value.length;
}
/**
* The length of the buffer as reported to P4GL application.
*
* @return The length or 0 if uninitialized.
*/
@Override
protected long visibleLength()
{
return internalLength();
}
/**
* Returns the next index position into the array which is the <code>null</code> byte,
* starting at a given index position.
* <p>
* This implementation is a no-op.
*
* @param pos
* The 0-based index into the array at which to start searching.
*
* @return The next index into the array which is the <code>null</code> byte or the length
* of the array if no <code>null</code> byte exists. This implementation always
* returns 0.
*/
@Override
protected long findNextNull(long pos)
{
return 0;
}
/**
* Read the byte at the given location. All boundary checking and error handling must have
* already been done in the caller such that this is known to be a valid access. Any
* unexpected failure may cause a platform-specific exception that terminates the process!
*
* @param pos
* The 0-based index position at which to read the byte.
*
* @return The byte at that location.
*/
@Override
protected byte readByte(long pos)
{
return value[(int) pos];
}
/**
* Write the byte into the given location. All boundary checking and error handling must have
* already been done in the caller such that this is known to be a valid access. Any
* unexpected failure may cause a platform-specific exception that terminates the process!
*
* @param val
* The byte to write.
* @param pos
* The 0-based index position at which to read the byte.
*/
@Override
protected void writeByte(byte val, long pos)
{
value[(int) pos] = val;
}
/**
* Read the bytes at the given location. All boundary checking and error handling must have
* already been done in the caller such that this is known to be a valid access. Any
* unexpected failure may cause a platform-specific exception that terminates the process!
*
* @param pos
* The 0-based index position at which to read the bytes.
* @param len
* The number of bytes to read.
* @param endian
* <code>true</code> to force the returned data to be ordered by the endian-ness of the
* instance.
*
* @return The byte range at that location.
*/
@Override
protected byte[] readByteRange(long pos, long len, boolean endian)
{
// create buffer to contain the data
byte[] data = new byte[(int) len];
// find the ending index; array indexes are limited to 32-bit in the Java Language Spec
int idx = (int) pos;
int end = idx + (int) len;
// copy the data in (may copy nothing if end == pos)
System.arraycopy(this.value, idx, data, 0, end - idx);
return data;
}
/**
* Write the bytes at the given location. All boundary checking and error handling must have
* already been done in the caller such that this is known to be a valid access. Any
* unexpected failure may cause a platform-specific exception that terminates the process!
* <p>
* This implementation is a no-op.
*
* @param data
* The bytes to write.
* @param pos
* The 0-based index position at which to start writing the bytes.
* @param len
* How many bytes should be written. -1 means write everything to the end of the
* array.
* @param endian
* <code>true</code> to honor the endian-ness of the instance. The <code>data</code>
* array MUST be ordered in little-endian order if the endian flag is on.
*/
@Override
protected void writeByteRange(byte[] data, long pos, long len, boolean endian)
{
}
/**
* Write the given byte data into the data managed by this object, overwriting the data
* that are at the given location, if any.
* <p>
* This implementation:
* <ul>
* <li>honors the {@code trim} option (i.e., any existing bytes beyond the end of the
* write are left in place if {@code trim} is {@code false} or truncated if {@code trim}
* is {@code true});</li>
* <li>extends the target size if the write goes past the end of the existing value.</li>
* </ul>
*
* @param overlay
* Flag indicating if the OVERLAY option is used.
* @param data
* Data to be assigned to this object.
* @param offset
* Offset position (in characters) in the target large object.
* @param trim
* {@code true} to truncate any remaining data in the target large object; {@code
* false} to leave remaining data alone.
*/
@Override
public void write(boolean overlay, byte[] data, int offset, boolean trim)
{
if (overlay)
{
/*
TODO: this error is not thrown if the source is longchar, only if is clob
if (!isUnknown() && data != null && data.length > 0 && lengthOf() == 0)
{
ErrorManager.recordOrThrowError(11298, "COPY-LOB could not copy the source large object to the target", false);
return;
}
*/
if (!isUnknown() && data != null && lengthOf() <= offset + data.length)
{
// TODO: error
}
}
int dataLen;
if (data == null || (dataLen = data.length) == 0 && !trim || offset == 0 && value == data)
{
// nothing to write or data already is stored in this object (we don't do a deep equals
// check because it could be expensive and the common case is likely a full assignment
// anyway, so don't take the hit here, just quickly check for object reference equality)
if (!overlay)
{
if (data != null)
{
assign(data);
}
else
{
setUnknown();
}
}
return;
}
int currentLen = value != null ? value.length : 0;
// simple case: just replace existing value with data
if (!overlay || isUnknown() || (offset == 0 && (currentLen <= dataLen || trim)))
{
assign(data);
return;
}
// complex case: overlay data over existing data, possibly extending the length; truncate the array at
// the end of newly copied data if [trim] is true
int endData = offset + dataLen;
int newLen = trim ? endData : Math.max(currentLen, endData);
byte[] buf = new byte[newLen];
if (offset > 0)
{
// copy first portion from existing value into new buffer
System.arraycopy(value, 0, buf, 0, offset);
}
// copy new data to its target offset in new buffer
System.arraycopy(data, 0, buf, offset, dataLen);
if (currentLen > endData)
{
// copy last portion from existing value into new buffer
int remLen = newLen - endData;
System.arraycopy(value, endData, buf, endData, remLen);
}
assign(buf);
}
/**
* Assigns a blob value.
*
* @param value
* Value to assign.
*/
public void assign(blob value)
{
assign((BaseDataType) value);
}
/**
* Sets the {@code FieldReference} reference. This is only used when the object is obtained by a
* dereferenciation ({@code ::}) operation.
*
* @param fr
* The source of this LOB.
*/
@Override
public void setFieldReference(FieldReference fr)
{
this.fRef = fr;
// save current field name
if (fr != null && (fieldName == null || fieldName.isEmpty()))
{
fieldName = fr.getLegacyFieldName();
}
}
/**
* Obtain the {@code FieldReference} reference, if any.
*
* @return The parent {@code FieldReference} from which this LOB was obtained by dereferenciation.
*/
@Override
public FieldReference getFieldReference()
{
return fRef;
}
/**
* Gets the filed name associated with current {@code FieldReference} reference, if any.
*
* @return The field name to be used in LOB file name creation.
*/
public String getFieldName()
{
return fieldName;
}
}