FieldInfo.java

/*
** Module   : FieldInfo.java
** Abstract : Helper class for record buffer copy/compare operations
**
** Copyright (c) 2014-2017, Golden Code Development Corporation.
**
** -#- -I- --Date-- ------------------------------Description------------------------------------
** 001 ECF 20140305 Created initial version.
** 002 ECF 20150909 Added '+' to valid characters; fixed parsing error; added getFullSpec method.
*/
/*
** 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.persist;

import com.goldencode.p2j.util.*;

/**
 * A data structure which stores information about a field, parsed from a field specifier string
 * during construction. See the {@link #FieldInfo(String) constructor} for valid field specifier
 * forms. This is a helper class for buffer copy and compare operations performed by {@link
 * RecordBuffer}.
 */
class FieldInfo
{
   /** Valid, non-alphanumeric characters for a field specifier (beside '.', '[', and ']') */
   private static String VALID_CHARS = "+#$%&-_";
   
   /** Full spec */
   private final String fullSpec;
   
   /** Buffer name (optional) */
   private String bufferName = null;
   
   /** Field name (required) */
   private String fieldName = null;
   
   /** Zero-based extent field index (-1 if not an extent field) */
   private int extentIndex = -1;
   
   /**
    * Constructor which breaks a legacy field specification into its constituent parts. These
    * consist of an optional buffer name, a required field name, and an optional extent value.
    * 
    * @param   fieldSpec
    *          A legacy field specifier. This can take the following forms:
    *          <ul>
    *          <li><code>buffer.field[extent]</code></li>
    *          <li><code>buffer.field</code></li>
    *          <li><code>field[extent]</code></li>
    *          <li><code>field</code></li>
    *          </ul>
    * 
    * @throws  ErrorConditionException
    *          if <code>fieldSpec</code> includes an extent index specifier that is invalid (and
    *          we are not in silent error mode).
    * @throws  IllegalArgumentException
    *          if <code>fieldSpec</code> is otherwise malformed, or we have the extent index
    *          error in silent error mode (which, btw, should not be a valid state).
    */
   FieldInfo(String fieldSpec)
   {
      fullSpec = fieldSpec.toLowerCase();
      StringBuilder bld = new StringBuilder();
      int len = fullSpec.length();
      int openBracketPos = -1;
      for (int i = 0; i < len; i++)
      {
         char c = fullSpec.charAt(i);
         if (c == '.')
         {
            if (bufferName == null)
            {
               if (bld.length() == 0)
               {
                  throw new IllegalArgumentException("Invalid leading dot in field spec");
               }
               
               bufferName = bld.toString();
               bld.setLength(0);
            }
            else
            {
               // quite silly, but evidently the dot is not only a buffer/field delimiter, but
               // also a valid character within a field name
               bld.append(c);
            }
            
            continue;
         }
         
         if (c == '[')
         {
            if (fieldName == null && bld.length() > 0)
            {
               fieldName = bld.toString();
               bld.setLength(0);
               openBracketPos = i;
            }
            else
            {
               // this will create an invalid extent index, which we will handle later, to match
               // Progress' behavior
               bld.append(c);
            }
            
            continue;
         }
         
         if (c == ']')
         {
            if (extentIndex < 0 && bld.length() > 0)
            {
               boolean error = false;
               String extStr = bld.toString();
               bld.setLength(0);
               try
               {
                  int val = Integer.parseInt(extStr);
                  extentIndex = val - 1; // convert from 1-based to 0-based index
                  
                  if (extentIndex < 0)
                  {
                     error = true;
                  }
               }
               catch (NumberFormatException exc)
               {
                  error = true;
               }
               if (error)
               {
                  String spec = (openBracketPos < 0
                                 ? fieldSpec
                                 : fieldSpec.substring(0, openBracketPos));
                  String msg =
                     String.format("Array index %s for %s must be a constant positive integer",
                                   extStr,
                                   spec);
                  ErrorManager.recordOrThrowError(11864, msg, false);
                  
                  throw new IllegalArgumentException(msg);
               }
            }
            
            // Progress silently tolerates and ignores any number of closing brackets after the
            // first closing bracket
            continue;
         }
         
         if (!Character.isLetterOrDigit(c) && VALID_CHARS.indexOf(c) < 0)
         {
            throw new IllegalArgumentException(fieldSpec);
         }
         
         bld.append(c);
      }
      
      if (fieldName == null)
      {
         fieldName = bld.toString();
         bld.setLength(0);
      }
      
      if (fieldName.length() == 0)
      {
         // we should have a field name by now
         throw new IllegalArgumentException(fieldSpec);
      }
      
      if (bld.length() > 0)
      {
         // we had junk characters after we thought we were done parsing all useful information;
         // Progress appears to stuff everything after the buffer into the field name in this case
         // and to let downstream processing handle the errors
         extentIndex = -1;
         if (bufferName == null)
         {
            fieldName = fieldSpec;
         }
         else
         {
            fieldName = fieldSpec.substring(fieldSpec.lastIndexOf('.') + 1);
         }
      }
   }
   
   /**
    * Get the lower case full spec.
    * 
    * @return  Full field spec passed into the constructor, lowercased.
    */
   String getFullSpec()
   {
      return fullSpec;
   }
   
   /**
    * Get lower case buffer name (if any).
    * 
    * @return  Buffer name (may be <code>null</code>).
    */
   String getBufferName()
   {
      return bufferName;
   }
   
   /**
    * Get lower case field name.
    * 
    * @return  Field name.
    */
   String getFieldName()
   {
      return fieldName;
   }
   
   /**
    * Get extent index, if any.
    * 
    * @return  Zero-based extent index, or -1 if no extent spec was provided.
    */
   int getExtentIndex()
   {
      return extentIndex;
   }
}