DataRelation.java

/*
** Module   : DataRelation.java
** Abstract : A data relation object.
**
** Copyright (c) 2019-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM  20190403 Created initial version.
** 002 OM  20190611 Added runtime implementation.
** 003 OM  20190716 Calling the right super c'tor based on the parent dataset's dynamic attribute.
**     OM  20190721 Validate NAME attribute.
**     CA  20190812 Changes to allow for mutable buffers; any API which receives a Buffer instance
**                  and is invoked from converted code must resolve the runtime instance before
**                  saving the instance.
** 004 OM  20201030 Invalid attribute API support for getters/setters.
**     OM  20201202 Improved compatibility of attributes with P4GL.
**     OM  20210930 Only the name of dynamic relations have name constraints.
**     OM  20220120 Added getRelationFields(boolean) method.
**     CA  20220923 Internal var definitions must be done via TypeFactory and not UndoableFactory.
**     CA  20220608 Made getRelationFieldArray public.
**     CA  20220918 Automatically delete the created dynamic query when the relation gets deleted.
**     CA  20221129 Reset FILL-WHERE-STRING when relation is deactivated.
**     HC  20230116 Replaced some handle usages with the actual wrapped resources for performance.
**     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 005 IAS 20230312 Added 'getParentTable()/getChildTable()' methods.
** 006 OM  20230315 get***BufferNative() methods return BufferImpl values.
** 007 CA  20230523 Added _isRecursive, which returns Java boolean.
** 008 CA  20240324 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime.
** 009 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
**                  default methods cause the Java metaspace to spike one order of magnitude.
** 010 RNC 20241030 Created getQuery(boolean) 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.*;
import com.goldencode.util.*;

import java.util.*;

/**
 * A {@code DataRelation} defines one relation between a pair of parent and child buffers in a
 * {@code DataSet} that have a one-to-one or one-to-many parent-child relationship. A 
 * {@code DataRelation} identifies the parent and child buffers, and the fields in each buffer
 * that define the primary and foreign key fields of the relation.
 */
