clob.java
/*
** Module : clob.java
** Abstract : Data wrapper for SQL CLOB data.
**
** Copyright (c) 2018-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20181214 Created initial version.
** 002 ECF 20190215 Added binaryEquals.
** 003 CA 20190514 Added clob(character) ctor.
** 004 CA 20190530 Added clob(BDT) ctor.
** 005 ECF 20190628 Implemented COPY-LOB runtime.
** 006 CA 20191119 clob initial state is unknown.
** 007 CA 20191203 Fixed overlay and unknown COPY-LOB related bugs.
** 008 OM 20210225 Pushed up binaryEquals() to LargeObject.
** OM 20210312 Added parent FieldReference accessors in case the LOB was obtained by dereferenciation
** of a buffer field.
** OM 20210328 Improved COPY-LOB engine and validations.
** OM 20210404 Reworked codepage management for CLOB fields. These fields should know their CP from the
** moment they are created (before first assign).
** AL2 20220328 Added proxy checks for BDT.
** EVL 20221222 Adding runtime support for LOB fields export.
** CA 20220613 Do not process empty codepage.
** CA 20230207 Field name can be null when transferred via OpenClient calls.
** 009 CA 20230215 'instantiateDefault' now is implemented by each sub-class.
** CA 20230215 'duplicate()' method returns the real type instead of BDT.
** 010 AL2 20250530 Added getIndependentFromContext.
** 011 TG 20250519 Added clob.getCodePage(clob).
*/
/*
** 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 java.io.*;
import java.sql.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.util.*;
/**
* Data wrapper type for SQL CLOB data.
*/
public class clob
extends longchar
{
/** SQL CLOB object */
private Clob sqlClob = null;
/** The parent buffer field where this object was obtained by dereferenciation, if this is the case. */
private FieldReference fRef;
/** Flags this field if it was constructed with a custom CODEPAGE specified in the field definition. */
private boolean customCP = false;
/** The field name used with current field reference. */
private String fieldName = null;
/**
* Default constructor.
*/
public clob()
{
setUnknown();
}
/**
* Copy constructor.
*
* @param value
* Clob to be copied.
*/
public clob(clob value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
if (value.val() instanceof clob)
{
value = (clob) value.val();
}
else
{
assign(value.val());
return;
}
}
if (value.unknown)
{
this.unknown = true;
return;
}
this.unknown = false;
this.value = value.value; // codePageConvert(value.value, value._getCodePage(), _getCodePage());
this.sqlClob = value.sqlClob;
this.fRef = null; //value.fRef
// copy customCP/CP ?
}
/**
* Constructor which is initialized from a database record.
*
* @param sqlClob
* SQL CLOB from which data is read.
*
* @throws SQLException
* if there is an error reading data.
*/
public clob(Clob sqlClob)
throws SQLException
{
this.sqlClob = sqlClob;
// 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 clob(String value)
{
assign(value);
}
/**
* Constructor which initializes an instance from the given data.
*
* @param value
* Data.
* @param codepage
* The codepage.
*/
public clob(String value, String codepage)
{
customCP = (codepage != null && !codepage.isEmpty());
longchar.fixCodePage(this, customCP ? codepage : I18nOps._getCPInternal(), true);
assign(value);
}
/**
* Constructor which initializes an instance from the given data.
*
* @param value
* Data.
*/
public clob(character value)
{
assign(value);
}
/**
* Constructor which initializes an instance from the given data.
*
* @param value
* Data.
*/
public clob(longchar value)
{
assign(value);
}
/**
* Constructor which initializes an instance from the given data.
*
* @param value
* Data.
*/
public clob(BaseDataType value)
{
assign(value);
}
/**
* Get the type.
*
* @return type.
*/
public Type getType()
{
return Type.CLOB;
}
/**
* Read data from the SQL CLOB 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;
}
String s = null;
if (sqlClob != null)
{
int len = (int) sqlClob.length();
if (len > 0)
{
s = sqlClob.getSubString(1, len);
}
else if (len == 0)
{
s = "";
}
sqlClob.free();
sqlClob = null;
}
assign(s);
}
/**
* Return the value codepage fixed status.
*
* @return Always {@code true}. The codepage for {@code clob}-s cannot be changed.
*/
public boolean isCodePageFixed()
{
return true;
}
/**
* Implementation of {@code IS-COLUMN-CODEPAGE} ABL function.
*
* @param value
* The data to be tested.
*
* @return {@code YES} only when {@code value} is a {@code CLOB} field defined with
* {@code IS-COLUMN-CODEPAGE} attribute.
*/
public static logical isColumnCodepage(BaseDataType value)
{
if (value == null)
{
return new logical(); // or err 5729 ?
}
if (value instanceof clob)
{
return new logical(((clob) value).customCP);
}
ErrorManager.recordOrThrowError(5729);
// Incompatible datatypes found during runtime conversion. (5729)
return new logical();
}
/**
* Returns the code page of a the specified clob variable.
*
* @param value
* The value for which the codepage will be retrieved.
*
* @return See above
*/
public static character getCodePage(clob value)
{
return value.getCodePage();
}
/**
* @see com.goldencode.p2j.util.BaseDataType#duplicate()
*
* This class is not undoable, so return a reference to this object.
*
* @return This object.
*/
@Override
public clob 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()
{
clob clb = new clob();
clb.setUnknown();
return clb;
}
/**
* 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 instantiateUnknown();
}
/**
* Write the given character 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 characters 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. Ignored.
*/
@Override
public void write(boolean overlay, String data, int offset, boolean trim)
{
// if we are in overlay mode, fill with 'offset - 1' spaces to the right
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 (data != null && (value == null || offset >= value.length()))
{
int rightFill = offset - (value == null ? 0 : value.length());
assign(StringHelper.rightAlignText("", rightFill));
}
}
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)))
{
assign(data);
return;
}
// complex case: overlay data over existing data, possibly extending the length
int endData = offset + dataLen;
int newLen = trim ? endData : Math.max(currentLen, endData);
char[] buf = new char[newLen];
if (offset > 0)
{
// copy first portion from existing value into new buffer
value.getChars(0, offset, buf, 0);
}
// copy new data to its target offset in new buffer
data.getChars(0, dataLen, buf, offset);
if (newLen > endData)
{
// copy last portion from existing value into new buffer
value.getChars(endData, currentLen, buf, endData);
}
assign(new String(buf));
}
/**
* 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;
}
/**
* 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);
out.writeObject(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);
fieldName = (String) in.readObject();
}
/**
* Assigns the value represented by {@code value} to this object. If needed (the source {@code value} has
* a different codepage), a codepage conversion is performed.
*
* @param value
* The source {@code Text} to be assigned.
*/
@Override
public void assign(Text value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
assign(value.val());
return;
}
super.assign(TextOps.codePageConvert(value, _getCodePage()));
}
/**
* Assigns the value represented by {@code value} to this object. Since all {@code clob} fields have a
* fixed codepage, a codepage conversion will occur.
*
* @param value
* The source {@code longchar} / {@code clob} to be assigned.
*/
@Override
public void assign(longchar value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
assign(value.val());
return;
}
if (value.isUnknown())
{
setUnknown();
}
else
{
super.assign(TextOps.codePageConvert(value, _getCodePage()));
}
}
/**
* Creates a string representation of the instance data using the 'export'
* format. For the character data type the entire string is returned
* enclosed in a pair of double quotes. If the instance represents the
* <code>unknown value</code>, a '?' will be returned. Any instances of
* double quotes inside the string will be doubled up on output.
*
* @return The 'export' formatted string.
*/
@Override
public String toStringExport()
{
return isUnknown() ? "?" : value == null ? "" : value;
}
/**
* Identify if this data is dependent upon the context local. This identifies
* if the data is safe to be cached in cross-session places or the data should
* not leak from the context. If is is not safe to be shared cross-session, then
* this should return {@code null}. If other representation is suitable for caching,
* then this can return another object that wraps internal values that are not
* dependent upon the context (e.g. date, date-time, date-time-tz).
*
* @return Always. {@code null}.
*/
@Override
public Object getIndependentFromContext()
{
return null;
}
}