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