CharacterFormatParser.java

/*
** Module   : CharacterFormatParser.java
** Abstract : Base class for Progress character format parsers/formatters.
**
** Copyright (c) 2019-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 VVT 20210408 Created initial version.
**     VVT 20210427 API changed: the format is not passed to constructor anymore.
**     VVT 20210917 Javadoc fixed. 
*/
/*
** 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 com.goldencode.p2j.ui.client.format.DisplayFormat;

/**
 * Base class for Progress character format parsers/formatters.
 * 
 * This class provides the main {@link #parse(String)} method along with a set of callback methods,
 * each callback is called when a special character of the corresponding type is parsed.
 * 
 * The {@link #parse(String)} method is also responsible for checking the
 * Progress character format validity.
 * 
 * By default, all callbacks do nothing, they are expected to be re-defined in subclasses.
 * 
 * This class may be used directly for:
 * <p>
 * <li>Checking the character format string for validity by calling the {@link #parse(String)} method. 
 * <li>Getting the maximum length of a formatted string, using the
 * {@link #parse(String)} method return value.
 *
 */
public class CharacterFormatParser
{

   /** The state constants for the Progress format string parser. */
   private static enum ParseState
   {
      /** Default parse state */
      DEFAULT,

      /** Next character is an escaped character */
      ESCAPE,

      /** Last character was an opening parenthesis. */
      LPAREN,

      /** We are parsing a repeat count */
      DUPES
   }

   /**
    * Do the parse, call all 'visitXXX' callback methods.
    * 
    * @param format
    *        the format string to parse 
    * 
    * @return the maximum length of formatted value.
    */
   public final int parse(final String format)
   {
      final int pflen = format.length();
      if (pflen == 0)
      {
         characterFormatIsIncomplete(format);
      }

      int maxPresLength = 0;
      int formatDupes = 0;

      ParseState pstate = ParseState.DEFAULT;

      // Deferred action
      DeferredAction action = null;

      for (int pfi = 0; pfi < pflen; pfi++)
      {
         final char ch = format.charAt(pfi);

         switch (pstate)
         {
            case DEFAULT:
               if (action != null)
               {
                  if (ch == '(')
                  {
                     pstate = ParseState.DUPES;
                     break;
                  }

                  // No repeat counter, just execute and drop the action.
                  action.exec(1);
                  action = null;
               }

               if (ch == '~')
               {
                  visitEscape();
                  pstate = ParseState.ESCAPE;
                  break;
               }

               ++maxPresLength;
               action = getActionForACharacter(ch, maxPresLength, format);
               break;

            case ESCAPE:
               // Insert the escaped character and return to default mode.
               visitFillChar(ch);
               ++maxPresLength;
               pstate = ParseState.DEFAULT;
               break;

            case LPAREN:
               // At least one digit is expected after an opening parenthesis
               formatDupes = Character.digit(ch, 10);
               if (formatDupes < 0)
               {
                  ErrorManager.genInvalidCharError(pfi, format);
               }

               pstate = ParseState.DUPES;
               break;

            case DUPES:
               if (ch == ')')
               {
                  // Note we are incremented the max presentation length
                  // once, so we must decrement it back here
                  maxPresLength += formatDupes - 1;
                  
                  // The maximum format length must be upper-limited by the 4gl character maximum
                  // length
                  if (formatDupes == 0 || maxPresLength > 32000)
                  {
                     genValueCannotBeDisplayedError(format);
                  }

                  action.exec(formatDupes);
                  action = null;
                  formatDupes = 0;
                  pstate = ParseState.DEFAULT;
                  break;
               }

               // Only digits are allowed between the parenthesizes
               final int digit = Character.digit(ch, 10);
               if (digit < 0)
               {
                  ErrorManager.genInvalidCharError(pfi, format);
               }

               formatDupes = formatDupes * 10 + digit;
               break;
         }
      }

      // Check the total format length
      if (maxPresLength > 32000)
      {
         genValueCannotBeDisplayedError(format);
      }

      // Format string parsed. Checking for invalid final states
      switch (pstate)
      {
         case DEFAULT:
            break;

         case ESCAPE:
            // Note: the escape character at the end of otherwise non-empty format
            // string is allowed and silently ignored in 4gl
            if (maxPresLength == 0)
            {
               genValueCannotBeDisplayedError(format);
            }
            break;

         case DUPES:
         case LPAREN:
            characterFormatIsIncomplete(format);
            break;
      }

      return maxPresLength;
   }

