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);
   }
}