=== modified file 'src/com/goldencode/cache/Cache.java'
--- old/src/com/goldencode/cache/Cache.java	2021-09-17 15:14:10 +0000
+++ new/src/com/goldencode/cache/Cache.java	2022-12-15 12:42:47 +0000
@@ -11,6 +11,7 @@
 **                  ExpiryCache implementation class. Added for-each loop iteration methods keys(), values(),
 **                  and entries(), plus a size() method.
 **     VVT 20210917 Javadoc fixed. 
+**     RAA 20221215 Added computeIfAbsent.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -67,6 +68,8 @@
 
 package com.goldencode.cache;
 
+import java.util.function.*;
+
 /**
  * A simple interface for a cache, with a map-like API.
  * 
@@ -116,6 +119,26 @@
    public V putIfAbsent(K key, V value);
    
    /**
+    * Function which does the following things:
+    * 1. First, it checks if the key is present in the cache.
+    * If the key is present, and a non-null value is related to the key, then it returns that value.
+    * If not (the key is not present or is present, but has a null value related), it moves on to step 2.
+    * 
+    * 2. It attempts to compute the value using the given {@code mappingFunction}.
+    * It also enters the calculated value into the map unless the calculated value is null. 
+    * 
+    * @param   key
+    *          A hashable key.
+    * @param   mappingFunction
+    *          Function that takes one argument and produces a result.
+    *          That result, unless it is null, is then put in the cache.
+    *          
+    * @return  The value at the given key, either directly retrieved or calculated.
+    */
+   public V computeIfAbsent(K key,
+            Function<? super K, ? extends V> mappingFunction);
+   
+   /**
     * Remove the entry associated with the given key from the cache.
     * 
     * @param   key

=== modified file 'src/com/goldencode/cache/ExpiryCache.java'
--- old/src/com/goldencode/cache/ExpiryCache.java	2021-10-07 04:33:10 +0000
+++ new/src/com/goldencode/cache/ExpiryCache.java	2022-12-15 12:43:38 +0000
@@ -18,6 +18,7 @@
 **                  cache grows and subsequent attempts will be made to shrink it back down. Refactored the
 **                  implementation and added methods to give some for-each loop iteration capability.
 **      OM 20210831 Replaced anonymous classes with equivalent lambdas.
+*      RAA 20221215 Added computeIfAbsent.
 */
 
 /*
@@ -312,6 +313,42 @@
    }
    
    /**
+    * Function which does the following things:
+    * 1. First, it checks if the key is present in the cache.
+    * If the key is present, and a non-null value is related to the key, then it returns that value.
+    * If not (the key is not present or is present, but has a null value related), it moves on to step 2.
+    * 
+    * 2. It attempts to compute the value using the given {@code mappingFunction}.
+    * It also enters the calculated value into the map unless the calculated value is null.
+    * When the cache is full, before adding another element, it pushes out the least desirable element(s).
+    * 
+    * @param   key
+    *          A hashable key.
+    * @param   mappingFunction
+    *          Function that takes one argument and produces a result.
+    *          That result, unless it is null, is then put in the cache.
+    *          
+    * @return  The value at the given key, either directly retrieved or calculated.
+    */
+   public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
+   {
+      
+      Objects.requireNonNull(mappingFunction);
+      V v;
+      if ((v = get(key)) == null) 
+      {
+         V newValue;
+         if ((newValue = mappingFunction.apply(key)) != null) 
+         {
+            put(key, newValue);
+            return newValue;
+         }
+      }
+
+      return v;
+    }
+   
+   /**
     * Remove the entry associated with the given key from the cache.
     * 
     * @param   key

=== modified file 'src/com/goldencode/p2j/persist/AdaptiveComponent.java'
--- old/src/com/goldencode/p2j/persist/AdaptiveComponent.java	2022-11-24 14:34:04 +0000
+++ new/src/com/goldencode/p2j/persist/AdaptiveComponent.java	2022-12-15 12:46:15 +0000
@@ -18,6 +18,8 @@
 ** 009 ECF 20200906 New ORM implementation.
 **     OM  20220704 getSortCriteria() returns updated set of SortCriterion.
 **     DDF 20221114 Changed class to public in order to use it in AdaptiveQueryTraceAspect
+**     RAA 20221215 Changed how a SortCriterion is generated. It now uses SortCriterionFactory
+**                  instead of just calling the constructor.
 */
 
 /*
@@ -422,7 +424,7 @@
       String alias = getBuffer().getDMOAlias();
       for (SortCriterion criterion : sortCriteria)
       {
-         ret.add(criterion.getAlias().equals(alias) ? criterion : new SortCriterion(criterion, alias));
+         ret.add(criterion.getAlias().equals(alias) ? criterion : SortCriterionFactory.getInstance(criterion, alias));
       }
       
       return ret;

=== modified file 'src/com/goldencode/p2j/persist/PreselectQuery.java'
--- old/src/com/goldencode/p2j/persist/PreselectQuery.java	2022-11-17 12:31:48 +0000
+++ new/src/com/goldencode/p2j/persist/PreselectQuery.java	2022-12-15 12:52:26 +0000
@@ -589,6 +589,8 @@
 **                           assume an error (query off-end) and do not try to fetch the record.
 **     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded 
 **                           as 'partial/incomplete'.   At this time, extent fields are always loaded.
+**     RAA 20221215          Changed how a SortCriterion is generated. It now uses SortCriterionFactory
+**                           instead of just calling the constructor.
 */
 
 /*
@@ -4959,7 +4961,7 @@
          }
          
          // create the SortCriterion instance and add it to the master list and to the appropriate sub-list
-         SortCriterion crit = new SortCriterion(buffer, part);
+         SortCriterion crit = SortCriterionFactory.getInstance(buffer, part);
          
          sortCriteria.add(crit);
          subSortCrit.add(crit);
@@ -5024,7 +5026,7 @@
          
          try
          {
-            workList.add(new SortCriterion(buffer, sb.toString()));
+            workList.add(SortCriterionFactory.getInstance(buffer, sb.toString()));
          }
          catch (PersistenceException exc)
          {

=== modified file 'src/com/goldencode/p2j/persist/PresortQuery.java'
--- old/src/com/goldencode/p2j/persist/PresortQuery.java	2022-07-01 17:16:22 +0000
+++ new/src/com/goldencode/p2j/persist/PresortQuery.java	2022-12-15 12:48:03 +0000
@@ -92,6 +92,8 @@
 **     OM  20201007          Optimized SortCriterion by using DmoMeta data instead of map lookups.
 **     CA  20210310          A dynamic predicate must set the default lock to NONE, instead of SHARE.
 **     OM  20220701          Dropped overriding methods which were duplicating code from super class.
+**     RAA 20221215          Changed how a SortCriterion is generated. It now uses SortCriterionFactory
+**                           instead of just calling the constructor.
 */
 
 /*
@@ -961,7 +963,7 @@
       while (iter.hasNext())
       {
          RecordBuffer buffer = iter.next().getBuffer();
-         SortCriterion crit = new SortCriterion(
+         SortCriterion crit = SortCriterionFactory.getInstance(
                buffer, buffer.getDMOAlias() + "." + DatabaseManager.PRIMARY_KEY + " asc");
          sortCriteria.add(crit);
       }

=== modified file 'src/com/goldencode/p2j/persist/SortCriterion.java'
--- old/src/com/goldencode/p2j/persist/SortCriterion.java	2022-07-27 21:57:13 +0000
+++ new/src/com/goldencode/p2j/persist/SortCriterion.java	2022-12-15 12:49:09 +0000
@@ -97,6 +97,9 @@
 **     OM  20210704          Added specialized copy constructor.
 **     AL2 20220706          Added isCompatible to filter out the sort clauses not belonging to the buffer.
 **     OM  20220727          FieldId and PropertyId are different for denormalized extent fields.
+**     RAA 20221213          Added a cache for storing the result of toSortExpression() method.
+**     RAA 20221215          Changed how a SortCriterion is generated. It now uses SortCriterionFactory
+**                           instead of just calling the constructor.
 */
 
 /*
@@ -225,6 +228,9 @@
    /** Lock for cache */
    private static final Object cacheLock = new Object();
    
