rowid.java
/*
** Module : rowid.java
** Abstract : Progress 4GL compatible rowid object
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ------------------Description-------------------
** 001 ECF 20080721 @39290 Created initial version. Represents the
** Progress rowid data type. Much of the
** implementation was borrowed from the handle
** type.
** 002 ECF 20090212 @41302 Implemented a faster equals(). Handles most
** common cases without having to invoke the
** superclass' implementation.
** 003 CA 20130123 Added c'tor which builds a new instance from a
** BaseDataType value, if possible. This should be
** used only when constructing an instance using the
** value returned by a DYNAMIC-FUNCTION call; 4GL
** does some special casting in this case, which is
** not implemented yet.
** 004 OM 20121023 Added/fixed support for TO-ROWID(STRING(ROWID)).
** 005 CS 20130315 Made parseRowidString public.
** 006 GES 20130322 Fixed the BDT c'tor and modified the assign(BDT,boolean) to be the
** common location for the _POLY morphing. This is safe because all
** current usage would have generated a ClassCastException if that
** method was ever used with an invalid type. Now, instead of the CCE,
** the code will morph the value as is possible. The morphing code
** still needs to be implemented.
** 007 OM 20130416 Cosmetic change: removed '.' at the end of error message.
** 008 MAG 20140602 Fixed the BDT c'tor and modified the assign(BDT,boolean) on _POLY morphing.
** Add BDT types on compareTo(Object).
** 009 ECF 20151209 Removed bitwise parity check and converted characters to lowercase
** when parsing rowid strings.
** 010 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 011 GES 20160617 Added unknown literal processing for the polymorphic assign case.
** 012 CA 20160627 Reworked undoable support - the undoables register themselves with
** all blocks up the stack, until either 1. the tx block which created
** it is reached or 2. the last tx block where it was saved is reached.
** 013 GES 20171206 Removed explicit bypass on pending error (new silent error mode).
** 014 GES 20180420 Implemented the backing support for serialization because it is
** needed for RAW-TRANSFER.
** 015 OM 20190206 Added implementation of field size needed by record-length.
** 016 IAS 20201007 Added Type enum
** 017 VVT 20210331 parseRowidString(String) fixed: does not throw anymore;
** toString(String fmt) fixed: now always emit all 16 digits. See #5218.
** ME 20210504 Added character/longchar value assign (poly conversion).
** CA 20210609 Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly
** initialized at the method's execution, and not at the caller's arguments.
** AL2 20220319 Added value proxy check in assign.
** 018 CA 20230215 'instantiateDefault' now is implemented by each sub-class.
** CA 20230215 'duplicate()' method returns the real type instead of BDT.
*/
/*
** 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.util.regex.*;
/**
* A class that represents a Progress 4GL compatible rowid data type.
* Internally, the row identifier is stored as a 64-bit integer.
*/
public class rowid
extends BaseDataType
{
/** Stores the integral row identifier, unique within a database */
private Long value = null;
/** Rowid valid string representation regex pattern. */
private final static Pattern RowIdPattern = Pattern.compile("0x[0]*([0-9a-f]{1,})");
/**
* Worker method that parses and validates a <code>java.lang.String</code>
* according to rowid Progress data type constraints:
* <ul>
* <li>must start with "0x" prefix
* <li>must only contain lower-case hexadecimal digits
* <li>the number of digits must be more than 2 and even
* <li>the parsed value must fit into a 64-bit unsigned integer (this is the FWD limitation,
* Progress accepts any number of digits.)
* </ul>
*
* @param value
* A hex string matching the conditions above.
* Any other string, including <code>null</code>, will
* indicate the unknown value.
*
* @return A <code>Long</code> representing the internal value for a
* rowid or <code>null</code> if parsing fails.
*/
public static Long parseRowidString(String value)
{
// 4gl requires the total number of digits be even
if (value == null || (value.length() % 2 == 1))
{
return null;
}
final Matcher matcher = RowIdPattern.matcher(value);
if (!matcher.matches())
{
return null;
}
// Get the [start,end) of actual digits without the leading zeroes
// (which were skipped by the pattern).
final int start = matcher.start(1);
final int end = matcher.end(1);
// Assert we will not get integer overflow
if ((end - start) > 16)
{
return null;
}
// Parse the number
long result = 0;
for (int i = start; i < end; i++)
{
final char c = value.charAt(i);
result = result * 16 + (c <= '9' ? c - '0' : (c - 'a' + 10));
}
return Long.valueOf(result);
}
/**
* Default constructor, creates an instance that represents the unknown
* value.
*/
public rowid()
{
}
/**
* This is a special c'tor which should be used only when converting the
* value returned by a function or method with polymorphic return type into the
* expected type (i.e. DYNAMIC-FUNCTION()). In such cases, the 4GL does some
* automatic type conversion (see {@link #assign(BaseDataType)}).
*
* @param value
* The value to be used for this instance.
*/
public rowid(BaseDataType value)
{
initialize(value);
}
/**
* Constructs an instance based upon an existing instance.
*
* @param value
* The instance to copy.
*/
public rowid(rowid value)
{
assign(value);
}
/**
* Constructs an instance given a 64-bit integer value.
*
* @param value
* 64-bit integer value; should be unique across all records in a
* database. May be <code>null</code> to indicate unknown value.
*/
public rowid(Long value)
{
this.value = value;
}
/**
* Constructs an instance given a character encoding (hex string in the
* format 0xhhhhhhhhhhhhhhhh, where each h is an lower-case, hexadecimal
* digit, limited to maximum 16 digits because that actual internal
* implementation uses a java 64-bit Long).
*
* @param val
* A hex string in the format 0xhhhhhhhhhhhhhhhh.
* Any other string will, including <code>null</code> will
* indicate the unknown value.
*/
public rowid(character val)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(val))
{
if (val.val() instanceof character)
{
val = (character) val.val();
}
else
{
initialize(val);
return;
}
}
// trivial cases:
if (val == null || val.isUnknown())
{
this.value = null;
return;
}
this.value = parseRowidString(val.toJavaType());
}
/**
* Constructs an instance given a character encoding (hex string in the
* format 0xhhhhhhhhhhhhhhhh, where each h is an lower-case, hexadecimal
* digit, limited to maximum 16 digits because that actual internal
* implementation uses a java 64-bit Long).
*
* @param val
* A hex string in the format 0xhhhhhhhhhhhhhhhh.
* Any other string will, including <code>null</code> will
* indicate the unknown value.
*/
public rowid(String val)
{
this.value = parseRowidString(val);
}
/**
* Creates a new instance of the same type that represents the
* <code>unknown value</code>.
*
* @return An instance that represents the <code>unknown value</code>.
*/
public BaseDataType instantiateUnknown()
{
return new rowid();
}
/**
* 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 rowid();
}
/**
* Does the same as standard <code>clone()</code> method but returns an
* instance of <code>BaseDataType</code> and doesn't throw the
* <code>CloneNotSupportedException</code>.
*
* @return A clone of this instance.
*/
public rowid duplicate()
{
return (new rowid(this));
}
/**
* Get the type
* @return type
*/
public Type getType()
{
return Type.ROWID;
}
/**
* An equality test which handles the most common cases with the least
* amount of overhead. The superclass implementation is invoked otherwise.
* <p>
* This implementation is consistent with {@link #hashCode()}.
*
* @see com.goldencode.p2j.util.BaseDataType#equals(java.lang.Object)
*/
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o instanceof rowid)
{
rowid that = (rowid) o;
boolean u1 = (this.value == null);
boolean u2 = (that.value == null);
if (u1 && u2)
{
return true;
}
if (u1 || u2)
{
return false;
}
return (this.value.equals(that.value));
}
return super.equals(o);
}
/**
* Hash code implementation which is consistent with {@link
* BaseDataType#equals}.
*
* @return Hash code value for this object instance. All instances of
* this class return the same hash code value, since all
* instances are inherently equal.
*/
public int hashCode()
{
int result = 17;
if (value != null)
{
result = 37 * result + value.hashCode();
}
return result;
}
/**
* Returns the value of this instance as an <code>Long</code>.
* <p>
* If this instance represents the <code>unknown value</code>, a
* <code>null</code> will be returned.
*
* @return The value as a <code>Long</code> or <code>null</code> if
* this is <code>unknown</code>.
*/
public Long getValue()
{
return value;
}
/**
* Determines if this instance represents the <code>unknown value</code>.
*
* @return <code>true</code> if this instance is set to the
* <code>unknown value</code>.
*/
public boolean isUnknown()
{
return (value == null);
}
/**
* Sets this instance to represent the unknown value.
* <p>
* Any existing value is discarded.
*/
public void setUnknown()
{
if (value != null)
{
checkUndoable(true);
}
value = null;
}
/**
* Sets the data of this instance based on the given instance.
* <p>
* If the value is not of type <code>rowid</code>, the following automatic type
* conversion will occur:
* <p>
* <pre>
* This type support only rowid and unknown types (including unknown literal).
* </pre>
* <p>
* This variant is meant to handle the cases of built-in functions and methods in the 4GL
* which have polymorphic return types (e.g. DYNAMIC-FUNCTION()). This should NOT be used for
* non-polymorphic assignments.
*
* @param value
* The instance from which to copy state.
*
* @throws ClassCastException
* if <code>value</code> is not an instance of this class.
*/
public void assign(BaseDataType value)
{
if (value == null)
{
setUnknown();
return;
}
// the value may be a proxy; make sure to unwrap and work with the real value
value = value.val();
if (value.isUnknown())
{
setUnknown();
}
else if (value instanceof rowid)
{
assign((rowid) value);
}
else if (value instanceof Text)
{
assign((Text) value);
}
else
{
// conversion impossible
incompatibleTypesOnConversion();
}
}
/**
* Sets the data of this instance based on the given instance.
*
* @param value
* The instance from which to copy state.
*/
public void assign(rowid 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;
}
checkUndoable(value);
this.value = value.value;
}
/**
* Sets the data of this instance based on the given instance.
*
* @param value
* The instance from which to copy state.
*/
public void assign(Text value)
{
checkUndoable(value);
this.value = parseRowidString(value.toJavaType());
}
/**
* Sets the state (data and unknown value) of this instance based on the
* state of the passed instance.
*
* @param value
* The instance from which to copy state.
*/
public void assign(Undoable value)
{
assign((rowid) value);
}
/**
* Compares this instance with the specified instance and returns a -1
* if this instance is less than the specified, 0 if the two instances
* are equal and 1 if this instance is greater than the specified
* instance. This is the implementation of the <code>Comparable</code>
* interface.
* <p>
* The rowids' internal, 64-bit integer values are compared.
*
* @param obj
* The instance to compare against.
*
* @return 0 or 1 depending on if the passed instance is or is not
* <code>unknown</code>.
*/
public int compareTo(Object obj)
{
rowid that = null;
if (obj instanceof rowid)
{
that = (rowid) obj;
}
else if (obj instanceof BaseDataType)
{
that = new rowid((BaseDataType) obj);
}
else
{
return -1;
}
Long l1 = this.value;
Long l2 = that.value;
if (l1 == l2)
{
return 0;
}
else if (l1 == null)
{
return 1;
}
else if (l2 == null)
{
return -1;
}
return l1.compareTo(l2);
}
/**
* Creates a string representation of the instance data as a hex string
* with a leading "0x" prefix.
* <p>
* If the instance represents the <code>unknown value</code>, a '?' will be
* returned.
*
* @return The formatted string.
*/
public String toString()
{
return toString(null);
}
/**
* Creates a string representation of the instance data as a hex string
* with a leading "0x" prefix.
* <p>
* If the instance represents the <code>unknown value</code>, a '?' will be
* returned.
*
* @param fmt
* The Progress 4GL format string or <code>null</code> if the
* default format is to be used. This parameter is not validated and
* is ignored.
*
* @return The formatted string.
*/
public String toString(String fmt)
{
if (isUnknown())
{
return "?";
}
return String.format("0x%016x", value);
}
/**
* Creates a string representation of the instance data in a form that is
* compatible with the <code>MESSAGE</code> language statement. If the
* instance represents the <code>unknown value</code>, a '?' will be
* returned.
*
* @return The 'message' formatted string.
*/
public String toStringMessage()
{
return toString(null);
}
/**
* 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.
*/
public String toStringExport()
{
return toString(null);
}
/**
* Return the default display format string for this type. This
* implementation returns <code>null</code>, because there is no formatted
* representation of a rowid: it must be converted to a string first using
* the Progress STRING() function.
*
* @return <code>null</code>.
*/
public String defaultFormatString()
{
return null;
}
/**
* Calculate the length of a formatted value (the text form of the value)
* using this format string.
*
* @param fmt
* The format string.
*
* @return The length in characters of the any resulting formatted
* value.
*/
public int formatLength(String fmt)
{
return 18; // an 8-byte hex string + leading '0x' (0x0123456789ABCDEF)
}
/**
* Obtain the length (in bytes) of this BDT will use when serialized.
*
* @return the length of this BDT will use when serialized.
*/
@Override
public int getSize()
{
return isUnknown() ? 1 : 9;
}
/**
* 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.
* @throws NotSerializableException
* always.
*/
public void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
boolean unknown = in.readBoolean();
if (!unknown)
{
value = in.readLong();
}
}
/**
* 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.
* @throws NotSerializableException
* always.
*/
public void writeExternal(ObjectOutput out)
throws IOException
{
boolean unknown = isUnknown();
out.writeBoolean(unknown);
if (!unknown)
{
out.writeLong(value);
}
}
@Override
protected boolean isIncompatibleTypesOnConversion(BaseDataType value)
{
// invalid conversion is being thrown even if unknown
if (value instanceof clob || value instanceof blob || value instanceof longchar)
{
incompatibleTypesOnConversion();
}
return super.isIncompatibleTypesOnConversion(value);
}
}