# BufferCompare.java

    /*
    ** Module   : BufferCompare.java
    ** Abstract : A configurable buffer comparison operation.
    **
    ** Copyright (c) 2019-2021, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20190208 Created initial version to simplify API for BUFFER-COMPARE operations.
    ** 002 OM  20210225 Fixed Mode.fromString() method. Avoid NPE in run().
    */

    /*
    ** 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 java.util.*;
    import com.goldencode.p2j.util.*;

    /**
     * Object which performs a buffer comparison operation. All configuration methods return the
     * current object instance, to allow method chaining.
     *
     * TODO: add support for EXPLICIT COMPARES expressions.
     */
    public class BufferCompare
    {
       /** Comparison mode types */
       public static enum Mode
       {
          /** The default mode, aka "case-neutral". */
          DEFAULT("case-neutral"),

          /** The case-sensitive mode. */
          CASE_SENSITIVE("case-sensitive"),

          /** The binary compare mode. */
          BINARY("binary");

          /** Internal name */
          private final String name;

          /**
           * Constructor.
           *
           * @param   name
           *          Internal name.
           */
          Mode(String name)
          {
             this.name = name;
          }

          /**
           * Helper method to determine mode from a string.
           *
           * @param   modeString
           *          Text representation of a buffer compare mode.
           *
           * @return  Appropriate mode constant for the given string.
           *
           * @throws  ErrorConditionException
           *          if the given string doesn't match a constant.
           */
          static Mode fromString(character modeString)
          {
             if (modeString == null || modeString.isUnknown())
             {
                return DEFAULT;
             }

             switch (modeString.getValue().trim().toLowerCase())
             {
                case "case-sensitive":
                   return CASE_SENSITIVE;

                case "binary":
                   return BINARY;

                default:
                   // no error is generated if invalid compare mode is passed
                   return DEFAULT;
             }
          }

          /**
           * Obtain a short description of this compare mode.
           *
           * @return  a short description of this compare mode.
           */
          @Override
          public String toString()
          {
             return name;
          }
       }

       /** "Left" object of the comparison */
       private final DataModelObject dmo1;

       /** "Right" object of the comparison */
       private final DataModelObject dmo2;

       /** {@code true} if {@code fields} should be excluded; {@code false} for them to be included */
       private boolean exclusive = true;

       /** {@code true} to compare all fields; {@code false} to halt at the first difference */
       private boolean exhaustive = false;

       /** Optional field names to include or exclude from comparison */
       private String[] fields = null;

       /** Comparison mode */
       private Mode mode = Mode.DEFAULT;

       /** Delegate which sets the results of the comparison into a results object */
       private Accessor resultDelegate = null;

       /** {@code true} to skip any LOBs when performing comparison; {@code false} to include them */
       private boolean noLobs = false;

       /**
        * Constructor which accepts DMOs to compare.
        *
        * @param   dmo1
        *          "Left" object of the comparison.
        * @param   dmo2
        *          "Right" object of the comparison.
        */
       public BufferCompare(DataModelObject dmo1, DataModelObject dmo2)
       {
          this.dmo1 = dmo1;
          this.dmo2 = dmo2;
       }

       /**
        * Exclude the given fields from the comparison. Include all others (except LOBs if {@code
        * #noLobs()} was invoked).
        *
        * @param   fields
        *          List of legacy field names to exclude.
        *
        * @return  This object instance.
        */
       public BufferCompare except(String... fields)
       {
          this.exclusive = true;
          this.fields = fields;

          return this;
       }

       /**
        * Include only the given fields for the comparison. Exclude all others.
        *
        * @param   fields
        *          List of legacy field names to include.
        *
        * @return  This object instance.
        */
       public BufferCompare using(String... fields)
       {
          this.exclusive = false;
          this.fields = fields;

          return this;
       }

       /**
        * Configure a case-sensitive comparison (only relevant for character and CLOB fields).
        *
        * @return  This object instance.
        */
       public BufferCompare caseSensitive()
       {
          mode = Mode.CASE_SENSITIVE;

          return this;
       }

       /**
        * Configure a binary comparison.
        *
        * @return  This object instance.
        */
       public BufferCompare binary()
       {
          mode = Mode.BINARY;

          return this;
       }

       /**
        * Set the target object into which a boolean result of the comparison will be stored. The
        * result will be {@code true} if all compared fields matched; else {@code false}.
        *
        * @param   target
        *          Result target.
        *
        * @return  This object instance.
        */
       public BufferCompare saveResultIn(logical target)
       {
          this.exhaustive = false;
          this.resultDelegate = new AccessorWrapper(target);

          return this;
       }

       /**
        * Set the target object into which a text result of the comparison will be stored. The result
        * will be a list of the legacy names of fields, if any, whose values did not match.
        *
        * @param   target
        *          Result target.
        *
        * @return  This object instance.
        */
       public BufferCompare saveResultIn(character target)
       {
          this.exhaustive = true;
          this.resultDelegate = new AccessorWrapper(target);

          return this;
       }

       /**
        * Set the target field into which a result of the comparison will be stored. The type of the
        * field must be {@code logical} or {@code character}. If {@code logical}, the result will be
        * {@code true} if all compared fields matched; else {@code false}. If {@code character}, the
        * result will be a list of the names of legacy fields, if any, whose values did not match.
        *
        * @param   target
        *          Result target.
        *
        * @return  This object instance.
        */
       public BufferCompare saveResultIn(FieldReference target)
       {
          this.exhaustive = character.class.isAssignableFrom(target.getType());
          this.resultDelegate = target;

          return this;
       }

       /**
        * Exclude any LOB fields from the comparison. If the buffer record contains no LOB fields,
        * this option has no effect.
        *
        * @return  This object instance.
        */
       public BufferCompare noLobs()
       {
          this.noLobs = true;

          return this;
       }

       /**
        * Perform the comparison. This method should only be invoked once all configuration methods
        * have been invoked. In the typical use pattern, this method is called at the end of the
        * method chain which starts with {@code new BufferCompare(dmo1, dmo2)}.
        */
       public void run()
       {
          // if no result target was configured, the comparison is avoided, since there is no way to
          // retrieve the result, and there are no side effects of the comparison
          // TODO: this could change, once EXPLICIT COMPARES expressions are supported
          if (resultDelegate == null)
          {
             return;
          }

          List<String> diffs = RecordBuffer.compare(dmo1, dmo2, fields, noLobs, mode, exclusive, exhaustive);

          if (diffs == null || diffs.isEmpty())
          {
             if (exhaustive)
             {
                resultDelegate.set(new character(""));
             }
             else
             {
                resultDelegate.set(new logical(true));
             }
          }
          else
          {
             if (exhaustive)
             {
                // use the P4GL simple comma-separated list format
                StringBuilder buf = new StringBuilder();
                for (String next : diffs)
                {
                   if (buf.length() > 0)
                   {
                      buf.append(',');
                   }
                   buf.append(next);
                }
                resultDelegate.set(new character(buf.toString()));
             }
             else
             {
                resultDelegate.set(new logical(false));
             }
          }
       }
    }