+   /** Cache for storing the result of {@code toSortExpression} method. */
+   private final Vector<String> toStringCache; 
+   
    /** <code>true</code> if sort is ascending, else it is descending */
    private final boolean ascending;
    
@@ -253,13 +259,13 @@
    private final String originalName;
    
    /** Sort property name, usually qualified */
-   private String name;
+   private final String name;
    
    /** Subscript value, if any */
-   private int subscript = -1;
+   private final int subscript;
    
    /** SQL dialect which used this sort criterion; used for rtrim calls on character properties */
-   private Dialect dialect = null;
+   private final Dialect dialect;
    
    /**
     * Constructor which parses original sort component text, deriving the property name, sort
@@ -347,10 +353,11 @@
       // parse the test and extract alias, property and optional extent
       int spacePos = text.indexOf(' ');
       String rawName = ((spacePos != -1) ? text.substring(0, spacePos) : text).trim();
-      name = rawName;
+      String actualName = rawName;
       originalName = internStrings ? rawName.intern() : rawName;
       int bracketPos = rawName.indexOf("[");
       int endPos = -1;
+      int actualSubscript = -1;
       if (bracketPos >= 0)
       {
          endPos = rawName.indexOf("]");
@@ -360,17 +367,17 @@
          }
          
          // extract base name
-         name = rawName.substring(0, bracketPos);
+         actualName = rawName.substring(0, bracketPos);
       }
       
-      int dotPos = name.lastIndexOf(".");
+      int dotPos = actualName.lastIndexOf(".");
       if (dotPos < 0)
       {
          throw new PersistenceException("Sort property may not be unqualified:  " + rawName);
       }
       
-      String propName = name.substring(dotPos + 1);
-      String alias = name.substring(0, dotPos);
+      String propName = actualName.substring(dotPos + 1);
+      String alias = actualName.substring(0, dotPos);
       DmoMeta dmoMeta = DmoMetadataManager.getDmoInfo(dmoClass);
       Property propMeta = dmoMeta.getFieldInfo(propName);
       this.propertyName = propMeta.name; // already interned, anyway
@@ -385,20 +392,20 @@
             String sub = rawName.substring(bracketPos + 1, endPos);
             try
             {
-               subscript = Integer.parseInt(sub);
+               actualSubscript = Integer.parseInt(sub);
             }
             catch (NumberFormatException exc)
             {
                throw new PersistenceException("Unsupported subscript:  " + sub);
             }
             
-            name = alias + "." + propertyName;
+            actualName = alias + "." + propertyName;
          }
       }
       
       if (internStrings)
       {
-         name = name.intern();
+         actualName = actualName.intern();
       }
       
       // determine sort direction
@@ -448,6 +455,11 @@
          ignoreCase = isCharacter && !caseSens;
          computedColumnPrefix = ccPrefix;
       }
+      
+      name = actualName;
+      subscript = actualSubscript;
+      toStringCache = new Vector<>(16);
+      toStringCache.setSize(16);
    }
    
    /**
@@ -475,6 +487,8 @@
       this.originalName = other.originalName;
       this.name = alias + "." + propertyName + 
                   (subscript < 0 ? "" : "[" + subscript + "]") + (ascending ? " asc" : " desc");
+      toStringCache = new Vector<>(16);
+      toStringCache.setSize(16);
    }
    
    /**
@@ -565,7 +579,7 @@
                continue;
             }
             
-            next = new SortCriterion(dialect, sortComp.trim(), dmoInfo);
+            next = SortCriterionFactory.getInstance(dialect, sortComp.trim(), dmoInfo);
             sortCriteria.add(next);
             sortNames.add(next.getUnqualifiedName());
          }
@@ -582,7 +596,7 @@
       if (makeUnique && !sortNames.contains(DatabaseManager.PRIMARY_KEY))
       {
          String sortComp = dmoAlias + "." + DatabaseManager.PRIMARY_KEY + " " + (pKeyAscend ? "asc" : "desc");
-         sortCriteria.add(new SortCriterion(dialect, sortComp, dmoInfo));
+         sortCriteria.add(SortCriterionFactory.getInstance(dialect, sortComp, dmoInfo));
       }
       
       // eliminate superfluous sort components if sort criteria define a superset of a unique
@@ -930,6 +944,19 @@
       // ASC:   ASC + NULLS LAST    (CASE WHEN [Order] IS NULL THEN 1 ELSE 0 END), [Order] ASC  
       // DESC:  DESC + NULLS FIRST  (CASE WHEN [Order] IS NULL THEN 0 ELSE 1 END), [Order] DESC
       
+      int cacheKey = 0;
+      cacheKey += (invert ? 1 << 3 : 0);
+      cacheKey += (useTableAlias ? 1 << 2 : 0);
+      cacheKey += (uniqueAlias ? 1 << 1 : 0);
+      cacheKey += (doSpecificProcessing ? 1 : 0);
+      String cacheResult = null;
+      cacheResult = toStringCache.get(cacheKey);
+      
+      if (cacheResult != null)
+      {
+         return cacheResult;
+      }
+      
       boolean mssqlFix = dialect instanceof P2JSQLServer2008Dialect;
       StringBuilder buf = new StringBuilder();
       StringBuilder sortExpr = mssqlFix ? new StringBuilder() : null;
@@ -983,7 +1010,9 @@
          buf.append(ascending ? "asc" : "desc");
       }
       
-      return buf.toString();
+      String result = buf.toString();
+      toStringCache.setElementAt(result, cacheKey);
+      return result;
    }
    
    /**

=== added file 'src/com/goldencode/p2j/persist/SortCriterionFactory.java'
--- old/src/com/goldencode/p2j/persist/SortCriterionFactory.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/SortCriterionFactory.java	2022-12-15 12:15:28 +0000
@@ -0,0 +1,291 @@
+/*
+** Module   : SortCriterionFactory.java
+** Abstract : Factory for an object which encapsulates a single "order by" sort criterion
+**
+** Copyright (c) 2004-2022, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
+** 001 RAA 20221214          Created initial version.
+*/
+
+/*
+** 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.logging.*;
+
+import com.goldencode.cache.*;
+import com.goldencode.p2j.persist.dialect.*;
+import com.goldencode.p2j.persist.orm.*;
+import com.goldencode.p2j.util.*;
+
+/**
+ * Factory design pattern for a {@link SortCriterion}. Its purpose is to quickly retrieve an already created
+ * {@code SortCriterion}. If that does not happen, the instance is created and then stored in cache. 
+ */
+public final class SortCriterionFactory
+{
+   /** Logger for {@code SortCriterionFactory}. */
+   private static final Logger log = LogHelper.getLogger(SortCriterionFactory.class.getName());
+   
+   /** Cache for quickly returning an already created {@code SortCriterion}. */
+   static final Cache<Key, SortCriterion> cache = new LRUCache<>(2048);
+   
+   /**
+    * Function which simulates the behavior of the {@link SortCriterion} constructor with
+    * {@link RecordBuffer} and {@code String} parameters.
+    * 
+    * It is strongly advised to use this function because the instance has a chance to be retrieved
+    * from cache, meaning that it would be faster than creating a new one.
+    * 
+    * @param   buffer
+    *          Record buffer.
+    * @param   text
+    *          Text representation of sort component, as provided by query.
+    *          
+    * @return  The instance of the requested {@code SortCriterion}.
+    */
+   static SortCriterion getInstance(RecordBuffer buffer, String text)
+   throws PersistenceException
+   {
+      return getInstance(buffer.getDialect(), text, buffer.getDmoInfo(), false);
+   }
+   
+   /**
+    * Function which simulates the behavior of the {@link SortCriterion} constructor with
+    * {@link Dialect}, {@code String} and {@link DmoMeta} parameters.
+    * 
+    * It is strongly advised to use this function because the instance has a chance to be retrieved
+    * from cache, meaning that it would be faster than creating a new one.
+    * 
+    * @param   dialect
+    *          Database dialect.
+    * @param   text
+    *          Text representation of sort component, as provided by query.
+    * @param   dmoInfo
+    *          Metadata for the table.
+    *          
+    * @return  The instance of the requested {@code SortCriterion}.
+    */
+   static SortCriterion getInstance(Dialect dialect, String text, DmoMeta dmoInfo)
+   throws PersistenceException
+   {
+      return getInstance(dialect, text, dmoInfo, true);
+   }
+   
+   /**
+    * Function which simulates the behavior of the {@link SortCriterion} constructor with
+    * {@link Dialect}, {@code String}, {@link DmoMeta} and {@code boolean} parameters.
+    * 
+    * It is strongly advised to use this function because the instance has a chance to be retrieved
+    * from cache, meaning that it would be faster than creating a new one.
+    * 
+    * @param   dialect
+    *          Database dialect.
+    * @param   text
+    *          Text representation of sort component, as provided by query.
+    * @param   dmoInfo
+    *          Metadata for the table.
+    * @param   internStrings
+    *          {@code true} to intern string fields, else {@code false}.
+    *          
+    * @return  The instance of the requested {@code SortCriterion}.
+    */
+   static SortCriterion getInstance(Dialect dialect, String text,
+                                    DmoMeta dmoInfo, boolean internStrings)
+   throws PersistenceException
+   {
+      Key key = new Key(dialect, text, dmoInfo, internStrings);
+      synchronized (cache)
+      {
+         return cache.computeIfAbsent(key, k -> {
+            try
+            {
+               return new SortCriterion(dialect, text, dmoInfo, internStrings);
+            }
+            catch (PersistenceException e)
+            {
+               if (log.isLoggable(Level.WARNING))
+               {
+                  log.log(Level.WARNING, "Could not create a new SortCriterion", e);
+               }
+            }
+            
+            return null;
+         });
+      }
+   }
+   
+   /**
+    * Function which simulates the behavior of the {@link SortCriterion} constructor with
+    * {@code SortCriterion} and {@code String} parameters.
+    * 
+    * There are two reasons as to why this method does not involve caching:
+    * 1. It is rarely used.
+    * 2. Adding another element to the cache key (alias) and storing multiple {@code SortCriterion} with
+    * just different aliases might provide more negative than positive aspects. It would make the
+    * common cases (>95%) more difficult to obtain and the ones that are rarer easier to find.
+    * 
+    * @param   other
+    *          The source {@code SortCriterion} to copy.
+    * @param   alias
+    *          The new alias to be used.
+    *          
+    * @return  The instance of the requested {@code SortCriterion}.
+    */
+   static SortCriterion getInstance(SortCriterion other, String alias)
+   {
+      return new SortCriterion(other, alias);
+   }
+   
+   /**
+    * Key for cached {@code SortCriterion}s.
+    */
+   private static class Key
+   {
+      /** Database dialect. */
+      private final Dialect dialect;
+      
+      /** Text representation of sort component, as provided by query. */
+      private final String text;
+      
+      /** Metadata for the table. */
+      private final DmoMeta dmoInfo;
+      
+      /** Database dialect. */
+      private final boolean internStrings;
+      
+      /** {@code true} to intern string fields, else {@code false}. */
+      private final int hashCode;
+      
+      /**
+       * Constructor.
+       * 
+       * @param   dialect
+       *          Database dialect.
+       * @param   text
+       *          Text representation of sort component, as provided by query.
+       * @param   dmoInfo
+       *          Metadata for the table.
+       * @param   internStrings
+       *          {@code true} to intern string fields, else {@code false}.
+       */
+      Key(Dialect dialect, String text, DmoMeta dmoInfo, boolean internStrings)
+      {
+         this.dialect = dialect;
+         this.text = text;
+         this.dmoInfo = dmoInfo;
+         this.internStrings = internStrings;
+         
+         int result = 17;
+         result = 37 * result + (dialect == null ? 0 : dialect.hashCode());
+         result = 37 * result + (text == null ? 0 : text.hashCode());
+         result = 37 * result + (dmoInfo == null ? 0 : dmoInfo.hashCode());
+         result = 37 * result + (internStrings == false ? 0 : 1);
+         
+         this.hashCode = result;
+      }
+      
+      /**
+       * Override of the default hash code algorithm. Consistent with {@link #equals(Object)}.
+       * 
+       * @return  Hash code.
+       */
+      @Override
+      public int hashCode()
+      {
+         return hashCode;
+      }
+      
+      /**
+       * Override of the default equivalence algorithm. Consistent with {@link #hashCode()}.
+       * 
+       * return   {@code true} if this object is equivalent to the given object, else {@code
+       *          false}.
+       */
+      @Override
+      public boolean equals(Object o)
+      {
+         if (!(o instanceof Key))
+         {
+            return false;
+         }
+         
+         Key that = (Key) o;
+         if (this.dialect != that.dialect)
+         {
+            return false;
+         }
+         
+         if ((this.text == null && that.text != null) ||
+             (this.text != null && that.text == null) ||
+             (!this.text.equals(that.text)))
+         {
+            return false;
+         }
+         
+         if (this.dmoInfo != that.dmoInfo)
+         {
+            return false;
+         }
+         
+         if (this.internStrings != that.internStrings)
+         {
+            return false;
+         }
+         
+         return true;
+      }
+   }
+}

