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