/**********************************************************************
Copyright (c) 2004 Erik Bengtson and others. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributors:
2003 Marco Schulze - replaced catch(NotPersistenceCapable) with TypeManager.isSupportedType()
2004 Andy Jefferson - added restriction on Discriminator, added check on subclass using same table
2004 Andy Jefferson - added comments. Added fix to inverse map with non primitive keys
2004 Andy Jefferson - added way of enforcing inclusion of "JPOXMETADATA"
2005 Andy Jefferson - catch "subclass-table" cases where user has a strange inheritance setup
2005 Andy Jefferson - added discriminator for element table for 1-N join table cases
2007 Andy Jefferson - removed "AssociationEnd" for simplicity
...
**********************************************************************/
package org.jpox.store.rdbms.query;
import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.Set;
import org.jpox.ClassLoaderResolver;
import org.jpox.exceptions.JPOXUserException;
import org.jpox.metadata.AbstractClassMetaData;
import org.jpox.metadata.DiscriminatorMetaData;
import org.jpox.metadata.DiscriminatorStrategy;
import org.jpox.store.StoreManager;
import org.jpox.store.mapped.DatastoreClass;
import org.jpox.store.mapped.DatastoreContainerObject;
import org.jpox.store.mapped.DatastoreIdentifier;
import org.jpox.store.mapped.IdentifierFactory;
import org.jpox.store.mapped.expression.LogicSetExpression;
import org.jpox.store.mapped.expression.MetaDataStringLiteral;
import org.jpox.store.mapped.expression.NullLiteral;
import org.jpox.store.mapped.expression.QueryExpression;
import org.jpox.store.mapped.expression.ScalarExpression;
import org.jpox.store.mapped.mapping.JavaTypeMapping;
import org.jpox.store.mapped.query.AbstractIteratorStatement;
import org.jpox.store.query.IncompatibleQueryElementTypeException;
import org.jpox.store.rdbms.table.SCOTable;
import org.jpox.util.JPOXLogger;
import org.jpox.util.StringUtils;
/**
* Class to generate a QueryStatement for iterating through the elements of a Set.
* This can be in an Extent/Query for example where the user has selected a candidate class
* and to include/exclude subclasses. The generated query statement typically contains UNIONs
* for each of the possible classes involved.
* <P>
* To give an example, lets assume that we have class A which is the candidate
* and this has a subclass B. We want to find all objects of the candidate type
* and optionally its subclasses and we want information about what type the object
* is (A or B). The query will be of the form
* <PRE>
* SELECT THIS.A_ID,'org.jpox.samples.A' as JPOXMETADATA, THIS.COLUMN1, THIS.COLUMN2
* FROM A THIS
* LEFT OUTER JOIN B SUBELEMENT0 ON SUBELEMENT0.B_ID = THIS.A_ID
* WHERE SUBELEMENT0.B_ID IS NULL
*
* UNION
*
* SELECT THIS.A_ID,'org.jpox.samples.B' as JPOXMETADATA, THIS.COLUMN1, THIS.COLUMN2
* FROM A THIS
* INNER JOIN B 'ELEMENT' ON 'ELEMENT'.B_ID = THIS.A_ID
* </PRE>
* <H3>Extent</H3>
* Here we use the above arrangement, with the "source" being the element table and the
* mapping being the ID column mapping.
*
* <H3>"JoinTable" Relationship</H3>
* Here the join table is the candidate table, and the "source" would be the element table,
* and the mapping in the join table to join to this element table PK column.
*
* <H3>"FK" Relationship</H3>
* Here we use the same arrangement as the Extent case. The "source" is the element table,
* and the mapping being the ID column mapping.
*
* <H3>Map "Key" table</H3>
* Here we have the key table as the candidate, and we join to the value table. So the "source" is
* the value table, with the mapping being the mapping of the key column in the value table.
*
* <H3>Primitive key/value in SCO table</H3>
* This also supports retrieval of keys or values of a map that are stored as a column in a different table.
* TODO Add full documentation for all combinations supported.
* TODO Commonise the code with DiscriminatorIteratorStatement
*
* @version $Revision: 1.9 $
*/
public class UnionIteratorStatement extends AbstractIteratorStatement
{
/** Type of any source. */
Class sourceType = null;
/** Mapping for any source. */
JavaTypeMapping sourceMapping = null;
/** Table for any source. */
DatastoreContainerObject sourceTable = null;
/** Whether to join to any source. */
boolean sourceJoin = false;
/**
* Whether to join to exclude target subclasses (when exist).
* With "complete-table" we dont need to exclude them since the table just contains that class.
*/
boolean joinToExcludeTargetSubclasses = true;
/** Whether to include the JPOXMETADATA tag in the query to distinguish classes. */
Boolean withMetadata = null;
/** Whether to allow null entries in the iterator. */
boolean allowsNull = false;
/**
* Constructor.
* @param clr The ClassLoaderResolver
* @param candidateType the candidate is who we are looking to
* @param includeSubclasses if the subclasses of the candidate should be included in the result
* @param storeMgr the store manager
* @param sourceType Type of the source
* @param sourceMapping Mapping for the source
* @param sourceTable table for the source
* @param sourceJoin Whether to join to the source
* @param withMetadata if has subclasses, include the metadata in the query
* @param joinToExcludeTargetSubclasses Whether to exclude any target subclasses from this SELECT
* @param allowsNull Whether to allow for null values returned by the iterator
*/
public UnionIteratorStatement(ClassLoaderResolver clr,
Class candidateType,
boolean includeSubclasses,
StoreManager storeMgr,
Class sourceType,
JavaTypeMapping sourceMapping,
DatastoreContainerObject sourceTable,
boolean sourceJoin,
Boolean withMetadata,
boolean joinToExcludeTargetSubclasses,
boolean allowsNull)
{
super(candidateType, clr, includeSubclasses, storeMgr);
this.sourceType = sourceType;
this.sourceMapping = sourceMapping;
this.sourceTable = sourceTable;
this.sourceJoin = sourceJoin;
this.joinToExcludeTargetSubclasses = joinToExcludeTargetSubclasses;
this.withMetadata = withMetadata;
this.allowsNull = allowsNull;
// check if the element target is assignable from the candidate
if (!sourceType.isAssignableFrom(candidateType))
{
if (candidateType.isInterface())
{
if (!candidateType.isAssignableFrom(sourceType))
{
throw new IncompatibleQueryElementTypeException(candidateFullClassName, sourceType.getName());
}
}
else
{
throw new IncompatibleQueryElementTypeException(sourceType.getName(), candidateType.getName());
}
}
// Give warning if table is incorrect for candidate and subclasses not included in query.
if (!sourceTable.equals(candidateTable) && !includeSubclasses)
{
JPOXLogger.DATASTORE.warn(LOCALISER.msg("033003", this, candidateFullClassName));
}
}
/**
* Accessor for the Query Statement.
* @param candidateAlias Alias for the candidate
* @return The Query Statement for iterating through objects
*/
public QueryExpression getQueryStatement(DatastoreIdentifier candidateAlias)
{
if (storeMgr.getOMFContext().getTypeManager().isSupportedType(candidateFullClassName))
{
// SCO candidates, embedded in the source so just select the source table
QueryExpression qs = dba.newQueryStatement(sourceTable, candidateAlias, clr);
qs.select(sourceMapping);
return qs;
}
else
{
// FCO candidates
if (candidateTable == null)
{
// Candidate class has no table! so see if it has one subclass (with table) only and use that
AbstractClassMetaData acmd = storeMgr.getOMFContext().getMetaDataManager().getMetaDataForClass(candidateFullClassName, clr);
AbstractClassMetaData subclassCmds[] = storeMgr.getClassesManagingTableForClass(acmd, clr);
if (subclassCmds != null && subclassCmds.length == 1)
{
// Candidate uses "subclass-table" and only one subclass
candidateTable = storeMgr.getDatastoreClass(subclassCmds[0].getFullClassName(), clr);
candidateFullClassName = subclassCmds[0].getFullClassName();
}
else
{
throw new JPOXUserException("Attempt to create iterator for class " + candidateFullClassName +
" that is using \"subclass-table\" inheritance strategy and that doesnt have only 1 subclass." +
" This is not currently supported");
}
}
if (!includeSubclasses && withMetadata == null)
{
// No need to include the JPOXMETADATA if no subclasses considered
withMetadata = Boolean.FALSE;
}
// Find set of possible candidates
Set subclasses = null;
if (includeSubclasses)
{
subclasses = storeMgr.getSubClassesForClass(candidateFullClassName, true, clr);
Iterator iter = subclasses.iterator();
while (iter.hasNext())
{
String subclassName = (String)iter.next();
try
{
Class subclass = clr.classForName(subclassName);
if (Modifier.isAbstract(subclass.getModifiers()))
{
// Remove since abstract hence not instantiable
iter.remove();
}
}
catch (Exception e)
{
// Remove since class not found
iter.remove();
}
}
}
// Create the main query
String queryCandidateClassName = candidateFullClassName;
try
{
Class candidateClass = clr.classForName(candidateFullClassName);
if (Modifier.isAbstract(candidateClass.getModifiers()))
{
// Candidate is abstract so not instantiable so try to find a subclass using same table
Iterator iter = subclasses.iterator();
while (iter.hasNext())
{
String subclassName = (String)iter.next();
DatastoreClass subclassTable = storeMgr.getDatastoreClass(subclassName, clr);
if (subclassTable == candidateTable)
{
// Input candidate is abstract but this subclass uses the same table so swap it for that
queryCandidateClassName = subclassName;
iter.remove(); // Not needed as subclass now since used as primary class
break;
}
}
}
}
catch (Exception e) {}
QueryExpression stmt = getQueryForElement(queryCandidateClassName, candidateTable, false, candidateAlias);
if (includeSubclasses)
{
// Add a select for each subclass elements, and union the selects
Iterator iterator = subclasses.iterator();
while (iterator.hasNext())
{
String subCandidateType = (String) iterator.next();
DatastoreClass subCandidateTable = storeMgr.getDatastoreClass(subCandidateType, clr);
if (subCandidateTable != null)
{
// Add UNION to any subclasses that have their own table
// This omits those using "subclass-table".
// Those will have their own subclasses where they store their objects.
QueryExpression stmt_subclass = getQueryForElement(subCandidateType, subCandidateTable,
false, candidateAlias);
stmt.union(stmt_subclass);
}
}
}
if (allowsNull && sourceTable instanceof SCOTable)
{
// JoinTable case where we select the join table and join to the element table, and need to allow for nulls
// Add a SELECT for nulls in the join table, and UNION it to the main query
// TODO Add statement for nulls [CORE-2994]
/**
* SELECT NULL AS JPOXMETADATA
* FROM JOINTBL THIS
* LEFT OUTER JOIN ELEM_TBL ELEM1 ON ELEM1.ID = THIS.ELEM_ID
* WHERE THIS.ELEM_ID IS NULL
*/
QueryExpression nullStmt = getQueryForElement(candidateFullClassName,
storeMgr.getDatastoreClass(candidateFullClassName, clr), true, candidateAlias);
ScalarExpression elemIdExpr = sourceMapping.newScalarExpression(nullStmt, nullStmt.getMainTableExpression());
nullStmt.andCondition(new NullLiteral(nullStmt).eq(elemIdExpr));
stmt.union(nullStmt);
}
return stmt;
}
}
/**
* Utility method to generate a select statement to find objects of subclasses of the element class in the Set.
* <p>
* In simple terms this creates a SELECT like
* <pre>
* SELECT THIS.A_ID,'org.jpox.samples.A' as JPOXMETADATA, THIS.COLUMN1, THIS.COLUMN2
* FROM A THIS
* LEFT OUTER JOIN B SUBELEMENT0 ON SUBELEMENT0.B_ID = THIS.A_ID
* WHERE SUBELEMENT0.B_ID IS NULL
* </pre>
* The actual query will vary if this subclass has its own subclasses. If
* there are no subclasses of this class then the LEFT OUTER JOIN parts will
* not be present, nor will the SUBELEMENT_? parts of the WHERE clause.
* </p>
* <p>
* If the target element shares the same table as the source element then
* no (left outer) join will be made and it will be a simple select.
* </p>
* <p>
* If a discriminator column is present on the table, it will be used to
* separate the classes in that table rather than using the left outer join
* strategy
* </p>
* @param targetElementType The Class for the element
* @param targetElementTable The table for this class (will not be null)
* @param allowNull Whether this query is to allow null elements (means we use LEFT OUTER JOIN)
* @return QueryStatement
*/
private QueryExpression getQueryForElement(String targetElementType, DatastoreClass targetElementTable,
boolean allowNull, DatastoreIdentifier candidateId)
{
QueryExpression stmt;
JavaTypeMapping discriminatorMapping = null;
DiscriminatorMetaData discriminatorMetaData = null;
LogicSetExpression discriminatorTableExpr = null;
if (sourceTable instanceof SCOTable)
{
// * Selecting the join table of a JoinTable (normal) relationship, and joining to the element table
stmt = dba.newQueryStatement(sourceTable, candidateId, clr);
// Add inner joins to all classes above up to elementType and across to the join table
DatastoreIdentifier targetTableIdentifier = joinSourceToTargetElement(stmt, targetElementTable, allowNull);
discriminatorMapping = targetElementTable.getDiscriminatorMapping(false);
discriminatorMetaData = targetElementTable.getDiscriminatorMetaData();
discriminatorTableExpr = stmt.getTableExpression(targetTableIdentifier);
// Add left outer joins to exclude any target element subclasses
if (joinToExcludeTargetSubclasses)
{
joinToExcludeTargetWhenSubElementsExists(stmt, sourceMapping, targetElementType);
}
}
else
{
// * Selecting FCO objects directly (Extents etc)
// * Selecting the element table of a ForeignKey (inverse) relationship
stmt = dba.newQueryStatement(candidateTable, candidateId, clr);
discriminatorMapping = sourceTable.getDiscriminatorMapping(false);
discriminatorMetaData = sourceTable.getDiscriminatorMetaData();
discriminatorTableExpr = stmt.getMainTableExpression();
// in case of the elementType is a subClass of the element for the candidateTable
// if the root (candidate) element type is not the same as the current target element type
// joins the root to the current element
if ((!targetElementTable.toString().equals(sourceTable.toString()) &&
!candidateTable.getType().equals(targetElementType)) ||
sourceJoin)
{
// Add inner joins to all classes above up to elementType and across to the join table
if (sourceJoin)
{
joinTargetToSourceElement(stmt, targetElementTable, false);
}
else
{
joinSourceToTargetElement(stmt, targetElementTable, false);
}
}
if (joinToExcludeTargetSubclasses)
{
joinToExcludeTargetWhenSubElementsExists(stmt, candidateTable.getIDMapping(), targetElementType);
}
}
if (discriminatorMapping != null && discriminatorMetaData.getStrategy() != DiscriminatorStrategy.NONE)
{
// Restrict to valid discriminator values where we have a discriminator specified on this table
String discriminatorValue = targetElementType;
if (discriminatorMetaData.getStrategy() == DiscriminatorStrategy.VALUE_MAP)
{
// Get the MetaData for the target class since that holds the "value"
AbstractClassMetaData targetCmd = storeMgr.getOMFContext().getMetaDataManager().getMetaDataForClass(targetElementType, clr);
discriminatorValue = targetCmd.getInheritanceMetaData().getDiscriminatorMetaData().getValue();
}
ScalarExpression discrExpr = discriminatorMapping.newScalarExpression(stmt, discriminatorTableExpr);
ScalarExpression discrVal = discriminatorMapping.newLiteral(stmt, discriminatorValue);
stmt.andCondition(discrExpr.eq(discrVal));
}
if (withMetadata == null || withMetadata.booleanValue())
{
// Select JPOXMETADATA if required
selectMetadata(stmt, targetElementType, allowNull);
}
return stmt;
}
/**
* Joins the container table (source) to the element table (target).
* This is used when the source mapping is from target to the source (normal
* relationships where the target is SCO).
* @param stmt The Query Statement
* @param elementTargetTable the table that contains the elements
* @param leftOuterJoin whether to do a LEFT OUTER join, instead of INNER join
* @return Table identifier for the target table in the query
*/
private DatastoreIdentifier joinSourceToTargetElement(QueryExpression stmt, DatastoreClass elementTargetTable,
boolean leftOuterJoin)
{
// TODO Make this identifier specifiable as input to the iterator statement
DatastoreIdentifier targetTableIdentifier =
stmt.getStoreManager().getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, "ELEMENT");
LogicSetExpression teTargetElement = stmt.newTableExpression(elementTargetTable, targetTableIdentifier);
ScalarExpression sourceExpr = sourceMapping.newScalarExpression(stmt, stmt.getMainTableExpression());
ScalarExpression targetExpr =
elementTargetTable.getIDMapping().newScalarExpression(stmt, teTargetElement);
if (leftOuterJoin)
{
stmt.leftOuterJoin(sourceExpr, targetExpr, teTargetElement, true);
}
else
{
stmt.innerJoin(sourceExpr, targetExpr, teTargetElement, true);
}
return targetTableIdentifier;
}
/**
* Joins the element table (target) to the container table (source).
* This is used when the source mapping is from the source to the target.
* @param stmt The Query Statement
* @param elementTargetTable the table that contains the elements
* @param leftOuterJoin whether to do a LEFT OUTER join, instead of INNER join
*/
private void joinTargetToSourceElement(QueryExpression stmt, DatastoreClass elementTargetTable,
boolean leftOuterJoin)
{
// TODO Make this identifier specifiable as input to the iterator statement
DatastoreIdentifier sourceTableIdentifier =
stmt.getStoreManager().getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, "VALUE");
LogicSetExpression teSourceElement = stmt.newTableExpression(sourceTable, sourceTableIdentifier);
ScalarExpression sourceExpr = sourceMapping.newScalarExpression(stmt, teSourceElement);
ScalarExpression targetExpr =
elementTargetTable.getIDMapping().newScalarExpression(stmt, stmt.getMainTableExpression());
if (leftOuterJoin)
{
stmt.leftOuterJoin(sourceExpr, targetExpr, teSourceElement, true);
}
else
{
stmt.innerJoin(sourceExpr, targetExpr, teSourceElement, true);
}
}
/**
* Join to exclude targetElementType instances when exists sub elements
* instances of targetElementType. This is made to avoid returning
* targetElement when there are sub classes instances of targetElement
* <pre>
* ...
* ELEMENT_TABLE TARGETELEMENT
* LEFT OUTER JOIN SUB_TARGETELEMENT ON TARGETELEMENT.ELEMENT_ID = SUBCLASS_TARGETELEMENT.SUB_TARGETELEMENT_ID
* WHERE SUB_TARGETELEMENT.SUB_TARGETELEMENT_ID IS NULL
* ...
* </pre>
* <p>When the subelements are using the same table as the target element, no
* exclusion is performed</p>
*
* @param stmt The Query Statement
* @param targetElementMapping the JavaTypeMapping for the target element for joining to
* @param targetElementType The target element
*/
private void joinToExcludeTargetWhenSubElementsExists(
QueryExpression stmt,
JavaTypeMapping targetElementMapping,
String targetElementType)
{
// Add LEFT OUTER JOIN to all direct subclasses of the targetRootElementQCL - if any
Iterator iterTargetSubElementType = storeMgr.getSubClassesForClass(targetElementType, false, stmt.getClassLoaderResolver()).iterator();
int subSequenceIdentifier = 0;
while (iterTargetSubElementType.hasNext())
{
String targetSubElementType = (String) iterTargetSubElementType.next();
QueryExpression targetQS;
DatastoreClass cbtTargetSubElementType = storeMgr.getDatastoreClass(targetSubElementType, clr);
DatastoreIdentifier tiTargetSubElementType;
LogicSetExpression teTargetSubElementType;
ScalarExpression targetSubElementTypeExpr;
DatastoreClass[] targetSubElementTypes = null;
if (cbtTargetSubElementType == null)
{
AbstractClassMetaData targetSubCmd =
storeMgr.getOMFContext().getMetaDataManager().getMetaDataForClass(targetSubElementType, stmt.getClassLoaderResolver());
AbstractClassMetaData[] targetSubCmds = storeMgr.getClassesManagingTableForClass(targetSubCmd, clr);
targetSubElementTypes = new DatastoreClass[targetSubCmds.length];
for (int i=0;i<targetSubCmds.length;i++)
{
targetSubElementTypes[i] = storeMgr.getDatastoreClass(targetSubCmds[i].getFullClassName(), clr);
}
}
else
{
targetSubElementTypes = new DatastoreClass[1];
targetSubElementTypes[0] = cbtTargetSubElementType;
}
for (int i=0;i<targetSubElementTypes.length;i++)
{
// Add leftOuterJoin to sub-target elements if they are not in the same table as the target
if (!targetSubElementTypes[i].toString().equals(storeMgr.getDatastoreClass(targetElementType, clr).toString()))
{
tiTargetSubElementType =
storeMgr.getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE,
"SUBELEMENT" + (subSequenceIdentifier++));
// create a new statement
targetQS = dba.newQueryStatement(targetSubElementTypes[i], tiTargetSubElementType, stmt.getClassLoaderResolver());
// table expression from the table identifier
teTargetSubElementType = targetQS.newTableExpression(targetSubElementTypes[i], tiTargetSubElementType);
JavaTypeMapping targetMapping = targetSubElementTypes[i].getIDMapping();
ScalarExpression targetElementExpr =
targetElementMapping.newScalarExpression(stmt, stmt.getMainTableExpression());
targetSubElementTypeExpr = targetMapping.newScalarExpression(stmt, teTargetSubElementType);
stmt.leftOuterJoin(targetElementExpr,targetSubElementTypeExpr, teTargetSubElementType, true);
ScalarExpression seTargetSubElementType = targetMapping.newScalarExpression(targetQS, teTargetSubElementType);
stmt.andCondition(new NullLiteral(stmt).eq(seTargetSubElementType));
}
}
}
}
/**
* Adds an expression to the statement. The expression is a metadata that
* identifies an element in a hierarchy level of classes. This will appear
* in the SELECT as "'org.jpox.samples.MyClass ' AS JPOXMETADATA".
*
* @param stmt Query Statement
* @param pcClass The Persistence Capable class
* @param allowNull Whether this is for null elements
*/
private void selectMetadata(QueryExpression stmt, String pcClass, boolean allowNull)
{
// If we are looking for subclasses, we need to add the class to the
// select, and unfortunately some RDBMS set an odd limit on the first
// row in the output set column for the length of the name column.
// This fix looks through the classes concerned in our query and takes
// the longest name and pads all other names to this length. This is
// used in the "'package.classname' AS JPOXMETADATA" parts of the query.
int maxClassNameLen = candidateFullClassName.length();
boolean hasSubClasses = false;
Iterator iterator = storeMgr.getSubClassesForClass(candidateFullClassName, true, clr).iterator();
if (iterator.hasNext())
{
hasSubClasses = true;
while (iterator.hasNext())
{
String nextClass = (String) iterator.next();
if (nextClass.length() > maxClassNameLen)
{
maxClassNameLen = nextClass.length();
}
}
}
// Include metadata if explicitly told to include it, or if subclasses present
if ((withMetadata == null && hasSubClasses) ||
(withMetadata != null && withMetadata.booleanValue()))
{
if (allowNull)
{
NullLiteral nullLtl = new NullLiteral(stmt);
nullLtl.as(MetaDataStringLiteral.QUERY_META_DATA);
stmt.selectScalarExpression(nullLtl);
}
else
{
String classname = pcClass;
if (maxClassNameLen > classname.length())
{
classname = StringUtils.leftAlignedPaddedString(pcClass, maxClassNameLen);
}
stmt.selectScalarExpression(new MetaDataStringLiteral(stmt,classname));
}
}
}
}