@LegacyResource(resource = LegacyResource.DATA_RELATION)
public class DataRelation
extends HandleResource
implements ADMData,
           CommonHandleChain,
           DataRelationInterface,
           DynamicResource,
           QueryAssociable
{
   /** The parent {@code DataSet} where this {@code DataRelation} is defined. */
   private final DataSet dataSet; 
   
   /** The parent buffer. */
   private final BufferImpl parentBuffer;
   
   /** The child buffer. */
   private final BufferImpl childBuffer;
   
   /** {@code true} to flag this as a RECURSIVE relation. */
   private final boolean recursive;
   
   /** The relation fields for this object. */
   private final String[] relationFields;
   
   /** The set of AFTER fields. */
   private final List<String> afterFields;
   
   /** The set of AFTER fields. */
   private final List<String> beforeFields;
   
   /** For PARENT-ID relations, this specifies the field that points to parent 'foreign' key. */
   private final String parentLink;
   
   /** The ACTIVE status of this relation. */
   private boolean active = true;
   
   /** The NESTED status of this relation. */
   private boolean nested = false;
   
   /** The REPOSITION status of this relation. */
   private boolean reposition = false;
   
   /** The FOREIGN-KEY-HIDDEN status of this relation. */
   private boolean fkHidden = false;
   
   /** The MAXIMUM-LEVEL status of this relation. */
   private int maximumLevel = 0;
   
   /** The current value of {@code WHERE-STRING} attribute. */
   private String whereString = null;
   
   /** Current value of PRIVATE-DATA. */
   private String privateData = null;
   
   /** Current value of PRIVATE-DATA. */
   private String admData = null;
   
   /** The value of {@code NAME} attribute. */
   private String name = null;
   
   /** 
    * Flags for valid property. The {@code DataRelation}s are valid from the moment they are
    * created until the object is dropped/deleted. Once it is deleted and made invalid, the
    * object cannot be made valid again.
    */
   private boolean isValid = true;
   
   /** The current value  of {@code QUERY} attribute. Uses lazy initialization. */
   private QueryWrapper query = null;
   
   /** 
    * Reference counter. Only used to create incremental names for unnamed (static)
    * {@code DataRelations}.
    */
   private static final RefCount refCount = new RefCount();
   
   /**
    * Creates a new simple {@code DataRelation} based on join between pairs of fields from parent
    * and child buffer. This is the constructor called when a {@code ADD-RELATION} method is
    * successful.
    * 
    * @param   dataSet
    *          The {@code DataSet}.
    * @param   parent
    *          The parent buffer.
    * @param   child
    *          The child buffer.
    * @param   relationFields
    *          The set of fields. The list has an even number of values, the values on odd indexes
    *          being the parent fields and the even ones are the child fields.
    * @param   strPairs
    *          The pair of fields, as string as they appear in the legacy statement.
    * @param   recursive
    *          {@code true} if this is a recursive relation.
    */
   public DataRelation(DataSet dataSet,
                       Buffer parent,
                       Buffer child,
                       String[] relationFields,
                       String strPairs,
                       boolean recursive)
   {
      super(dataSet._dynamic());
      this.dataSet = dataSet.ref();
      this.parentBuffer = ((BufferImpl) parent).ref();
      this.childBuffer = ((BufferImpl) child).ref();
      this.relationFields = relationFields;
      this.recursive = recursive;
      
      // create the default WHERE-STRING
      String pbn = ((BufferImpl) parent)._name();
      String cbn = ((BufferImpl) child)._name();
      
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < relationFields.length; i += 2)
      {
         sb.append(sb.length() == 0 ? " WHERE " : " and ");
         sb.append(cbn).append('.').append(relationFields[i + 1]).append('=');
         sb.append(pbn).append('.').append(relationFields[i]);
      }
      
      // use lazy-initialization for [query]
      this.whereString = sb.toString();
      
      // mark unused fields for this kind of DataRelation:
      this.afterFields = null;
      this.beforeFields = null;
      this.parentLink = null;
   }
   
   /**
    * Creates a new simple {@code DataRelation} based on join between pairs of fields from parent and child
    * buffer. This is the constructor called when a {@code ADD-PARENT-ID-RELATION} method is successful.
    *
    * @param   parent
    *          The parent buffer.
    * @param   child
    *          The child buffer.
    * @param   parentLink
    *          The name of the {@code RECID} field of {@code child} buffer that is used to join
    *          with the {@code parent} buffer.
    * @param   beforeFields
    *          The list of fields to be serialized before.
    * @param   afterFields
    *          The list of fields to be serialized after.
    * @param   dynamic
    *          Use {@code true} for dynamically created relations.
    */
   public DataRelation(DataSet dataSet, 
                       Buffer parent,
                       Buffer child,
                       String parentLink,
                       List<String> beforeFields,
                       List<String> afterFields,
                       boolean dynamic)
   {
      super(dataSet._dynamic());
      this.dataSet = dataSet.ref();
      this.parentBuffer = ((BufferImpl) parent).ref();
      this.childBuffer = ((BufferImpl) child).ref();
      this.parentLink = parentLink;
      this.beforeFields = beforeFields.isEmpty() ? null : beforeFields;
      this.afterFields = afterFields.isEmpty() ? null : afterFields;
      
      // create the default WHERE-STRING
      String pbn = ((BufferImpl) parent)._name();
      String cbn = ((BufferImpl) child)._name();
      
      // use lazy-initialization for [query]
      // I found different "styles" of essentially the same predicate, the only differences being the single
      // space prefix and capitalization for WHERE and RECID keywords 
      // this.whereString = "WHERE " + cbn + '.' + parentLink + "=RECID(" + pbn + ')';
      this.whereString = " where " + cbn + '.' + parentLink + "=recid(" + pbn + ')';
      
      // mark unused fields for this kind of DataRelation:
      this.recursive = false;
      this.relationFields = null;
      
      if (dynamic)
      {
         confirmDynamic();
      }
   }
   
   /**
    * Confirms that this object is dynamic.<p>
    * This method finishes setting this object parent-procedure and makes sure it is deleted at the
    * appropriate moment (when the parent dataset is deleted). This is only needed for dynamically created
    * relations.
    */
   void confirmDynamic()
   {
      pm.processResource(this, true);
   }
   
   /**
    * Corresponds to {@code ACTIVE} ABL attribute. Deactivate an individual data-relation in a
    * {@code DataSet} object.
    *
    * @return  the current state of {@code ACTIVE} attribute. 
    */
   @Override
   public logical isActive()
   {
      return logical.of(active);
   }
   
   /**
    * Corresponds to {@code ACTIVE} ABL attribute. Deactivate an individual data-relation in a
    * {@code DataSet} object.
    *
    * @param   active
    *          The new state of {@code ACTIVE} attribute.
    */
   @Override
   public void setActive(boolean active)
   {
      if (this.active == active)
      {
         return;
      }
      
      if (_isParentIdRelation() || !active)
      {
         this.active = active;
         DataSource childDSsrc = (DataSource) this.childBuffer.dataSourceNative();
         if (!active && childDSsrc != null)
         {
            childDSsrc.setFillWhereString((String) null); // reset
         }
      }
      else 
      {
         if (dataSet.allowsActiveChild(childBuffer) == null)
         {
            this.active = true;
         }
         else
         {
            ErrorManager.recordOrShowError(11979, childBuffer.name().toStringMessage());
            // Buffer <name> can have only 1 active parent relation.
         }
      }
   }
   
   /**
    * Corresponds to {@code ACTIVE} ABL attribute. Deactivate an individual data-relation in a
    * {@code DataSet} object.
    *
    * @param   active
    *          The new state of {@code ACTIVE} attribute. 
    */
   @Override
   public void setActive(logical active)
   {
      if (active == null || active.isUnknown())
      {
         unableToAssignUnknown("ACTIVE", "DATA-RELATION widget");
         return;
      }
      
      setActive(active.getValue());
   }
   
   /**
    * Obtain the buffer handle of the child member of this data-relation object. This method
    * corresponds to the real-only {@code CHILD-BUFFER} attribute.
    *
    * @return  the buffer handle of the child member.
    */
   @Override
   public handle getChildBuffer()
   {
      return new handle(childBuffer);
   }

   /**
    * Obtain the buffer of the child member of this data-relation object. This method
    * corresponds to the real-only {@code CHILD-BUFFER} attribute.
    *
    * @return  the buffer of the child member.
    */
   public BufferImpl getChildBufferNative()
   {
      return childBuffer;
   }
   
   /**
    * Obtain the name of the child table.
    *
    * @return  the name of the child table.
    */
   public String getChildTable()
   {
      return childBuffer.getTable().toJavaType();
   }
   
   /**
    * Obtain the handle to the {@code DataSet} query that contains the currently selected row in
    * the {@code BindingSource}. Corresponds to {@code CURRENT-QUERY} ABL method.
    *
    * @return  the handle to the {@code DataSet} query that contains the currently selected row
    */
   @Override
   public handle getCurrentQuery()
   {
      UnimplementedFeature.missing("Attribute CURRENT-QUERY not implemented");
      return new handle();
   }
   
   /**
    * Obtain the handle to the {@code DataSet} query that contains the currently selected row in
    * the {@code BindingSource}. Corresponds to {@code CURRENT-QUERY} ABL method.
    *
    * @param   bandIndex
    *          0-based integer value that specifies the band index in a hierarchical grid
    *
    * @return  the handle to the {@code DataSet} query that contains the currently selected row
    */
   @Override
   public handle getCurrentQuery(long bandIndex)
   {
      UnimplementedFeature.missing("Attribute CURRENT-QUERY not implemented");
      return new handle();
   }
   
   /**
    * Obtain the handle to the {@code DataSet} query that contains the currently selected row in
    * the {@code BindingSource}. Corresponds to {@code CURRENT-QUERY} ABL method.
    *
    * @param   bandIndex
    *          0-based integer value that specifies the band index in a hierarchical grid
    *
    * @return  the handle to the {@code DataSet} query that contains the currently selected row
    */
   @Override
   public handle getCurrentQuery(NumberType bandIndex)
   {
      UnimplementedFeature.missing("Attribute CURRENT-QUERY not implemented");
      return new handle();
   }
   
   /**
    * Obtains a comma-separated list of parent-field, child-field pairs describing the
    * relationship between parent and child buffers as specified in the data-relation object
    * definition. Corresponds to {@code RELATION-FIELDS} readonly ABL attribute.
    *
    * @return  the set of field pairs that describes the data-relation.
    */
   @Override
   public character getRelationFields()
   {
      return new character(_getRelationFields());
   }
   
   /**
    * Obtains the current WHERE expression from the query generated that links the child table to
    * its parent. Corresponds to {@code WHERE-STRING} attribute.
    *
    * @return  the current WHERE expression that links the child table to its parent.
    */
   @Override
   public character getWhereString()
   {
      return new character(whereString);
   }
   
   /**
    * Modifies the current WHERE expression that links the child table to its parent. This is the
    * setter implementation for {@code WHERE-STRING} ABL attribute.
    *
    * @param   where
    *          the new predicate.
    */
   @Override
   public void setWhereString(String where)
   {
      if (createQuery(where))
      {
         whereString = where;
      }
      else
      {
         ErrorManager.recordOrShowError(11981);
         // Failed to auto-prepare an automatic fill query.
         ErrorManager.recordOrShowError(3131, "WHERE-STRING", "", LegacyResource.DATA_RELATION);
         // Unable to set attribute %1 in widget %2 of type %3.
      }
   }
   
   /**
    * Modifies the current WHERE expression that links the child table to its parent. This is the
    * setter implementation for {@code WHERE-STRING} ABL attribute.
    * <p>
    * Unlike other attributes, this attribute can be set tu unknown value!
    *
    * @param   where
    *          the new predicate.
    */
   @Override
   public void setWhereString(Text where)
   {
      setWhereString(where.toJavaType());
   }
   
   /**
    * Checks whether the foreign key fields in the child records of a nested data-relation are
    * hidden. This is the getter implementation of the ABL {@code FOREIGN-KEY-HIDDEN} attribute.
    *
    * @return  the current value of {@code FOREIGN-KEY-HIDDEN} attribute for this relation.
    */
   @Override
   public logical getForeignKeyHidden()
   {
      return logical.of(fkHidden);
   }
   
   /**
    * Configures whether the foreign key fields in the child records of a nested data-relation are
    * hidden. This is the setter implementation of the ABL {@code FOREIGN-KEY-HIDDEN} attribute.
    *
    * @param   hide
    *          the new value of {@code FOREIGN-KEY-HIDDEN} attribute of this relation.
    */
   @Override
   public void setForeignKeyHidden(boolean hide)
   {
      this.fkHidden = hide;
   }
   
   /**
    * Configures whether the foreign key fields in the child records of a nested data-relation are
    * hidden. This is the setter implementation of the ABL {@code FOREIGN-KEY-HIDDEN} attribute.
    *
    * @param   hide
    *          the new value of {@code FOREIGN-KEY-HIDDEN} attribute of this relation.
    */
   @Override
   public void setForeignKeyHidden(logical hide)
   {
      if (hide == null || hide.isUnknown()) 
      {
         unableToAssignUnknown("FOREIGN-KEY-HIDDEN", "DATA-RELATION widget");
         return;
      }
      setForeignKeyHidden(hide.getValue());
   }
   
   /**
    * Obtain the maximum level where a recursive data-relation stops. This is the getter part of
    * the {@code MAXIMUM-LEVEL} ABL attribute.
    *
    * @return  the number of iterations at which this recursion stops.
    */
   @Override
   public integer getMaximumLevel()
   {
      return new integer(maximumLevel);
   }
   
   /**
    * Configures the maximum level where a recursive data-relation stops. This is the setter part
    * of the {@code MAXIMUM-LEVEL} ABL attribute.
    *
    * @param   max
    *          the new number of iterations at which the recursion stops.
    */
   @Override
   public void setMaximumLevel(NumberType max)
   {
      if (max == null || max.isUnknown())
      {
         unableToAssignUnknown("MAXIMUM-LEVEL", "DATA-RELATION widget");
         return;
      }
   
      setMaximumLevel(max.intValue());
   }
   
   /**
    * Configures the maximum level where a recursive data-relation stops. This is the setter part
    * of the {@code MAXIMUM-LEVEL} ABL attribute.
    *
    * @param   max
    *          the new number of iterations at which the recursion stops.
    */
   @Override
   public void setMaximumLevel(long max)
   {
      maximumLevel = (int) max;
   }
   
   /**
    * Obtain the reposition mode of this data-relation object. When {@code true}, the relation
    * mode is {@code REPOSITION}. Otherwise, the relation mode is {@code SELECTION}. The default
    * value is {@code false}. This is the getter of the {@code REPOSITION} attribute.
    *
    * @return  {@code true} when the reposition mode is {@code REPOSITION} and {@code false}
    *          when reposition mode is {@code SELECTION}.
    */
   @Override
   public logical getReposition()
   {
      return logical.of(reposition);
   }
   
   /**
    * Sets the reposition mode of this data-relation object. Use {@code true} to set the relation
    * mode to {@code REPOSITION} and {@code false} to set it to {@code SELECTION}. This is the
    * setter of the {@code REPOSITION} attribute.
    *
    * @param   reposition
    *          the new reposition mode as described above.
    */
   @Override
   public void setReposition(logical reposition)
   {
      if (reposition == null || reposition.isUnknown())
      {
         unableToAssignUnknown("REPOSITION", "DATA-RELATION widget");
         return;
      }
      
      setReposition(reposition.getValue());
   }
   
   /**
    * Sets the reposition mode of this data-relation object. Use {@code true} to set the relation
    * mode to {@code REPOSITION} and {@code false} to set it to {@code SELECTION}. This is the
    * setter of the {@code REPOSITION} attribute.
    *
    * @param   reposition
    *          the new reposition mode as described above.
    */
   @Override
   public void setReposition(boolean reposition)
   {
      this.reposition = reposition;
   }
   
   /**
    * Obtain a comma-separated list of fields from the parent record in the relation to be
    * written to XML after all nested child records.
    *
    * @return  a comma-separated list of fields from the parent record in the relation to be
    *          written to XML after all nested child records.
    */
   @Override
   public character getParentFieldsAfter()
   {
      if (!_isParentIdRelation() || afterFields == null)
      {
         return new character();
      }
      
      StringBuilder sb = new StringBuilder();
      for (String field : afterFields)
      {
         if (sb.length() != 0)
         {
            sb.append(',');
         }
         sb.append(field);
      }
      
      return new character(sb.toString());
   }
   
   /**
    * Obtain a comma-separated list of fields from the parent record in the relation to be
    * written to XML before any nested child records.
    *
    * @return  a comma-separated list of fields from the parent record in the relation to be
    *          written to XML after all nested child records.
    */
   @Override
   public character getParentFieldsBefore()
   {
      if (!_isParentIdRelation() || beforeFields == null)
      {
         return new character();
      }
      
      StringBuilder sb = new StringBuilder();
      for (String field : beforeFields)
      {
         if (sb.length() != 0)
         {
            sb.append(',');
         }
         sb.append(field);
      }
      
      return new character(sb.toString());
   }
   
   /**
    * Obtain the buffer handle of the parent member. This is the FWD implementation of 
    * {@code PARENT-BUFFER} ABL read-only attribute 
    *
    * @return  the buffer handle of the parent member.
    */
   @Override
   public handle getParentBuffer()
   {
      return new handle(parentBuffer);
   }
   
   /**
    * Obtain the buffer of the parent member. This is the FWD implementation of
    * {@code PARENT-BUFFER} ABL read-only attribute
    *
    * @return  the buffer of the parent member.
    */
   public BufferImpl getParentBufferNative()
   {
      return parentBuffer;
   }
   
   /**
    * Obtain the name of the parent table.
    *
    * @return  the name of the parent table.
    */
   public String getParentTable()
   {
      return parentBuffer.getTable().toJavaType();
   }
   
   /**
    * Checks whether child rows of a {@code DataSet} temp-table buffer are nested within their
    * parent rows when writing the XML representation of {@code DataSet} object that contains
    * data-relations.
    *
    * @return  {@code yes} if the child rows are nested within their parent rows.
    */
   @Override
   public logical isNested()
   {
      return logical.of(nested);
   }
   
   /**
    * Configures whether child rows of a {@code DataSet} temp-table buffer are nested within their
    * parent rows when writing the XML representation of {@code DataSet} object that contains
    * data-relations.
    *
    * @param   nested
    *          The new value of the {@code NESTED} attribute.
    */
   @Override
   public void setNested(boolean nested)
   {
      this.nested = nested;
   }
   
   /**
    * Configures whether child rows of a {@code DataSet} temp-table buffer are nested within their
    * parent rows when writing the XML representation of {@code DataSet} object that contains
    * data-relations.
    *
    * @param   nested
    *          The new value of the {@code NESTED} attribute.
    */
   @Override
   public void setNested(logical nested)
   {
      if (nested == null || nested.isUnknown())
      {
         unableToAssignUnknown("NESTED", "DATA-RELATION widget");
         return;
      }
      setNested(nested.getValue());
   }
   
   /**
    * Checks whether this data-relation is recursive.
    *
    * @return  {@code yes} when this data-relation is recursive.
    */
   @Override
   public logical isRecursive()
   {
      return logical.of(recursive);
   }
   
   /**
    * Checks whether this data-relation is recursive.
    *
    * @return  {@code true} when this data-relation is recursive.
    */
   public boolean _isRecursive()
   {
      return recursive;
   }
   
   /**
    * Checks whether the relationship between parent and child records is based on the
    * {@code RECID} type of the parent record. In a {@code PARENT-ID-RELATION}, the value of
    * {@code RELATION-FIELDS} attribute will be the name of the child buffer’s {@code RECID} field.
    *
    * @return  {@code YES} if this is a  {@code PARENT-ID-RELATION}
    */
   @Override
   public logical isParentIdRelation()
   {
      return logical.of(_isParentIdRelation());
   }
   
   /**
    * Get the query associated with this relation.
    *
    * @return The associated query or {@code unknown} if none exists.
    */
   @Override
   public handle getQueryAsHandle()
   {
      return new handle(getQuery());
   }
   
   /**
    * Get the query associated with this relation.
    *
    * @return The associated query or {@code null} if none exists.
    */
   public QueryWrapper getQuery()
   {
      if (query == null)
      {
         // this will actually be called only when the default [whereString] was not accessed yet
         createQuery(whereString);
      }
      
      return query;
   }

   /**
    * Get the query associated with this relation, but try to recreate it first if
    * <code>shouldRecreate</code> is true and at least one of the following conditions hold:
    * <li>the <code>prepareString</code> associated with the {@link #query} is null</li>
    * @param   shouldRecreate
    *          <code>true</code> if the query should be recreated, <code>false</code> otherwise
    *
    * @return The associated query or {@code null} if none exists.
    */
   public QueryWrapper getQuery(boolean shouldRecreate)
   {
      boolean delete = false;
      if (shouldRecreate && query != null)
      {
         delete = query.prepareString().isUnknown();
      }

      if (delete && query.valid())
      {
         query.delete();
         query = null;
      }

      return getQuery();
   }
   
   /**
    * Set the query associated with the given resource.
    *
    * @param   qry
    *          The query to associate.
    */
   @Override
   public void setQueryAsHandle(handle qry)
   {
      handle.invalidAttribute("QUERY", true, LegacyResource.DATA_RELATION);
   }
   
   /**
    * Set the query associated with the given resource.
    *
    * @param   qry
    *          The query to associate.
    */
   @Override
   public void setQueryAsHandle(QueryWrapper qry)
   {
      handle.invalidAttribute("QUERY", true, LegacyResource.DATA_RELATION);
   }
   
   /**
    * Get the value of the ADM-DATA attribute.
    *
    * @return See above.
    */
   @Override
   public character getADMData()
   {
      return new character(admData);
   }
   
   /**
    * Set the value of the ADM-DATA attribute.
    *
    * @param value The new value.
    */
   @Override
   public void setADMData(String value)
   {
      admData = value;
   }
   
   /**
    * Set the value of the ADM-DATA attribute.
    *
    * @param value The new value.
    */
   @Override
   public void setADMData(character value)
   {
      admData = (value == null || value.isUnknown()) ? null : value.getValue();
   }
   
   /**
    * Get the {@code name} attribute of this object.
    *
    * @return See above.
    */
   @Override
   public character name()
   {
      if (name == null)
      {
         // TODO: is this implementation enough? 
         name = "RELATION" + (refCount.get());
         refCount.increment();
      }
      return new character(name);
   }
   
   /**
    * Set the {@code name} attribute of this object.
    *
    * @param name The name to be set.
    */
   @Override
   public void name(String name)
   {
      if (name == null)
      {
         unableToAssignUnknown("NAME", "DATA-RELATION widget");
         return;
      }
      
      // this is strange, the static relation do not have naming constraints. The dynamic ones have.
      // It must be some kind of code-injection protection
      if (_dynamic() && !HandleChain.validateName(name))
      {
         ErrorManager.recordOrThrowError(3131, "NAME", "", LegacyResource.DATA_RELATION);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      this.name = name;
   }
   
   /**
    * Set the {@code name} attribute of this object.
    *
    * @param name The name to be set.
    */
   @Override
   public void name(character name)
   {
      name(name.toJavaType());
   }
   
   /**
    * Reports if this object is valid for use.
    *
    * @return  {@code true} if we are valid (can be used).
    */
   @Override
   public boolean valid()
   {
      return isValid;
   }
   
   /**
    * Get the {@code NEXT-SIBLING} attribute of this handle.
    *
    * @return See above.
    */
   @Override
   public handle getNextSibling()
   {
      handle.invalidAttribute("NEXT-SIBLING", false, LegacyResource.DATA_RELATION);
      return new handle();
   }
   
   /**
    * Get the {@code PREV-SIBLING} attribute of this handle.
    *
    * @return See above.
    */
   @Override
   public handle getPrevSibling()
   {
      handle.invalidAttribute("PREV-SIBLING", false, LegacyResource.DATA_RELATION);
      return new handle();
   }
   
   /**
    * Get the {@code PRIVATE-DATA} attribute of this handle.
    *
    * @return See above.
    */
   @Override
   public character getPrivateData()
   {
      return new character(privateData);
   }
   
   /**
    * Set the {@code PRIVATE-DATA} attribute of this handle.
    *
    * @param data The new private-data value.
    */
   @Override
   public void setPrivateData(character data)
   {
      privateData = (data == null || data.isUnknown()) ? null : data.getValue();
   }
   
   /**
    * Set the {@code PRIVATE-DATA} attribute of this handle.
    *
    * @param data The new private-data value.
    */
   @Override
   public void setPrivateData(String data)
   {
      privateData = data;
   }
   
   /**
    * Returns {@code true} if the buffer is dynamic.
    * <p>
    * This is not emitted by conversion, is used internally by FWD.
    *
    * @return {@code true} if the resource is dynamic, {@code false} otherwise.
    */
   @Override
   public boolean _dynamic()
   {
      return false;
   }

   /**
    * Checks  whether the resource is dynamic or static.
    * 
    * @return   {@code true} if the resource is dynamic, {@code false} otherwise. 
    */
   @Override
   public logical dynamic()
   {
      return logical.FALSE;
   }
   
   /**
    * Extracts the list of relation fields for only one of the tables, in form of an array. If this is a
    * {@code PARENT-ID} relation only the parent link is returned.
    *
    * @param   parent
    *          Pass {@code true} for fields from parent table and {@code false} for fields from child table.
    *
    * @return  the list of relation fields.
    */
   public String[] getRelationFields(boolean parent)
   {
      if (_isParentIdRelation())
      {
         return parent ? new String[0] : new String[] { parentLink };
      }
      
      String[] ret = new String[relationFields.length / 2];
      for (int i = 0; i < relationFields.length; i += 2)
      {
         ret[i / 2] = relationFields[parent ? i : i + 1];
      }
      return ret;
   }
   
   /**
    * Allows each resource to declare whether it has some naming constraints.
    *
    * @return  This method always returns {@code false}, requiring that its name should be
    *          validated before assignment.
    *
    * @see     HandleChain#validateName
    */
   protected boolean hasNameConstraints()
   {
      return true;
   }
   
   /**
    * Worker to be implemented by each resource.  Called by {@link #delete()}.
    *
    * @return   Always {@code true}.
    */
   @Override
   protected boolean resourceDelete()
   {
      isValid = false;
      if (query != null && query.valid())
      {
         query.delete();
      }
      
      return true;
   }
   
   /**
    * The original set of field pairs. Only used for duplication of relations.
    * 
    * @return  original set of field pairs, if any.
    */
   public String[] getRelationFieldArray()
   {
      return relationFields;
   }
   
   /**
    * Extract the list of relation fields. If this is a {@code PARENT-ID} relation only the
    * parent link is returned, otherwise the set of pairs that defines the RELATION-FIELDS
    * attribute is returned.
    * 
    * @return  the list of relation fields.
    */
   String _getRelationFields()
   {
      if (_isParentIdRelation())
      {
         return parentLink;
      }
      else
      {
         StringBuilder sb = new StringBuilder();
         for (String field : relationFields)
         {
            if (sb.length() != 0)
            {
               sb.append(',');
            }
            sb.append(field);
         }
         return sb.toString();
      }
   }
   
   /**
    * Checks whether this relation is active.
    * 
    * @return  the current state of {@code ACTIVE} attribute.
    */
   boolean _isActive()
   {
      return active;
   }
   
   /**
    * Query the current value of {@code REPOSITION} attribute.
    * 
    * @return  the current value of {@code REPOSITION} attribute.
    */
   boolean _isReposition()
   {
      return reposition;
   }
   
   /**
    * Internal check whether this is a parent-id-relation.
    * 
    * @return  {@code true} when {@link #parentLink} is not {@code null}.
    */
   private boolean _isParentIdRelation()
   {
      return parentLink != null;
   }
   
   /**
    * Modifies the current WHERE expression that links the child table to its parent. This is the
    * setter implementation for {@code WHERE-STRING} ABL attribute.
    *
    * @param   where
    *          The new predicate.
    * @return  {@code true} if operation is successful.
    */
   private boolean createQuery(String where)
   {
      // prepare the query
      String pbn = parentBuffer._name();
      String cbn = childBuffer._name();
      StringBuilder sb = new StringBuilder("FOR EACH ");
      sb.append(cbn);
      
      if (where != null)
      {
         // the [localWhereString] must contain the 'WHERE' token 
         sb.append(' ').append(where);
      }
      
      if (query != null)
      {
         query.delete();
      }
      
      query = QueryWrapper.createQueryUnnamedPool();
      query.setBuffers(childBuffer);
      query.setSubstitutionBuffers(parentBuffer);
      return query.prepare(sb.toString()).booleanValue();
   }
   
   /**
    * Returns a string representation of this object.
    *
    * @return a string representation of this object.
    */
   @Override
   public String toString()
   {
      return name + "{" + whereString + "}";
   }
}