blob: 7c0f79a7126fa5aea04349ffcabf0611ec8537c4 [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2020 IBM Corporation. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
// 11/10/2011-2.4 Guy Pelletier
// - 357474: Address primaryKey option from tenant discriminator column
// 10/01/2018: Will Dazey
// - #253: Add support for embedded constructor results with CriteriaBuilder
package org.eclipse.persistence.queries;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Vector;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.ExpressionOperator;
import org.eclipse.persistence.internal.expressions.FunctionExpression;
import org.eclipse.persistence.internal.expressions.MapEntryExpression;
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.NonSynchronizedSubVector;
import org.eclipse.persistence.internal.queries.JoinedAttributeManager;
import org.eclipse.persistence.internal.queries.ReportItem;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedInvokeConstructor;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.mappings.AggregateObjectMapping;
import org.eclipse.persistence.mappings.Association;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.DirectCollectionMapping;
import org.eclipse.persistence.mappings.foundation.AbstractColumnMapping;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.sessions.Session;
/**
* <b>Purpose</b>: A single row (type) result for a ReportQuery<p>
*
* <b>Description</b>: Represents a single row of attribute values (converted using mapping) for
* a ReportQuery. The attributes can be from various objects.
*
* <b>Responsibilities</b>:<ul>
* <li> Converted field values into object attribute values.
* <li> Provide access to values by index or item name
* </ul>
*
* @author Doug Clarke
* @since TOPLink/Java 2.0
*/
public class ReportQueryResult implements Serializable, Map {
/** Item names to lookup result values */
protected List<String> names;
/** Actual converted attribute values */
protected List<Object> results;
/** Id value if the retrievPKs flag was set on the ReportQuery. These can be used to get the actual object */
protected Object primaryKey;
/** If an objectLevel distinct is used then generate unique key for this result */
// GF_ISSUE_395
protected StringBuffer key;
/**
* INTERNAL:
* Used to create test results
*/
public ReportQueryResult(List<Object> results, Object primaryKeyValues) {
this.results = results;
this.primaryKey = primaryKeyValues;
}
public ReportQueryResult(ReportQuery query, AbstractRecord row, Vector toManyResults) {
super();
this.names = query.getNames();
buildResult(query, row, toManyResults);
}
/**
* INTERNAL:
* Create an array of attribute values (converted from raw field values using the mapping).
*/
protected void buildResult(ReportQuery query, AbstractRecord row, Vector toManyData) {
// GF_ISSUE_395
if (query.shouldDistinctBeUsed() && (query.shouldFilterDuplicates())) {
this.key = new StringBuffer();
}
if (query.shouldRetrievePrimaryKeys()) {
setId(query.getDescriptor().getObjectBuilder().extractPrimaryKeyFromRow(row, query.getSession()));
// For bug 3115576 this is only used for EXISTS sub-selects so no result is needed.
}
List<Object> results = new ArrayList<>();
for(ReportItem item: query.getItems()) {
if (item.isConstructorItem()) {
Object result = processConstructorItem(query, row, toManyData, (ConstructorReportItem) item);
results.add(result);
} else if (item.getAttributeExpression() != null && item.getAttributeExpression().isClassTypeExpression()) {
Object value = processItem(query, row, toManyData, item);
ClassDescriptor descriptor = ((org.eclipse.persistence.internal.expressions.ClassTypeExpression)item.getAttributeExpression()).getContainingDescriptor(query);
if (descriptor != null && descriptor.hasInheritance()) {
value = descriptor.getInheritancePolicy().classFromValue(value, query.getSession());
} else {
value = query.getSession().getDatasourcePlatform().convertObject(value, Class.class);
}
results.add(value);
} else {
// Normal items
Object value = processItem(query, row, toManyData, item);
results.add(value);
}
}
setResults(results);
}
private Object processConstructorItem(ReportQuery query, AbstractRecord row, Vector toManyData, ConstructorReportItem constructorItem) {
// For constructor items need to process each constructor argument.
Class[] constructorArgTypes = constructorItem.getConstructorArgTypes();
int numberOfArguments = constructorItem.getReportItems().size();
Object[] constructorArgs = new Object[numberOfArguments];
for (int argumentIndex = 0; argumentIndex < numberOfArguments; argumentIndex++) {
ReportItem argumentItem = constructorItem.getReportItems().get(argumentIndex);
Object result = null;
if(argumentItem.isConstructorItem()) {
result = processConstructorItem(query, row, toManyData, (ConstructorReportItem) argumentItem);
} else {
result = processItem(query, row, toManyData, argumentItem);
}
constructorArgs[argumentIndex] = ConversionManager.getDefaultManager().convertObject(result, constructorArgTypes[argumentIndex]);
}
try {
Constructor constructor = constructorItem.getConstructor();
Object returnValue = null;
if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
try {
returnValue = AccessController.doPrivileged(new PrivilegedInvokeConstructor(constructor, constructorArgs));
} catch (PrivilegedActionException exception) {
throw QueryException.exceptionWhileUsingConstructorExpression(exception.getException(), query);
}
} else {
returnValue = PrivilegedAccessHelper.invokeConstructor(constructor, constructorArgs);
}
return returnValue;
} catch (ReflectiveOperationException exc) {
throw QueryException.exceptionWhileUsingConstructorExpression(exc, query);
}
}
private Object processItemFromMapping(ReportQuery query, AbstractRecord row, DatabaseMapping mapping, ReportItem item, int itemIndex) {
Object value = null;
// If mapping is not null then it must be a direct mapping - see Reportitem.init.
// Check for non database (EIS) records to use normal get.
if (row instanceof DatabaseRecord) {
value = row.getValues().get(itemIndex);
} else {
value = row.get(mapping.getField());
}
//Bug 421056: JPA 2.1; section 4.8.5
if(item.getAttributeExpression().isFunctionExpression()) {
FunctionExpression exp = (FunctionExpression) item.getAttributeExpression();
int selector = exp.getOperator().getSelector();
//a value of null for max/min implies no rows could be applied
//we want to return null, per the spec, here before the mapping gets to alter the value
if (value == null && ((selector == ExpressionOperator.Maximum) || (selector == ExpressionOperator.Minimum))) {
return value;
}
}
//If the mapping was set on the ReportItem, then use the mapping to convert the value
if (mapping.isAbstractColumnMapping()) {
value = ((AbstractColumnMapping)mapping).getObjectValue(value, query.getSession());
} else if (mapping.isDirectCollectionMapping()) {
value = ((DirectCollectionMapping)mapping).getObjectValue(value, query.getSession());
}
return value;
}
/**
* INTERNAL:
* Return a value from an item and database row (converted from raw field values using the mapping).
*/
protected Object processItem(ReportQuery query, AbstractRecord row, Vector toManyData, ReportItem item) {
JoinedAttributeManager joinManager = null;
if (item.hasJoining()) {
joinManager = item.getJoinedAttributeManager();
if (joinManager.isToManyJoin()) {
// PERF: Only reset data-result if unset, must only occur once per item, not per row (n vs n^2).
if (joinManager.getDataResults_() == null) {
joinManager.setDataResults(new ArrayList(toManyData), query.getSession());
}
}
}
Object value = null;
int rowSize = row.size();
int itemIndex = item.getResultIndex();
DatabaseMapping mapping = item.getMapping();
ClassDescriptor descriptor = item.getDescriptor();
if (item.getAttributeExpression() != null) {
if (descriptor == null && mapping != null) {
descriptor = mapping.getReferenceDescriptor();
}
if (mapping != null && (mapping.isAbstractColumnMapping() || mapping.isDirectCollectionMapping())) {
if (itemIndex >= rowSize) {
throw QueryException.reportQueryResultSizeMismatch(itemIndex + 1, rowSize);
}
value = processItemFromMapping(query, row, mapping, item, itemIndex);
// GF_ISSUE_395+
if (this.key != null) {
this.key.append(value);
this.key.append("_");
}
} else if (descriptor != null) {
// Item is for an object result.
int size = descriptor.getAllSelectionFields(query).size();
if (itemIndex + size > rowSize) {
throw QueryException.reportQueryResultSizeMismatch(itemIndex + size, rowSize);
}
AbstractRecord subRow = row;
// Check if at the start of the row, then avoid building a subRow.
if (itemIndex > 0) {
Vector<DatabaseField> trimedFields = new NonSynchronizedSubVector<>(row.getFields(), itemIndex, rowSize);
Vector trimedValues = new NonSynchronizedSubVector(row.getValues(), itemIndex, rowSize);
subRow = new DatabaseRecord(trimedFields, trimedValues);
}
if (mapping != null && mapping.isAggregateObjectMapping()){
value = ((AggregateObjectMapping)mapping).buildAggregateFromRow(subRow, null, null, joinManager, query, false, query.getSession(), true);
} else {
//TODO : Support prefrechedCacheKeys in report query
value = descriptor.getObjectBuilder().buildObject(query, subRow, joinManager);
}
// this covers two possibilities
// 1. We want the actual Map.Entry from the table rather than the just the key
// 2. The map key is extracted from the owning object rather than built with
// a specific mapping. This could happen in a MapContainerPolicy
if (item.getAttributeExpression().isMapEntryExpression() && mapping.isCollectionMapping()){
Object rowKey = null;
if (mapping.getContainerPolicy().isMapPolicy() && !mapping.getContainerPolicy().isMappedKeyMapPolicy()){
rowKey = mapping.getContainerPolicy().keyFrom(value, query.getSession());
} else {
rowKey = mapping.getContainerPolicy().buildKey(subRow, query, null, query.getSession(), true);
}
if (((MapEntryExpression)item.getAttributeExpression()).shouldReturnMapEntry()){
value = new Association(rowKey, value);
} else {
value = rowKey;
}
}
// GF_ISSUE_395
if (this.key != null) {
Object primaryKey = descriptor.getObjectBuilder().extractPrimaryKeyFromRow(subRow, query.getSession());
if (primaryKey != null){//GF3233 NPE is caused by processing the null PK being extracted from referenced target with null values in database.
this.key.append(primaryKey);
}
this.key.append("_");
}
} else {
value = row.getValues().get(itemIndex);
// GF_ISSUE_395
if (this.key != null) {
this.key.append(value);
}
}
}
return value;
}
/**
* PUBLIC:
* Clear the contents of the result.
*/
@Override
public void clear() {
this.names = new ArrayList<>();
this.results = new ArrayList<>();
}
/**
* PUBLIC:
* Check if the value is contained in the result.
*/
public boolean contains(Object value) {
return containsValue(value);
}
/**
* PUBLIC:
* Check if the key is contained in the result.
*/
@Override
public boolean containsKey(Object key) {
return getNames().contains(key);
}
/**
* PUBLIC:
* Check if the value is contained in the result.
*/
@Override
public boolean containsValue(Object value) {
return getResults().contains(value);
}
/**
* PUBLIC:
* Returns a set of the keys.
*/
@Override
public Set entrySet() {
return new EntrySet();
}
/**
* Defines the virtual entrySet.
*/
protected class EntrySet extends AbstractSet {
@Override
public Iterator iterator() {
return new EntryIterator();
}
@Override
public int size() {
return ReportQueryResult.this.size();
}
@Override
public boolean contains(Object object) {
if (!(object instanceof Entry)) {
return false;
}
return ReportQueryResult.this.containsKey(((Entry)object).getKey());
}
@Override
public boolean remove(Object object) {
if (!(object instanceof Entry)) {
return false;
}
ReportQueryResult.this.remove(((Entry)object).getKey());
return true;
}
@Override
public void clear() {
ReportQueryResult.this.clear();
}
}
/**
* Entry class for implementing Map interface.
*/
protected static class RecordEntry implements Entry {
Object key;
Object value;
public RecordEntry(Object key, Object value) {
this.key = key;
this.value = value;
}
@Override
public Object getKey() {
return key;
}
@Override
public Object getValue() {
return value;
}
@Override
public Object setValue(Object value) {
Object oldValue = this.value;
this.value = value;
return oldValue;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof Map.Entry)) {
return false;
}
Map.Entry entry = (Map.Entry)object;
return compare(key, entry.getKey()) && compare(value, entry.getValue());
}
@Override
public int hashCode() {
return ((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0 : value.hashCode());
}
@Override
public String toString() {
return key + "=" + value;
}
private boolean compare(Object object1, Object object2) {
return (object1 == null ? object2 == null : object1.equals(object2));
}
}
/**
* Defines the virtual entrySet iterator.
*/
protected class EntryIterator implements Iterator {
int index;
EntryIterator() {
this.index = 0;
}
@Override
public boolean hasNext() {
return this.index < ReportQueryResult.this.size();
}
@Override
public Object next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
this.index++;
return new RecordEntry(getNames().get(this.index - 1), getResults().get(this.index - 1));
}
@Override
public void remove() {
if (this.index >= ReportQueryResult.this.size()) {
throw new IllegalStateException();
}
ReportQueryResult.this.remove(getNames().get(this.index));
}
}
/**
* Defines the virtual keySet iterator.
*/
protected class KeyIterator extends EntryIterator {
@Override
public Object next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
this.index++;
return getNames().get(this.index - 1);
}
}
/**
* PUBLIC:
* Compare if the two results are equal.
*/
@Override
public boolean equals(Object anObject) {
if (anObject instanceof ReportQueryResult) {
return equals((ReportQueryResult)anObject);
}
return false;
}
/**
* INTERNAL:
* Used in testing to compare if results are correct.
*/
public boolean equals(ReportQueryResult result) {
if (this == result) {
return true;
}
if (!getResults().equals(result.getResults())) {
return false;
}
// Compare PKs
if (getId() != null) {
if (result.getId() == null) {
return false;
}
return getId().equals(getId());
}
return true;
}
@Override
public int hashCode() {
List<Object> results = getResults();
Object id = getId();
int result = results != null ? results.hashCode() : 0;
result = 31 * result + (id != null ? id.hashCode() : 0);
return result;
}
/**
* PUBLIC:
* Return the value for given item name.
*/
@Override
public Object get(Object name) {
if (name instanceof String) {
return get((String)name);
}
return null;
}
/**
* PUBLIC:
* Return the value for given item name.
*/
public Object get(String name) {
int index = getNames().indexOf(name);
if (index == -1) {
return null;
}
return getResults().get(index);
}
/**
* PUBLIC:
* Return the indexed value from result.
*/
public Object getByIndex(int index) {
return getResults().get(index);
}
/**
* INTERNAL:
* Return the unique key for this result
*/
public String getResultKey(){
if (this.key != null){
return this.key.toString();
}
return null;
}
/**
* PUBLIC:
* Return the names of report items, provided to ReportQuery.
*/
public List<String> getNames() {
return names;
}
/**
* PUBLIC:
* Return the Id for the result or null if not requested.
*/
public Object getId() {
return primaryKey;
}
/**
* PUBLIC:
* Return the results.
*/
public List<Object> getResults() {
return results;
}
/**
* PUBLIC:
* Return if the result is empty.
*/
@Override
public boolean isEmpty() {
return getNames().isEmpty();
}
/**
* PUBLIC:
* Returns a set of the keys.
*/
@Override
public Set keySet() {
return new KeySet();
}
/**
* Defines the virtual keySet.
*/
protected class KeySet extends EntrySet {
@Override
public Iterator iterator() {
return new KeyIterator();
}
@Override
public boolean contains(Object object) {
return ReportQueryResult.this.containsKey(object);
}
@Override
public boolean remove(Object object) {
return ReportQueryResult.this.remove(object) != null;
}
}
/**
* ADVANCED:
* Set the value for given item name.
*/
@Override
public Object put(Object name, Object value) {
int index = getNames().indexOf(name);
if (index == -1) {
getNames().add((String)name);
getResults().add(value);
return null;
}
Object oldValue = getResults().get(index);
getResults().set(index, value);
return oldValue;
}
/**
* PUBLIC:
* Add all of the elements.
*/
@Override
public void putAll(Map map) {
Iterator entriesIterator = map.entrySet().iterator();
while (entriesIterator.hasNext()) {
Map.Entry entry = (Map.Entry)entriesIterator.next();
put(entry.getKey(), entry.getValue());
}
}
/**
* PUBLIC:
* If the PKs were retrieved with the attributes then this method can be used to read the real object from the database.
*/
public Object readObject(Class javaClass, Session session) {
if (getId() == null) {
throw QueryException.reportQueryResultWithoutPKs(this);
}
ReadObjectQuery query = new ReadObjectQuery(javaClass);
query.setSelectionId(getId());
return session.executeQuery(query);
}
/**
* INTERNAL:
* Remove the name key and value from the result.
*/
@Override
public Object remove(Object name) {
int index = getNames().indexOf(name);
if (index >= 0) {
getNames().remove(index);
Object value = getResults().get(index);
getResults().remove(index);
return value;
}
return null;
}
protected void setNames(List<String> names) {
this.names = names;
}
/**
* INTERNAL:
* Set the Id for the result row's object.
*/
protected void setId(Object primaryKey) {
this.primaryKey = primaryKey;
}
/**
* INTERNAL:
* Set the results.
*/
public void setResults(List<Object> results) {
this.results = results;
}
/**
* PUBLIC:
* Return the number of name/value pairs in the result.
*/
@Override
public int size() {
return getNames().size();
}
/**
* INTERNAL:
* Converts the ReportQueryResult to a simple array of values.
*/
public Object[] toArray(){
List<Object> list = getResults();
return (list == null) ? null : list.toArray();
}
/**
* INTERNAL:
* Converts the ReportQueryResult to a simple list of values.
*/
public List toList(){
return this.getResults();
}
@Override
public String toString() {
java.io.StringWriter writer = new java.io.StringWriter();
writer.write("ReportQueryResult(");
for (int index = 0; index < getResults().size(); index++) {
Object resultObj = getResults().get(index);
writer.write(String.valueOf(resultObj));
writer.write(" <"
+ (resultObj == null ? "null" : resultObj.getClass().getName())
+ ">");
if (index < (getResults().size() - 1)) {
writer.write(", ");
}
}
writer.write(")");
return writer.toString();
}
/**
* PUBLIC:
* Returns an collection of the values.
*/
@Override
public Collection values() {
return getResults();
}
}