IndexHelper.java
/*
** Module : IndexHelper.java
** Abstract : Helper object to manage database index information
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20080513 @38618 Created initial version. Helper object to manage
** database index information.
** 002 ECF 20080606 @38644 Support API change in DatabaseManager.
** 003 ECF 20080610 @38680 Fixed lookup of index by sort phrase. We were not
** properly handling primary key column in indexes.
** Added debug logging.
** 004 SVL 20080730 @39224 Sorters are stored by sort clauses, not by
** indexes.
** 005 CA 20080819 @39461 Support API change in DMOSorter.
** 006 ECF 20090508 @42125 Added maps of index names to property lists.
** Needed for validation improvements.
** 007 ECF 20090513 @42166 Fixed getIndexesForProperty(). Must not return
** null.
** 008 ECF 20090604 @42588 Fixed getIndexesForEntity(). Was not taking
** uniqueness flag into account properly when
** caching results.
** 009 ECF 20090707 @43100 Fixed lookupIndexesForDMO(). Instead of omitting
** merely the index containing only the primary key,
** we were omitting any index containing the primary key.
** 010 ECF 20131028 Import change.
** 011 VMN 20131128 Added map of entity to primary index and method getPrimaryIndex().
** 012 VMN 20140423 Added enhance schema name conversion support for hint "escape"
** attribute.
** 013 OM 20140509 Replaced Apache logging. Applied latest coding-style.
** Added intermediary implementation of getLegacyIndexName().
** 014 SVL 20140602 Added WHOLE_INDEX_PREFIX.
** 015 OM 20140617 Removed getLegacyIndexName() as it was a duplicate of
** TableMapper.getLegacyIndexName().
** 016 ECF 20150201 Fixed lookupIndexForSort to properly match a non-unique index which
** has had the table's primary key appended to its components.
** 017 ECF 20150801 Minor performance improvements. Some uses of String.intern() were
** redundant with those of clients of this class.
** 018 ECF 20160517 Reimplemented getPrimaryIndex to return null instead of throwing an
** exception if no primary index is found.
** 019 OM 20190226 Workaround for extent fields in word indexes.
** 020 ECF 20190814 Do not include word index in DMO index map.
** OM 20190829 Case sensitive index name lookup.
** 021 OM 20191031 Avoid NPE in some cases DTZ index components.
** 022 ECF 20200906 New ORM implementation.
** 023 OM 20200919 Improved access to dmo metadata.
** OM 20200924 P2JIndexComponent carries multiple information to avoid map lookups for them.
** OM 20201008 Detected direction of used index and stored for further access.
** OM 20201012 Force use locally cached meta information instead of map lookup.
** ECF 20210506 Use RecordBuffer.getDmoInfo() getter instead of direct field access to
** prevent NPE in proxy case.
** SVL 20230110 P2JIndex.components() provides direct access to the components array in order to
** improve performance.
** 024 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 025 CA 20240608 If a non-unique index is found for DMOSorter, force it to be unique by adding
** the primary key as last component, using ascendent direction. This is required
** because when comparing a dirty record and a query found record, the order must
** be on the primary key via pk, if the index is non-unique and both records are the
** same otherwise.
** 025 AL2 20240719 DMO sorter (used by dirty DMO logic) should take the recid in consideration.
** 026 LS 20250310 Reduced the level of the logger to INFO In lookupIndexForSort() when the
** direction of a component is not consistent with the first one.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import org.apache.commons.lang3.tuple.*;
/**
* Helper object to manage database index information. One instance of this
* class exists for each permanent, primary database. Provides methods to
* retrieve:
* <ul>
* <li>all indexed properties for a DMO type
* <li>all index names for the table backing a DMO type
* <li>all indexes in which the column backing a DMO property participates
* <li>the index name associated with an index-driven query's sort phrase
* <li>a {@link DMOSorter} instance appropriate to an index-driven query's
* sort phrase
* </ul>
* <p>
* It is anticipated that the lists of index names returned by some of the
* methods in this class will be used for purposes of serialization of
* index resources via a {@link LockManager}. Therefore, unless otherwise
* noted, these lists will be sorted in ascending, alphabetical order, to
* help prevent deadlocks in situations where all such indexes will be locked
* by a particular context. That is, if all contexts acquiring locks on these
* indexes are careful to do so in the same order, deadlocks should not occur.
*/
public final class IndexHelper
{
/** Prefix for index information string. */
public static final String WHOLE_INDEX_PREFIX = "WHOLE-INDEX,";
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(IndexHelper.class);
/** Is FINE level logging allowed? */
private static final boolean LOG_FINE = LOG.isLoggable(Level.FINE);
/** Cache of instances, mapped by database */
private static final Map<Database, IndexHelper> cache = new HashMap<>();
/** Primary (non-dirty) database with which this helper is associated */
private final Database database;
/** Map of sort clauses to pair of index names and their direction ({@code false} means reversed). */
private final Map<SortKey, Pair<String, Boolean>> indexBySort = new HashMap<>();
/** Map of property names to lists of indexes containing them */
private final Map<PropertyKey, List<String>> indexesByProperty = new HashMap<>();
/** Map of sort clauses to <code>DMOSorter</code> objects */
private final Map<SortKey, DMOSorter> sorters = new HashMap<>();
/** Map of entity keys to lists of all index names */
private final Map<EntityIndexKey, List<String>> indexesByEntity = new HashMap<>();
/** Map of entity to primary index */
private final Map<String, P2JIndex> primaryIndexByEntity = new HashMap<>();
/** Map of entity keys to maps of index names to index property names */
private final Map<EntityIndexKey, Map<String, List<String>>> indexMapsByEntity = new HashMap<>();
/** Map of entity keys to indexed properties */
private final Map<EntityIndexKey, Set<String>> propsByEntity = new HashMap<>();
/**
* Private constructor. Instances of this class are created only by the
* public factory method {@link #get(com.goldencode.p2j.persist.Database)}.
*
* @param database
* Database for which index information is needed.
*/
private IndexHelper(Database database)
{
this.database = database;
}
/**
* Get an instance of this class for the given database. If an instance
* does not exist in the shared cache, one is created and stored for future
* requests.
*
* @param database
* Database for which index information is needed.
*
* @return Instance of this class for the given database.
*/
public static IndexHelper get(Database database)
{
IndexHelper helper = null;
synchronized (cache)
{
helper = cache.get(database);
if (helper == null)
{
helper = new IndexHelper(database);
cache.put(database, helper);
}
}
return helper;
}
/**
* Retrieve a list of the names of all indexes in which the database
* column associated with the given DMO property participates.
*
* @param entity
* A DMO entity name which represents a database table.
* @param property
* A DMO property which represents a database column.
*
* @return A list of index names in ascending alphabetical order. The
* list will not contain duplicates, but it may be empty. It will
* never be <code>null</code>.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC
* driver of the associated database.
*/
public List<String> getIndexesForProperty(String entity, String property)
throws PersistenceException
{
List<String> indexes = null;
property = property.intern();
PropertyKey key = new PropertyKey(entity, property);
synchronized (indexesByProperty)
{
indexes = indexesByProperty.get(key);
}
if (indexes == null)
{
indexes = lookupIndexesForProperty(entity, property);
if (indexes == null)
{
indexes = Collections.emptyList();
}
else
{
Collections.sort(indexes);
indexes = Collections.unmodifiableList(indexes);
}
synchronized (indexesByProperty)
{
List<String> old = indexesByProperty.put(key, indexes);
if (old != null)
{
indexes = old;
indexesByProperty.put(key, old);
}
}
}
return indexes;
}
/**
* Given a sort phrase from an index-driven query, return the name of the
* index represented by the sort phrase.
*
* @param buffer
* Record buffer which contains information about the DMO entity
* associated with the sort phrase.
* @param sort
* A sort phrase which includes one or more DMO property
* references, each qualified by a DMO alias and followed by a
* keyword indicating sort direction.
*
* @return Name of the index associated with the given sort phrase, or
* <code>null</code> if no matching index can be found.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC
* driver of the associated database.
*/
public String getIndexForSort(RecordBuffer buffer, String sort)
throws PersistenceException
{
String index = null;
String entity = buffer.getEntityName();
SortKey key = new SortKey(entity, sort);
Pair<String, Boolean> indexAndDir;
synchronized (indexBySort)
{
indexAndDir = indexBySort.get(key);
if (indexAndDir == null)
{
indexAndDir = lookupIndexForSort(buffer, sort);
}
}
index = indexAndDir.getLeft();
if ("".equals(index))
{
index = null;
}
return index;
}
/**
* Retrieve primary index of given database table.
*
* @param entity
* A DMO entity name which represents a database table.
*
* @return Primary index.
*
* @throws PersistenceException
* if there is any error in retrieving primary index or more than one primary index
* is found.
*/
public P2JIndex getPrimaryIndex(String entity)
throws PersistenceException
{
P2JIndex index;
synchronized (primaryIndexByEntity)
{
index = primaryIndexByEntity.get(entity);
// nulls are permitted and mean the entity has no primary index
if (index == null && primaryIndexByEntity.containsKey(entity))
{
return null;
}
}
if (index == null)
{
String indexName = null;
DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity, null);
Class<? extends DataModelObject> dmoIface = dmoInfo.getAnnotatedInterface();
Indices lIndexes = dmoIface.getAnnotation(Indices.class);
if (lIndexes != null)
{
for (Index lIndex : lIndexes.value())
{
if (lIndex.primary())
{
if (indexName == null)
{
indexName = lIndex.name();
}
else
{
throw new PersistenceException("Table '" + entity +
"' has two primary indexes: '" +
indexName + "' and '" +
lIndex.name() +"'");
}
}
}
}
if (indexName != null)
{
Iterator<P2JIndex> iter = dmoInfo.getDatabaseIndexes();
while (iter.hasNext())
{
P2JIndex ind = iter.next();
if (indexName.equals(ind.getName()))
{
index = ind;
break;
}
}
}
synchronized (primaryIndexByEntity)
{
// cache index, even if none found (i.e., null)
primaryIndexByEntity.putIfAbsent(entity, index);
}
}
return index;
}
/**
* Retrieve a {@link DMOSorter} instance which is able to sort DMO instances according to the sort criteria
* specified by the given sort phrase.
*
* @param buffer
* Record buffer which contains information about the DMO entity associated with the sort phrase.
* @param sort
* A sort phrase which includes one or more DMO property references, each qualified by a DMO alias
* and followed by a keyword indicating sort direction.
*
* @return DMO sorter object which can sort DMOs according to the given index.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC driver of the associated database.
*/
public DMOSorter getSorterForSortPhrase(RecordBuffer buffer, String sort)
throws PersistenceException
{
Pair<String, Boolean> indexAndDir;
SortKey key = new SortKey(buffer.getEntityName(), sort);
synchronized (indexBySort)
{
indexAndDir = indexBySort.get(key);
if (indexAndDir == null)
{
// this will add an appropriate sorter to the sorters map, if possible
lookupIndexForSort(buffer, sort);
}
}
return sorters.get(key);
}
/**
* Obtain the direction of the index used to sort the buffer by a specified sort phrase.
*
* @param buffer
* Record buffer which contains information about the DMO entity associated with the sort phrase.
* @param sort
* A sort phrase which includes one or more DMO property references, each qualified by a DMO alias
* and followed by a keyword indicating sort direction.
*
* @return {@code true} if the direction is the same as the index, {@code false} if the direction is
* inversed (I.e. the result set is reversed compared to the index) and {@code null} if the sort
* phrase does not benefit from any index, or the sort phrase was not processed yet for this
* buffer.
*/
public Boolean getIndexDirection(RecordBuffer buffer, String sort)
{
SortKey key = new SortKey(buffer.getEntityName(), sort);
synchronized (indexBySort)
{
Pair<String, Boolean> indexAndDir = indexBySort.get(key);
return indexAndDir == null ? null : indexAndDir.getRight();
}
}
/**
* Retrieve a list of all index names associated with the table represented
* by the given DMO entity name, or the sublist which represents unique
* indexes only.
*
* @param entity
* A DMO entity name which represents a database table.
* @param unique
* If <code>true</code>, only unique indexes are returned; if
* <code>false</code>, all indexes are returned.
*
* @return The names of all or only unique indexes for the given entity,
* sorted in ascending alphabetical order. The list will not
* contain duplicates, but it may be empty. It will never be
* <code>null</code>.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC
* driver of the associated database.
*/
public List<String> getIndexesForEntity(String entity, boolean unique)
throws PersistenceException
{
List<String> list = null;
EntityIndexKey key = new EntityIndexKey(entity, unique);
synchronized (indexesByEntity)
{
list = indexesByEntity.get(key);
}
if (list == null)
{
list = lookupIndexesForDMO(entity, unique);
if (list != null)
{
Collections.sort(list);
list = Collections.unmodifiableList(list);
}
else
{
list = Collections.emptyList();
}
synchronized (indexesByEntity)
{
List<String> old = indexesByEntity.put(key, list);
if (old != null)
{
indexesByEntity.put(key, old);
}
}
}
return list;
}
/**
* Retrieve a mapping of index names to their component property names, for
* indexes associated with the table represented by the given DMO entity
* name.
*
* @param entity
* A DMO entity name which represents a database table.
* @param unique
* If <code>true</code>, only unique indexes are returned; if
* <code>false</code>, only non-unique indexes are returned.
*
* @return A mapping of index names to ordered lists of the names of their
* component properties, for the given DMO entity. The map
* represents only unique indexes, or only non-unique indexes,
* depending upon the <code>unique</code> parameter. The map may
* be empty, but it will never by <code>null</code>. The property
* names will not be wrapped in any functions. The map will
* iterate in ascending alphabetical order by index name.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC
* driver of the associated database.
*/
public Map<String, List<String>> getIndexMapForEntity(String entity, boolean unique)
throws PersistenceException
{
Map<String, List<String>> map = null;
EntityIndexKey key = new EntityIndexKey(entity, unique);
synchronized (indexMapsByEntity)
{
map = indexMapsByEntity.get(key);
}
if (map == null)
{
map = Collections.unmodifiableMap(lookupIndexMapForDMO(entity, unique));
synchronized (indexMapsByEntity)
{
Map<String, List<String>> old = indexMapsByEntity.put(key, map);
if (old != null)
{
indexMapsByEntity.put(key, old);
}
}
}
return map;
}
/**
* Retrieve the set of all property names for a particular DMO entity,
* which represent database columns that participate in an index on the
* entity's backing table, or the subset which represent database columns
* that participate in a unique index.
* <p>
* Note: the returned set will <em>not</em> include the reserved primary
* key property <code>id</code>.
*
* @param entity
* A DMO entity name which represents a database table.
* @param unique
* If <code>true</code>, only properties associated with unique
* indexes are returned; if <code>false</code>, all indexed
* properties are returned.
*
* @return Set of indexed property names. The set may be empty but it
* will never be <code>null</code>.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC
* driver of the associated database.
*/
public Set<String> getIndexedProperties(String entity, boolean unique)
throws PersistenceException
{
Set<String> set = null;
EntityIndexKey key = new EntityIndexKey(entity, unique);
synchronized (propsByEntity)
{
set = propsByEntity.get(key);
}
if (set == null)
{
set = lookupIndexedProperties(entity, unique);
if (set != null)
{
set = Collections.unmodifiableSet(set);
}
else
{
set = Collections.emptySet();
}
synchronized (propsByEntity)
{
Set<String> old = propsByEntity.put(key, set);
if (old != null)
{
propsByEntity.put(key, old);
}
}
}
return set;
}
/**
* Collect a list of all index names associated with the table represented
* by the given DMO entity name.
*
* @param entity
* A DMO entity name which represents a database table.
* @param unique
* If <code>true</code>, only unique indexes are returned; if
* <code>false</code>, all indexes are returned.
*
* @return The names of all indexes for the given entity, in no particular
* order, or <code>null</code> if none are found.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC
* driver of the associated database.
*/
private List<String> lookupIndexesForDMO(String entity, boolean unique)
throws PersistenceException
{
ArrayList<String> list = null;
Iterator<P2JIndex> iter = DmoMetadataManager.getDmoInfo(entity, null).getDatabaseIndexes();
outer:
while (iter.hasNext())
{
P2JIndex index = iter.next();
if (unique && !index.isUnique())
{
continue;
}
// Special case: omit index on (only) primary key.
ArrayList<P2JIndexComponent> comps = index.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
String compName = comp.getPropertyName();
if (Session.PK.equals(compName))
{
continue outer;
}
else
{
break;
}
}
if (list == null)
{
list = new ArrayList<>();
}
list.add(index.getName());
}
if (list != null)
{
list.trimToSize();
}
if (LOG_FINE)
{
LOG.log(Level.FINE,
"Indexes " + (unique ? "[unique]" : "[all]") + " for entity '" + entity + "': " + list);
}
return list;
}
/**
* Generate a mapping of index names to their component property names, for indexes associated
* with the table represented by the given DMO entity name.
*
* @param entity
* A DMO entity name which represents a database table.
* @param unique
* If {@code true}, only unique indexes are returned;
* if {@code false}, only non-unique indexes are returned.
*
* @return A mapping of index names to ordered lists of the names of their component
* properties, for the given DMO entity. The map represents only unique indexes, or
* only non-unique indexes, depending upon the {@code unique} parameter. The map may
* be empty, but it will never by {@code null}. The property names will not be wrapped
* in any functions. The map will iterate in ascending alphabetical order by index
* name.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC driver of the
* associated database.
*/
private Map<String, List<String>> lookupIndexMapForDMO(String entity, boolean unique)
throws PersistenceException
{
Map<String, List<String>> map = new TreeMap<>(); // may be empty, but never null
ArrayList<String> props = null;
Iterator<P2JIndex> iter = DmoMetadataManager.getDmoInfo(entity, null).getDatabaseIndexes();
if (iter == null)
{
return map;
}
while (iter.hasNext())
{
P2JIndex index = iter.next();
if (index.isWord())
{
continue;
}
if (index.isUnique())
{
if (!unique)
{
continue;
}
}
else if (unique)
{
continue;
}
props = new ArrayList<>();
ArrayList<P2JIndexComponent> comps = index.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
String propName = comp.getPropertyName();
if (Session.PK.equals(propName))
{
continue;
}
props.add(propName);
}
if (!props.isEmpty())
{
props.trimToSize();
map.put(index.getName(), props);
}
}
if (LOG_FINE)
{
LOG.log(Level.FINE,
"Indexes " + (unique ? "[unique]" : "[non-unique]") +
" for entity '" + entity + "': " + map);
}
return map;
}
/**
* Collect a list of the names of all indexes in which the database column associated with the
* given DMO property participates.
*
* @param entity
* A DMO entity name which represents a database table.
* @param property
* A DMO property which represents a database column.
*
* @return A list of index names in no particular order, or {@code null} if none are found.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC driver of the
* associated database.
*/
private List<String> lookupIndexesForProperty(String entity, String property)
throws PersistenceException
{
ArrayList<String> list = null;
Iterator<P2JIndex> iter = DmoMetadataManager.getDmoInfo(entity, null).getDatabaseIndexes();
if (iter == null)
{
return null;
}
while (iter.hasNext())
{
P2JIndex index = iter.next();
ArrayList<P2JIndexComponent> comps = index.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent next = comps.get(i);
String propName = next.getPropertyName();
if (propName != null && propName.equals(property))
{
if (list == null)
{
list = new ArrayList<>();
}
list.add(index.getName());
break;
}
}
}
if (list != null)
{
list.trimToSize();
}
if (LOG_FINE)
{
LOG.log(Level.FINE, "Indexes for '" + entity + ":" + property + "': " + list);
}
return list;
}
/**
* Given a sort phrase from an index-driven query, find the name of the index represented by
* the sort phrase. If a matching index is found, a {@link DMOSorter} instance is created and
* cached by the sort clause, for later retrieval using the
* {@link #getSorterForSortPhrase(RecordBuffer, String)} method.
*
* @param buffer
* Record buffer which contains information about the DMO entity associated with the
* sort phrase.
* @param sort
* A sort phrase which includes one or more DMO property references, each qualified by
* a DMO alias and followed by a keyword indicating sort direction.
*
* @return Name of the index associated with the given sort phrase, or {@code null} if no
* matching index can be found.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC driver of the
* associated database.
*/
private Pair<String, Boolean> lookupIndexForSort(RecordBuffer buffer, String sort)
throws PersistenceException
{
Pair<String, Boolean> ret = null;
String indexName = null;
Boolean direction = null;
int badDirection = -1;
SortKey key = new SortKey(buffer.getEntityName(), sort);
DmoMeta dmoInfo = buffer.getDmoInfo();
Iterator<P2JIndex> iter = dmoInfo.getDatabaseIndexes();
Class<? extends DataModelObject> dmoIface = dmoInfo.getAnnotatedInterface();
if (iter.hasNext())
{
List<SortCriterion> scList = SortCriterion.parse(sort, buffer, false);
while (iter.hasNext())
{
boolean match = true;
direction = null;
int i = 0;
badDirection = -1;
P2JIndex index = iter.next();
Iterator<P2JIndexComponent> comps = index.components(true);
Iterator<SortCriterion> crits = scList.iterator();
while (true) // loop iterators, exit only by breaks
{
boolean moreComps = comps.hasNext();
boolean moreCrits = crits.hasNext();
if (moreComps != moreCrits)
{
match = false;
break;
}
if (!moreComps) // moreCrits and moreComps are the same
{
break;
}
P2JIndexComponent next = comps.next();
String propName = next.getPropertyName();
SortCriterion crit = crits.next();
String critName = crit.getUnqualifiedName();
if (!critName.equals(propName))
{
match = false;
break;
}
i++;
boolean sameDir = next.isDescending() != crit.isAscending();
if (direction == null)
{
// detect index direction (whether the order given by sort is the same as the index)
direction = sameDir;
}
else
{
// just test whether the direction is kept and the index is a match (even if completely
// reversed). If a discrepancy is detected a warning is issued, but the index is accepted
// TODO: in the latter case there is probably a programming error. The index will not be
// matched server-side and the query will be slow!
if (direction != sameDir && badDirection == -1)
{
badDirection = i;
}
}
}
if (match)
{
indexName = index.getName().intern();
ret = Pair.of(indexName, direction);
sorters.put(key, new DMOSorter(buffer.getDatabase(),
dmoIface,
SortCriterion.parse(sort, buffer, true)));
if (badDirection != -1 && LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO,
"The sort properties (" + sort + ") match the index components (" + index +
") but the direction of " + badDirection + " component is not consistent with " +
"the first one. The index will NOT be used!");
}
break;
}
}
}
if (LOG_FINE)
{
LOG.log(Level.FINE,
"Index for sort phrase '" + sort + "': " +
(indexName != null ? indexName : "?") + " [" + buffer.getTable() + "]");
}
if (indexName == null)
{
ret = Pair.of("", true);
}
indexBySort.put(key, ret);
return ret;
}
/**
* Collect the set of all indexed property names associated with the table
* represented by the given DMO entity name.
* <p>
* Note: the returned set will <em>not</em> include the reserved primary
* key property {@code id}.
*
* @param entity
* A DMO entity name which represents a database table.
* @param unique
* If {@code true}, only properties associated with unique indexes are returned;
* if {@code false}, all indexed properties are returned.
*
* @return The names of all indexes for the given entity, or {@code null} if none are found.
*
* @throws PersistenceException
* if there is any error querying index metadata from the JDBC driver of the
* associated database.
*/
private Set<String> lookupIndexedProperties(String entity, boolean unique)
throws PersistenceException
{
Set<String> union = null;
DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity, null);
Iterator<P2JIndex> iter = dmoInfo.getDatabaseIndexes();
Map<String, String> mappedCols = DatabaseManager.getColumnToPropertyMap(
database, dmoInfo.getImplementationClass(), null, true);
if (iter == null)
{
return null;
}
while (iter.hasNext())
{
P2JIndex index = iter.next();
if (unique && !index.isUnique())
{
continue;
}
ArrayList<P2JIndexComponent> comps = index.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent next = comps.get(i);
String colName = next.getPropertyName();
if (Session.PK.equals(colName))
{
continue;
}
String propName = mappedCols.get(colName);
// detect possible DTZs; TODO: I do not like this approach (mappedCols), use the metadata instead
if (propName == null)
{
propName = mappedCols.get(colName + "_timestamp");
}
if (propName == null)
{
if (index.isWord())
{
// we just skip these properties (EXTENTS in a WORD-INDEX) for now
UnimplementedFeature.todo("Need full support for WORD-INDEX.");
continue;
}
else
{
throw new NullPointerException("Cannot map column '" + colName +
"' to a property in entity '" + entity + "'");
}
}
if (union == null)
{
union = new HashSet<>();
}
union.add(propName);
}
}
if (LOG_FINE)
{
LOG.log(Level.FINE,
"Indexed properties " + (unique ? "[unique]" : "[all]") +
" for entity '" + entity + "': " + union);
}
return union;
}
/**
* A hashable key which encapsulates a DMO entity and property name.
*/
private static class PropertyKey
{
/** DMO entity name */
private final String entity;
/** DMO property name */
private final String property;
/**
* Constructor.
*
* @param entity
* DMO entity name.
* @param property
* DMO property name.
*/
PropertyKey(String entity, String property)
{
this.entity = entity;
this.property = property;
}
/**
* Override the default implementation. Consistent with {@link
* #equals(Object)}.
*
* @return Hash code.
*
* @see java.lang.Object#hashCode()
*/
public int hashCode()
{
int result = 17;
result = 37 * result + entity.hashCode();
result = 37 * result + property.hashCode();
return result;
}
/**
* Override the default implementation.
*
* @param o
* Another instance of this class to compare with this one.
*
* @return <code>true</code> if <code>o</code> is equivalent to this
* object, else <code>false</code>.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object o)
{
if (!(o instanceof PropertyKey))
{
return false;
}
PropertyKey that = (PropertyKey) o;
return (entity.equals(that.entity) &&
property.equals(that.property));
}
}
/**
* A hashable key which encapsulates a DMO entity and a sort phrase.
*/
private static class SortKey
{
/** DMO entity name */
private final String entity;
/** Sort phrase */
private final String sort;
/**
* Constructor.
*
* @param entity
* DMO entity name.
* @param sort
* Sort phrase.
*/
SortKey(String entity, String sort)
{
this.entity = entity;
this.sort = sort;
}
/**
* Override the default implementation. Consistent with {@link #equals(Object)}.
*
* @return Hash code.
*
* @see java.lang.Object#hashCode()
*/
public int hashCode()
{
int result = 17;
result = 37 * result + entity.hashCode();
result = 37 * result + (sort == null ? 0 : sort.hashCode());
return result;
}
/**
* Override the default implementation.
*
* @param o
* Another instance of this class to compare with this one.
*
* @return <code>true</code> if <code>o</code> is equivalent to this
* object, else <code>false</code>.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object o)
{
if (!(o instanceof SortKey))
{
return false;
}
SortKey that = (SortKey) o;
return entity.equals(that.entity) && Objects.equals(this.sort, that.sort);
}
/**
* Provide a string representation of this object's state.
*
* @return String representation of this object.
*
* @see java.lang.Object#toString()
*/
public String toString()
{
return "SortKey [" + entity + ":" + sort + "]";
}
}
/**
* A hashable key which encapsulates a DMO entity and a boolean indicating
* unique vs. non-unique indexes.
*/
private static class EntityIndexKey
{
/** DMO entity name */
private final String entity;
/** Sort phrase */
private final boolean unique;
/**
* Constructor.
*
* @param entity
* DMO entity name.
* @param unique
* Uniqueness flag.
*/
EntityIndexKey(String entity, boolean unique)
{
this.entity = entity;
this.unique = unique;
}
/**
* Override the default implementation. Consistent with {@link
* #equals(Object)}.
*
* @return Hash code.
*
* @see java.lang.Object#hashCode()
*/
public int hashCode()
{
int result = 17;
result = 37 * result + entity.hashCode();
result = 37 * result + (unique ? 1 : 0);
return result;
}
/**
* Override the default implementation.
*
* @param o
* Another instance of this class to compare with this one.
*
* @return <code>true</code> if <code>o</code> is equivalent to this
* object, else <code>false</code>.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object o)
{
if (!(o instanceof EntityIndexKey))
{
return false;
}
EntityIndexKey that = (EntityIndexKey) o;
return (entity.equals(that.entity) && unique == that.unique);
}
}
}