ResourceIdHelper.java
/*
** Module : ResourceIdHelper.java
** Abstract : Facility to encapsulate semantics for Progress resource identifiers.
**
** Copyright (c) 2013-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description---------------------------------------
** 001 HC 20131030 Created initial version, see issue #2183.
** 002 CA 20131219 Negative resource IDs must not be generated, as they are not valid resource
** IDs in 4GL (thus a idFromString call may fail if resource ID is negative, even
** if it is a real ID).
** 003 GES 20140115 Documentation changes, added comments and added a small feature to
** idFromString().
** 004 CA 20190729 Changed nextId() to generate a 32-bit value - this is because of real-life
** application code expecting these values to be 32-bit.
** 005 ECF 20200924 Performance improvement: attempt simple, fast parse of resource ID string before using
** slower method.
** 006 GES 20200927 Added the definition for an invalid resource ID (0).
** CA 20210306 nextId() now computes always-incremental values, given a certain value. Random IDs are
** left only for com-handle resources.
** TJD 20220504 Java 11 compatibility minor changes
*/
/*
** 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.math.BigInteger;
import java.security.SecureRandom;
/**
* The class encapsulates semantics related to Progress resource identifiers,
* see {@link handle#resourceId(WrappedResource)}.
* <p>
* The main areas of functionality implemented by this class include allocations
* of new resource identifiers, string formatting and parsing.
* <p>
* A resource identifier takes two forms, numeric and textual. The numeric form is
* represented by {@link Long} data type.
* <p>
* Internally, P2J can avoid the string representation of the resource ID and use it as a long
* value. For this reason, the {@link #getNext()} API will never generate negative resource IDs,
* as these are not valid legacy resource IDs (as calling {@link #idFromString} with a negative
* resource ID will fail).
*/
public class ResourceIdHelper
{
/** Constant denoting the unknown resource id */
public static final Long UNKNOWN = null;
/** Constant denoting the unknown resource id string */
public static final String UNKNOWN_STRING = "?";
/** Constant denoting the zero resource id */
public static final Long ZERO = 0L;
/** Invalid resource id. */
public static final long INVALID_RESOURCE = 0L;
/** The default format string for a resource identifier (WARNING: not compatible with Progress). */
public static final String DEFAULT_FORMAT = ">>>>>>>>>>>>>>>>>>>9";
/**
* We are using Secure random number generator so the generated values
* are uniformly distributed in the whole 64-bit range.
*/
private static final SecureRandom rnd = new SecureRandom();
/** Max long constant */
private static final BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE);
/**
* Generates a valid resource identifier.
* <p>
* The returned value is a randomly generated positive 32bit number with no guarantee of
* uniqueness. It is the responsibility of the caller to check uniqueness of the returned
* value in the caller's context if required.
* <p>
* There is real-life production code which relies on the handle resource ID to be a 32-bit
* value; this is because the IDs start from small values, and they are generated incrementally.
* Because of this dependency, and until we need real 64-bit IDs, this method will always
* return a positive 32-bit value.
*/
public static Long getNext()
{
int num = 0;
// zero doesn't denote a valid id
while (num == 0)
{
num = rnd.nextInt();
}
// do not generate negative resource IDs.
if (num < 0)
{
num = num + Integer.MAX_VALUE;
}
return Long.valueOf(num);
}
/**
* Generates a valid resource identifier.
* <p>
* The returned value is a always incremental, and avoids negative or zero values.
*
* @param start
* The current resource ID index.
*
* @return See above.
*/
public static Long getNext(long start)
{
long num = start + 1;
if (num < 0)
{
num = num + Long.MAX_VALUE;
}
if (num == 0)
{
num = num + 1;
}
return num;
}
/**
* Returns <code>true</code> if the supplied identifier represents
* a zero resource identifier, <code>false</code> otherwise.
*
* @param resourceId
* Resource identifier.
*/
public static boolean isZero(Long resourceId)
{
return ZERO.equals(resourceId);
}
/**
* Returns <code>true</code> if the supplied identifier represents
* an unknown resource identifier, <code>false</code> otherwise.
*
* @param resourceId
* Resource identifier.
*
*/
public static boolean isUnknown(Long resourceId)
{
return UNKNOWN == resourceId;
}
/**
* Parses a resource id from its textual form into its numeric form. The number is parsed into
* a 64-bit integer using Progress conventions, see {@link NumberType#parseDouble}.
* <p>
* The method returns {@code UNKNOWN} if the input is {@code null} or equal to "?" or
* the method returns {@code ZERO} if the input contains a trimmed empty string or
* the method returns {@code ZERO} if the parsed input is zero or less than zero or
* the method throws {@link ErrorConditionException} if the input cannot be parsed to
* an integer.
*
* @param resourceId
* A string value representing a resource identifier.
*
* @return A numeric resource id.
*
* @throws ErrorConditionException
* If the input cannot be parsed into an integer, see method description
* for more details.
*/
public static Long idFromString(String resourceId)
throws ErrorConditionException
{
if (resourceId == null || resourceId.equals(UNKNOWN_STRING))
{
return UNKNOWN;
}
// we don't know why length > 255 is a special case in the 4GL, but it is
int len = resourceId.length();
if (len == 0 || len > 255)
{
return ZERO;
}
// try fast parse first; simplifying assumption vs Long.parseLong() is that string contains only
// digits, nothing else
long val = 0L;
int x = -1;
for (int i = 0, e = len - 1; i < len; i++, e--)
{
char c = resourceId.charAt(i);
if (c >= '0' && c <='9')
{
x = c - '0';
}
else
{
x = -1;
break;
}
val = val * 10 + x;
if (val < 0L)
{
// overflow, bail
ErrorManager.recordOrThrowError(78, "Value too large for integer");
return null;
}
}
if (x >= 0)
{
// if x is a non-negative integer, parsing finished successfully
return val;
}
// if we've gotten here, the fast parse did not work because we encountered a non-digit character,
// so now we have to do it the slower, more complete way
// TODO: report two errors: "** Decimal number is too large. (536)" and
// "** Value too large for integer. (78)"
// when the string exceeds 50 significant digits (i.e. not including numsign,
// white spaces, leading zeros, etc.).
// UPDATE: this would probably be obtained "for free" by utilizing a code path that
// runs through int64.setValue(String) which already has this same 2 error
// (536 then 78) behavior; parseDecimal() already does throw 536 and the
// int64.setValue() uses parseDecimal() and then if it gets a 536 it raises
// the 78 error; converted code tests should be run to confirm
// normalize the string with Progress conventions
String javaResourceId = NumberType.parseDecimal(resourceId, true);
BigInteger num = new BigInteger(javaResourceId);
// we only consider positive integers valid resource ids. this is safe because P2J will
// never generate a negative resource ID
if (num.signum() <= 0)
{
return ZERO;
}
// out of range of positive long?
if (num.compareTo(MAX_LONG) > 0)
{
ErrorManager.recordOrThrowError(78, "Value too large for integer");
return null;
}
return num.longValue();
}
/**
* Formats the numeric resource identifier with the default format string,
* {@link ResourceIdHelper#DEFAULT_FORMAT}.
* See {@link #idToString(String, Long)} for more details.
*
* @param resourceId
* Resource identifier.
*
* @return the formatted resource identifier.
*/
public static String idToString(Long resourceId)
{
return idToString(null, resourceId);
}
/**
* Formats the numeric resource identifier with the supplied format string.
* If this method receives a negative resource ID, an exception is thrown. Internally, P2J will
* never use negative IDs.
*
* @param format
* Format string.
* @param resourceId
* Resource identifier.
*
* @return the formatted resource identifier.
*
* @throws IllegalArgumentException
* If the resource ID is negative.
*/
public static String idToString(String format, Long resourceId)
{
if (isUnknown(resourceId))
{
return UNKNOWN_STRING;
}
BigInteger num = BigInteger.valueOf(resourceId);
if (num.signum() < 0)
{
throw new IllegalArgumentException("The resource ID can not be negative!");
}
return NumberType.toString(num.toString(), "", false, format == null ? DEFAULT_FORMAT : format, false);
}
}