# AssociationSyncher.java

    /*
    ** Module   : AssociationSyncher.java
    ** Abstract : Synchronizes records across foreign key associations
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20061113   @31136 Created initial version. Abstract base class
    **                           for classes which synchronize records across
    **                           foreign key associations.
    ** 002 ECF 20061114   @31145 Disabled prohibitively expensive operations.
    **                           This effectively removes all synchronization
    **                           except setting the foreign DMO in a local DMO
    **                           when a legacy key property in that local DMO
    **                           is changed.
    ** 003 ECF 20070416   @33018 Integrated user interrupt handling.
    ** 004 ECF 20070704   @34357 Closed session flush loophole. A pending flush
    **                           is now forced with the ChangeBroker after an
    **                           association is updated.
    ** 005 ECF 20070911   @35282 Replaced database name string with Database
    **                           information object. Required for non-local
    **                           database connections.
    ** 006 ECF 20071219   @36576 Changed acquireLock() signature. Allow lock
    **                           type to be specified.
    ** 007 CA  20080815   @39450 Support API change in DBUtils.
    ** 008 GES 20090421   @41823 Matched utility class package change.
    ** 009 ECF 20131029          Import change, minor housekeeping.
    ** 010 ECF 20160202          Replaced Apache commons logging with J2SE logging.
    ** 011 ECF 20200906          New ORM implementation.
    ** 012 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 013 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    */

    /*
    ** 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.lang.reflect.*;
    import java.util.*;
    import java.util.logging.*;
    import com.goldencode.util.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.logging.CentralLogger;

    /**
     * The abstract base class for subclasses which synchronize either end of a
     * foreign key association, based upon legacy key properties being changed in
     * the record on the opposite end of the association.  In the pre-conversion
     * application, there are no formal, foreign keys.  Such associations between
     * records are defined less stringently;  the association between two records
     * is determined dynamically at runtime.  With the introduction of formal
     * foreign keys into the schema, it is necessary to mimic this very late
     * binding association by dynamically updating foreign keys as the values of
     * legacy key columns change.  In the P2J environment, a DMO joined to
     * another DMO by a foreign key contains a reference to that DMO (and
     * associated getter and setter methods).  It is these references which are
     * updated by this class and its subclasses.
     */
    abstract class AssociationSyncher
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(AssociationSyncher.class.getName());

       /** Object which abstracts methods to assist in managing the foreign DMO */
       protected final Helper helper;

       /** Method used to set foreign DMO into local DMO */
       protected final Method foreignSetter;

       /** Name of table for which temporary record locks must be acquired */
       protected final String lockTable;

       /** Record buffer being monitored for property changes */
       protected final RecordBuffer buffer;

       /** Set of temporary locks which are acquired to make updates */
       private final Set<TempLock> tempLocks = new HashSet<>();

       /**
        * Constructor.
        *
        * @param   buffer
        *          Record buffer containing the DMO whose property change(s)
        *          will trigger synchronization attempts.
        * @param   info
        *          Foreign relation descriptor object.
        * @param   lockClass
        *          DMO class for which temporary record locks must be acquired.
        */
       AssociationSyncher(RecordBuffer buffer, RelationInfo info, Class<? extends Record> lockClass)
       {
          this.buffer = buffer;

          lockTable = DatabaseManager.getBackingTableName(lockClass);

          buffer.addLegacyForeignKeys(info.getForeignProperties());

          if (info.isUnique())
          {
             helper = new OneToOneHelper(info);
          }
          else
          {
             helper = new OneToManyHelper(info);
          }

          String ifaceName = info.getForeignInterfaceName();
          String method = null;
          try
          {
             // store foreign record setter method
             Class<?> localIface = info.getLocalInterface();
             method = "set" + ifaceName + "Record";
             Class<?>[] sig = new Class<?>[] { info.getForeignInterface() };
             foreignSetter = localIface.getDeclaredMethod(method, sig);
          }
          catch (NoSuchMethodException exc)
          {
             throw new IllegalArgumentException(
               "Invalid DMO class:  missing foreign setter [" +
               info.getLocalInterfaceName() +
               "." +
               method +
               "]");
          }
       }

       /**
        * Perform record synchronization.  This method is optimized to avoid the
        * overhead of performing a database query if not absolutely necessary.
        *
        * @param   dirtyProperties
        *          The set of properties which have been modified, thereby
        *          triggering the synchronization request.
        *
        * @throws  PersistenceException
        *          if any error occurs resolving the foreign record.
        */
       public final void attemptSynch(Set<String> dirtyProperties)
       throws PersistenceException
       {
          if (buffer.getCurrentRecord() == null)
          {
             return;
          }

          Set<String> properties = getLegacyKeys();
          Iterator<String> iter = dirtyProperties.iterator();
          while (iter.hasNext())
          {
             // If any legacy key property has been updated, a synch is needed.
             if (properties.contains(iter.next()))
             {
                doSynch();

                break;
             }
          }
       }

       /**
        * Retrieve the set of property names which represent the columns which
        * comprise the legacy key joining two records.  Changes to these
        * properties trigger synchronization of the two ends of a foreign key
        * association.  A set is returned because legacy keys are often
        * composites of multiple columns.
        *
        * @return  Set of property names comprising the legacy key.
        */
       protected abstract Set<String> getLegacyKeys();

       /**
        * Subclasses which need to remove any existing association linkages
        * before the new association is applied must implement this method.
        *
        * @param   newLocalDMOs
        *          Set of zero or more local DMOs which are about to be updated
        *          with the new association.
        * @param   newForeignDMO
        *          Foreign DMO which is about to be updated with the new
        *          association.
        *
        * @throws  IllegalAccessException
        *          if an access error occurs while invoking a method using
        *          reflection.
        * @throws  InvocationTargetException
        *          if a method invoked using reflection throws an exception.
        */
       protected abstract void preSynch(Set<Record> newLocalDMOs, Record newForeignDMO)
       throws IllegalAccessException,
              InvocationTargetException;

       /**
        * Retrieve the set of DMOs which comprise the local end of the foreign
        * association.  In cases where the foreign end of the association is
        * driving the synchronization, these will represent the "new" local
        * records which are resolved, based upon the new property values in one
        * of the foreign record's legacy keys.
        * <p>
        * The implementing class should obtain an exclusive lock on each record
        * returned, using the {@link #acquireLock} method to ensure it is
        * properly released or downgraded upon completion of synchronization.
        * This is not necessary for records already exclusively locked.
        * <p>
        * If the association is one-to-one, the set will contain zero or one
        * records.
        * <p>
        * If the association is one-to-many, the set may contain zero or more
        * records.
        *
        * @return  Set of newly resolved local DMOs.  The set may be empty, but
        *          it will not be <code>null</code>.
        *
        * @throws  PersistenceException
        *          if there are any errors retrieving DMOs from the database.
        */
       protected abstract Set<Record> retrieveLocalDMOs()
       throws PersistenceException;

       /**
        * Retrieve the DMO, if any, at the foreign end of the association.  In
        * cases where the local end of the association is driving the
        * synchronization, this will represent the "new" foreign record which is
        * resolved, based upon the new property values in one of the local
        * record's legacy keys.
        * <p>
        * The implementing class should obtain an exclusive lock on the record
        * returned, using the {@link #acquireLock} method to ensure it is
        * properly released or downgraded upon completion of synchronization.
        * This is not necessary if the record is already exclusively locked.
        *
        * @return  Foreign DMO, or <code>null</code> if none was available.
        *
        * @throws  PersistenceException
        *          if there are any errors retrieving the DMO from the database.
        */
       protected abstract Record retrieveForeignDMO()
       throws PersistenceException;

       /**
        * Acquire a temporary lock of the specified type for the given primary key
        * ID.  The table in which the record with the specified ID resides was
        * stored during construction of this object.  The lock is released when it
        * is no longer needed.
        *
        * @param   id
        *          ID of the record to lock.
        * @param   lockType
        *          Lock type to acquire for the record.
        *
        * @throws  LockUnavailableException
        *          if a NO-WAIT lock type variant is requested, but the lock
        *          cannot be acquired immediately.
        */
       protected final void acquireLock(Long id, LockType lockType)
       throws LockUnavailableException
       {
          boolean sharedDb = !buffer.getDmoInfo().multiTenant;
          TempLock lock = new TempLock(buffer.getPersistence(), lockType, lockTable, id, sharedDb);
          tempLocks.add(lock);
       }

       /**
        * Perform foreign association synchronization.  This will update the
        * appropriate DMO references on both ends of a bidirectional, foreign key
        * association.
        *
        * @throws  PersistenceException
        *          if any error occurs resolving records.
        */
       private void doSynch()
       throws PersistenceException
       {
          try
          {
             Set<Record> localDMOs = retrieveLocalDMOs();
             Record foreignDMO = retrieveForeignDMO();

    //       preSynch(localDMOs, foreignDMO);

             // Update the local DMO(s) referenced by the foreign end of the association.
    //       helper.replace(foreignDMO, localDMOs);

             // Update the foreign DMO referenced by the local end(s) of the association.
    //       ChangeBroker broker = ChangeBroker.get();
    //       Persistence persistence = buffer.getPersistence();
             Iterator<Record> iter = localDMOs.iterator();
             while (iter.hasNext())
             {
                Record next = iter.next();

                // Update the foreign record in the local DMO.
                foreignSetter.invoke(next, foreignDMO);

                // Force a pending flush for the updated DMO type.
    //          Class<? extends Record> dmoClass = next.getClass();
    //          broker.forcePendingFlush(persistence, dmoClass);
             }
          }
          catch (IllegalAccessException exc)
          {
             // Programming error.
             throw new RuntimeException(exc);
          }
          catch (InvocationTargetException exc)
          {
             DBUtils.handleException(buffer.getDatabase(), exc);

             // Programming error.
             throw new RuntimeException(exc);
          }
          catch (PersistenceException exc)
          {
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE,
                        buffer.describe() + ":  Error synchronizing foreign association",
                        exc);
             }
             exc.fillInStackTrace();
             throw exc;
          }
          finally
          {
             // Release temporary locks.
             Iterator<TempLock> iter = tempLocks.iterator();
             while (iter.hasNext())
             {
                TempLock lock = iter.next();
                lock.reset();
             }

             tempLocks.clear();
          }
       }

       /**
        * An interface which abstracts a helper API for managing a foreign DMO.
        * The abstraction hides the implementation differences between DMOs which
        * participate in a one-to-one association versus those that participate
        * in a one-to-many (many-to-one) association.
        */
       protected interface Helper
       {
          /**
           * Retrieve the set of local DMOs stored in a foreign DMO for this
           * association.
           *
           * @param   foreignDMO
           *          Foreign DMO from which to extract the set of local DMOs.
           *
           * @return  Set of local DMOs referenced by the given foreign DMO.  The
           *          set may be empty, but it will not be <code>null</code>.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public abstract Set<Record> getLocalDMOs(Record foreignDMO)
          throws IllegalAccessException,
                 InvocationTargetException;

          /**
           * Replace the set of local DMOs stored in a foreign DMO for this
           * association.
           *
           * @param   foreignDMO
           *          Foreign DMO which stores a reference to local DMOs.
           * @param   localDMOs
           *          The set of local DMOs which must replace the existing set.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public abstract void replace(Record foreignDMO, Set<Record> localDMOs)
          throws IllegalAccessException,
                 InvocationTargetException;

          /**
           * Remove the given local DMO from the set of local DMOs stored in a
           * foreign DMO for this association.
           *
           * @param   foreignDMO
           *          Foreign DMO which stores a reference to local DMOs.
           * @param   localDMO
           *          The local DMO to be removed from the existing set.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public abstract void remove(Record foreignDMO, Record localDMO)
          throws IllegalAccessException,
                 InvocationTargetException;
       }

       /**
        * An implementation of the <code>Helper</code> interface which assumes
        * that there is no more than one record on either end of the association.
        * This has implications for the methods used to retrieve and update the
        * foreign record's reference to the local end of the association.
        */
       protected final class OneToOneHelper
       implements Helper
       {
          /** Method used to get local DMO from foreign DMO */
          private final Method getter;

          /** Method used to set local DMO into foreign DMO */
          private final Method setter;

          /**
           * Constructor.  Looks up reflective methods used to get and set the
           * foreign DMO's reference to a local DMO.
           *
           * @param   info
           *          Object which contains information regarding the nature of
           *          the one-to-one, foreign association.
           */
          OneToOneHelper(RelationInfo info)
          {
             String method = null;
             String ifaceName = info.getLocalInterfaceName();
             try
             {
                Class<? extends DataModelObject> foreignIface = info.getForeignInterface();

                // store inverse (i.e., local) record getter method
                StringBuilder buf = new StringBuilder("get");
                buf.append(ifaceName);
                buf.append("Record");
                method = buf.toString();
                getter = foreignIface.getDeclaredMethod(method, (Class<?>[]) null);

                // store inverse (i.e., local) record setter method
                buf.setLength(0);
                buf.append("set");
                buf.append(ifaceName);
                buf.append("Record");
                method = buf.toString();
                Class<?>[] sig = new Class[] { info.getLocalInterface() };
                setter = foreignIface.getDeclaredMethod(method, sig);
             }
             catch (NoSuchMethodException exc)
             {
                throw new IllegalArgumentException(
                  "Invalid DMO class:  missing inverse getter or setter [" +
                  info.getForeignInterfaceName() +
                  "." +
                  method +
                  "]");
             }
          }

          /**
           * Retrieve the set of local DMOs stored in a foreign DMO for this
           * association.  Since this helper implementation manages a one-to-one
           * association, the set will never contain more than one record.
           *
           * @param   foreignDMO
           *          Foreign DMO from which to extract the set of local DMOs.
           *
           * @return  Set of local DMOs referenced by the given foreign DMO.  The
           *          set may be empty, but it will not be <code>null</code>.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public Set<Record> getLocalDMOs(Record foreignDMO)
          throws IllegalAccessException,
                 InvocationTargetException
          {
             Record dmo = (Record) getter.invoke(foreignDMO, (Object[]) null);

             return (dmo == null) ? new EmptySet<>() : Collections.singleton(dmo);
          }

          /**
           * Replace the local DMO stored in a foreign DMO for this association
           * with the contents of the given set.
           *
           * @param   foreignDMO
           *          Foreign DMO which stores a reference to local DMOs.
           * @param   localDMOs
           *          The set of local DMOs which must replace the existing set.
           *          Since this helper implementation manages a one-to-one
           *          association, this set must not have more than one element.
           *          However, it may be empty, in which case the local DMO
           *          reference in the foreign DMO will be nulled out.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           * @throws  IllegalArgumentException
           *          if <code>localDMOs</code> contains more than one element.
           */
          public void replace(Record foreignDMO, Set<Record> localDMOs)
          throws IllegalAccessException,
                 InvocationTargetException
          {
             if (foreignDMO == null)
             {
                return;
             }

             Record localDMO = null;
             Iterator<Record> iter = localDMOs.iterator();

             if (iter.hasNext())
             {
                localDMO = iter.next();
             }

             if (iter.hasNext())
             {
                throw new IllegalArgumentException(
                   "Cannot set multiple DMOs using method " + setter.getName());
             }

             // update the local record in the foreign DMO.  Foreign keys are never constrained to be
             // not-null, so a null parameter is OK.
             setter.invoke(foreignDMO, localDMO);
          }

          /**
           * Remove the reference to the given local DMO from the given foreign
           * DMO for this association.  This implementation actually ignores the
           * <code>localDMO</code> argument and assumes the intent of the caller
           * is simply to remove the reference entirely.
           *
           * @param   foreignDMO
           *          Foreign DMO which stores a reference to local DMOs.
           * @param   localDMO
           *          The local DMO to be removed from the existing set.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public void remove(Record foreignDMO, Record localDMO)
          throws IllegalAccessException,
                 InvocationTargetException
          {
             // Create a set containing only a null reference and pass it to the
             // updateLocal method.  This makes a simplifying assumption that
             // localDMO is the current, local DMO set in the foreign record.
             Set<Record> localDMOs = Collections.singleton(null);
             replace(foreignDMO, localDMOs);
          }
       }

       /**
        * An implementation of the <code>Helper</code> interface which assumes
        * a one-to-many association from a foreign DMO to zero or more local
        * DMOs.  This has implications for the methods used to retrieve and
        * update the foreign record's reference to the local end of the
        * association.
        */
       protected final class OneToManyHelper
       implements Helper
       {
          /** Object containing information about the nature of the association */
          private final RelationInfo info;

          /** Method used to retrieve set of local DMOs from foreign DMO */
          private final Method getter;

          /** Method used to set local DMO set into foreign DMO */
          private Method setter = null;

          /**
           * Constructor.  Looks up reflective method used to get the set of
           * local DMOs referenced by the foreign end of the association.
           *
           * @param   info
           *          Object which contains information regarding the nature of
           *          the one-to-many, foreign association.
           */
          OneToManyHelper(RelationInfo info)
          {
             this.info = info;
             String method = null;
             String ifaceName = info.getLocalInterfaceName();
             try
             {
                // store inverse (i.e., local) records getter method
                Class<? extends DataModelObject> foreignIface = info.getForeignInterface();
                method = "get" + ifaceName + "Set";
                Class<?>[] sig = new Class[] {};
                getter = foreignIface.getDeclaredMethod(method, sig);
             }
             catch (NoSuchMethodException exc)
             {
                throw new IllegalArgumentException(
                  "Invalid DMO class:  missing inverse getter [" +
                  info.getForeignInterfaceName() +
                  "." +
                  method +
                  "]");
             }
          }

          /**
           * Retrieve the set of local DMOs stored in a foreign DMO for this
           * association.
           *
           * @param   foreignDMO
           *          Foreign DMO from which to extract the set of local DMOs.
           *
           * @return  Set of local DMOs referenced by the given foreign DMO.  The
           *          set may be empty, but it will not be <code>null</code>.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public Set<Record> getLocalDMOs(Record foreignDMO)
          throws IllegalAccessException,
                 InvocationTargetException
          {
             Set<Record> set = (Set<Record>) getter.invoke(foreignDMO, (Object[]) null);

             return (set == null) ? new EmptySet<>() : set;
          }

          /**
           * Replace the set of local DMOs stored in a foreign DMO for this
           * association.
           *
           * @param   foreignDMO
           *          Foreign DMO which stores a reference to local DMOs.
           * @param   localDMOs
           *          The set of local DMOs which must replace the existing set.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public void replace(Record foreignDMO, Set<Record> localDMOs)
          throws IllegalAccessException,
                 InvocationTargetException
          {
             if (foreignDMO == null)
             {
                return;
             }

             // retrieve from the foreign DMO the current set of local DMOs which reference it
             Set<Record> set = (Set<Record>) getter.invoke(foreignDMO, (Object[]) null);

             if (set != null)
             {
                set.retainAll(localDMOs);
             }
             else
             {
                if (setter == null)
                {
                   String ifaceName = info.getLocalInterfaceName();
                   String method = null;
                   try
                   {
                      // store inverse (i.e., local) record setter method
                      Class<? extends DataModelObject> foreignIface = info.getForeignInterface();
                      method = "set" + ifaceName + "Set";
                      Class<?>[] sig = new Class[] { Set.class };
                      setter = foreignIface.getDeclaredMethod(method, sig);
                   }
                   catch (NoSuchMethodException exc)
                   {
                      throw new IllegalArgumentException(
                        "Invalid DMO class:  missing inverse setter [" +
                        info.getForeignInterfaceName() +
                         "." +
                        method +
                       "]");
                   }
                }

                setter.invoke(foreignDMO, localDMOs);
             }
          }

          /**
           * Remove the given local DMO from the set of local DMOs stored in a
           * foreign DMO for this association.
           *
           * @param   foreignDMO
           *          Foreign DMO which stores a reference to local DMOs.
           * @param   localDMO
           *          The local DMO to be removed from the existing set.
           *
           * @throws  IllegalAccessException
           *          if an access error occurs while invoking a method using
           *          reflection.
           * @throws  InvocationTargetException
           *          if a method invoked using reflection throws an exception.
           */
          public void remove(Record foreignDMO, Record localDMO)
          throws IllegalAccessException,
                 InvocationTargetException
          {
             // retrieve from the foreign DMO the current set of local DMOs which reference it
             Set<Record> set = (Set<Record>) getter.invoke(foreignDMO, (Object[]) null);

             if (set != null)
             {
                set.remove(localDMO);
             }
          }
       }
    }