   /**
    * This method is called by {@link #parse(String)} for each occurrence of
    * the escape character '~' in format string.
    * 
    * By default this method does nothing.
    */
   protected void visitEscape()
   {
      // no-op
   }

   /**
    * This method is called by {@link #parse(String)} for each occurrence of
    * a non-special character in format string.
    * 
    * By default this method does nothing.
    * 
    * @param ch
    *        the format character
    * 
    */
   protected void visitFillChar(final char ch)
   {
      // no-op
   }

   /**
    * This method is called by {@link #parse(String)} for each occurrence of
    * the '9' (the digit) special character in format string.
    * 
    * By default this method does nothing.
    * 
    * @param repeatCount
    *        the special character repeat count
    */
   protected void visitDigit(final int repeatCount)
   {
      // no-op
   }

   /**
    * This method is called by {@link #parse(String)} for each occurrence of
    * the '!' (upcase) special character in format string.
    * 
    * By default this method does nothing.
    * 
    * @param repeatCount
    *        the special character repeat count
    */
   protected void visitUpperLetter(final int repeatCount)
   {
      // no-op
   }

   /**
    * This method is called by {@link #parse(String)} for each occurrence of
    * the 'a' (letter) special character in format string.
    * 
    * By default this method does nothing.
    * 
    * @param repeatCount
    *        the special character repeat count
    * 
    */
   protected void visitLetter(final int repeatCount)
   {
      // no-op
   }

   /**
    * This method is called by {@link #parse(String)} for each occurrence of
    * the 'n' (alphanumeric) special character in format string.
    * 
    * By default this method does nothing.
    * 
    * @param repeatCount
    *        the special character repeat count
    */
   protected void visitAlphanum(final int repeatCount)
   {
      // no-op
   }

   /**
    * This method is called by {@link #parse(String)} for each occurrence of
    * the 'x' (any character) special character in format string.
    * 
    * By default this method does nothing.
    * 
    * @param repeatCount
    *        the special character repeat count 
    */
   protected void visitAnyChar(final int repeatCount)
   {
      // no-op
   }
   
   /**
    * Helper method to the {@link
    * DisplayFormat#genValueCannotBeDisplayedError(String, String)}
    * 
    * @param format
    *        the offending format string
    */
   private static void genValueCannotBeDisplayedError(final String format)
   {
      DisplayFormat.genValueCannotBeDisplayedError("", format);
   }

   /**
    * Helper method to process the error 164 error ("incomplete format string").
    * 
    * @param format
    *        the format string.
    */
   private final static void characterFormatIsIncomplete(String format)
   {
      ErrorManager.recordOrThrowError(164,
               String.format("Character format %s is incomplete", format), true);
   }

   /**
    * Check character position and map a character to an action.
    * For a special formatting character, return the corresponding action.
    * <p>
    * Otherwise execute the {@link #visitFillChar(char)}, passing the character as the argument,
    * and return {@code null}.
    * 
    * @param ch
    *        the character to dispatch
    * @param position
    *        the character position to check against the maximum length of 4gl character
    * @param fmt
    *        the currently parsed format for error reporting.
    * 
    * @return {@code true} if the character provided is a special formatting character
    *  
    */
   private DeferredAction getActionForACharacter(char ch, final int position, final String fmt)
   {
      // The 4gl character max size. 
      if (position >= 32000)
      {
         genValueCannotBeDisplayedError(fmt);
      }

      switch (ch)
      {
         case 'X':
         case 'x':
            return n -> visitAnyChar(n);
         case 'N':
         case 'n':
            return n -> visitAlphanum(n);
         case 'A':
         case 'a':
            return n -> visitLetter(n);
         case '!':
            return n -> visitUpperLetter(n);
         case '9':
            return n -> visitDigit(n);
         default:
            visitFillChar(ch);
            return null;
      }
   }

  /**
    * Interface for all deferred actions.
    *
    */
   private interface DeferredAction
   {
      /**
       * Execute the action several times
       * 
       * @param repeatCount
       *        the repeat count
       */
      void exec(final int repeatCount);
   }
}