blob: 022f7c85facaeb68d76534df845381ad09364362 [file] [log] [blame]
/*
* Copyright (c) 2011, 2021 Oracle and/or its affiliates. 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
// IBM - Bug 537795: CASE THEN and ELSE scalar expression Constants should not be casted to CASE operand type
package org.eclipse.persistence.nosql.adapters.mongo;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.util.Vector;
import jakarta.resource.cci.InteractionSpec;
import jakarta.resource.cci.MappedRecord;
import jakarta.resource.cci.Record;
import org.eclipse.persistence.descriptors.DescriptorQueryManager;
import org.eclipse.persistence.eis.EISAccessor;
import org.eclipse.persistence.eis.EISDescriptor;
import org.eclipse.persistence.eis.EISException;
import org.eclipse.persistence.eis.EISPlatform;
import org.eclipse.persistence.eis.interactions.EISInteraction;
import org.eclipse.persistence.eis.interactions.MappedInteraction;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionOperator;
import org.eclipse.persistence.internal.databaseaccess.DatasourceCall;
import org.eclipse.persistence.internal.databaseaccess.QueryStringCall;
import org.eclipse.persistence.internal.nosql.adapters.mongo.MongoInteractionSpec;
import org.eclipse.persistence.internal.nosql.adapters.mongo.MongoOperation;
import org.eclipse.persistence.internal.nosql.adapters.mongo.MongoRecord;
import org.eclipse.persistence.internal.expressions.ConstantExpression;
import org.eclipse.persistence.internal.expressions.FieldExpression;
import org.eclipse.persistence.internal.expressions.FunctionExpression;
import org.eclipse.persistence.internal.expressions.LogicalExpression;
import org.eclipse.persistence.internal.expressions.ParameterExpression;
import org.eclipse.persistence.internal.expressions.QueryKeyExpression;
import org.eclipse.persistence.internal.expressions.RelationExpression;
import org.eclipse.persistence.internal.expressions.SQLSelectStatement;
import org.eclipse.persistence.internal.expressions.SQLStatement;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.foundation.AbstractCompositeCollectionMapping;
import org.eclipse.persistence.mappings.foundation.AbstractCompositeDirectCollectionMapping;
import org.eclipse.persistence.mappings.foundation.AbstractCompositeObjectMapping;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.sequencing.Sequence;
import org.eclipse.persistence.sessions.DatabaseRecord;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
/**
* Platform for Mongo database.
*
* @author James
* @since EclipseLink 2.4
*/
public class MongoPlatform extends EISPlatform {
/** Mongo interaction spec properties. */
public static final String OPERATION = "mongo.operation";
public static final String COLLECTION = "mongo.collection";
public static final String OPTIONS = "mongo.options";
public static final String READ_PREFERENCE = "mongo.read-preference";
public static final String WRITE_CONCERN = "mongo.write-concern";
public static final String SKIP = "mongo.skip";
public static final String LIMIT = "mongo.limit";
public static final String BATCH_SIZE = "mongo.batch-size";
/** Configure if like should be SQL or regex. */
protected boolean isLikeRegex;
/**
* Default constructor.
*/
public MongoPlatform() {
super();
setIsMappedRecordSupported(true);
setIsIndexedRecordSupported(false);
setIsDOMRecordSupported(true);
setSupportsLocalTransactions(true);
}
/**
* Mongo does not support all Java types.
* Convert unsupported types to string.
*/
@Override
public void setValueInRecord(String key, Object value, MappedRecord record, EISAccessor accessor) {
Object recordValue = value;
if ((value instanceof BigDecimal) || (value instanceof BigInteger)) {
recordValue = getConversionManager().convertObject(value, ClassConstants.STRING);
}
record.put(key, recordValue);
}
/**
* Return if regex should be used for like.
*/
public boolean isLikeRegex() {
return isLikeRegex;
}
/**
* Set if regex should be used for like.
*/
public void setIsLikeRegex(boolean isLikeRegex) {
this.isLikeRegex = isLikeRegex;
}
/**
* Allow the platform to build the interaction spec based on properties defined in the interaction.
*/
@Override
public InteractionSpec buildInteractionSpec(EISInteraction interaction) {
InteractionSpec spec = interaction.getInteractionSpec();
if (spec == null) {
MongoInteractionSpec mongoSpec = new MongoInteractionSpec();
Object operation = interaction.getProperty(OPERATION);
if (interaction.isQueryStringCall()) {
mongoSpec.setCode(((QueryStringCall)interaction).getQueryString());
operation = MongoOperation.EVAL;
}
if (operation == null) {
throw new EISException("'" + OPERATION + "' property must be set on the query's interation.");
}
if (operation instanceof String) {
operation = MongoOperation.valueOf((String)operation);
}
mongoSpec.setOperation((MongoOperation)operation);
Object collection = interaction.getProperty(COLLECTION);
if (collection != null) {
mongoSpec.setCollection((String)collection);
}
// Allows setting of read preference as a property.
Object preference = interaction.getProperty(READ_PREFERENCE);
if (preference instanceof ReadPreference) {
mongoSpec.setReadPreference((ReadPreference)preference);
} else if (preference instanceof String) {
String constant = (String)preference;
if (constant.equals("PRIMARY")) {
mongoSpec.setReadPreference(ReadPreference.primary());
} else if (constant.equals("SECONDARY")) {
mongoSpec.setReadPreference(ReadPreference.secondary() );
} else {
throw new EISException("Invalid read preference property value: " + constant);
}
}
// Allows setting of write concern as a property.
Object concern = interaction.getProperty(WRITE_CONCERN);
if (concern instanceof WriteConcern) {
mongoSpec.setWriteConcern((WriteConcern)concern);
} else if (concern instanceof String) {
String constant = (String)concern;
if (constant.equals("FSYNC_SAFE")) {
mongoSpec.setWriteConcern(WriteConcern.FSYNC_SAFE);
} else if (constant.equals("JOURNAL_SAFE")) {
mongoSpec.setWriteConcern(WriteConcern.JOURNAL_SAFE);
} else if (constant.equals("MAJORITY")) {
mongoSpec.setWriteConcern(WriteConcern.MAJORITY);
} else if (constant.equals("NONE")) {
mongoSpec.setWriteConcern(/* WriteConcern.NONE */ new WriteConcern("none"));
} else if (constant.equals("NORMAL")) {
mongoSpec.setWriteConcern(WriteConcern.NORMAL);
} else if (constant.equals("REPLICAS_SAFE")) {
mongoSpec.setWriteConcern(WriteConcern.REPLICAS_SAFE);
} else if (constant.equals("SAFE")) {
mongoSpec.setWriteConcern(WriteConcern.SAFE);
} else {
throw new EISException("Invalid read preference property value: " + constant);
}
}
// Allows setting of options as a property.
Object options = interaction.getProperty(OPTIONS);
if (options instanceof Number) {
mongoSpec.setOptions(((Number)options).intValue());
} else if (options instanceof String) {
mongoSpec.setOptions(Integer.parseInt(((String)options)));
}
// Allows setting of skip as a property.
Object skip = interaction.getProperty(SKIP);
if (skip instanceof Number) {
mongoSpec.setSkip(((Number)skip).intValue());
} else if (skip instanceof String) {
mongoSpec.setSkip(Integer.parseInt(((String)skip)));
}
// Allows setting of limit as a property.
Object limit = interaction.getProperty(LIMIT);
if (limit instanceof Number) {
mongoSpec.setLimit(((Number)limit).intValue());
} else if (skip instanceof String) {
mongoSpec.setLimit(Integer.parseInt(((String)limit)));
}
// Allows setting of batchSize as a property.
Object batchSize = interaction.getProperty(BATCH_SIZE);
if (batchSize instanceof Number) {
mongoSpec.setBatchSize(((Number)batchSize).intValue());
} else if (skip instanceof String) {
mongoSpec.setBatchSize(Integer.parseInt(((String)batchSize)));
}
spec = mongoSpec;
}
return spec;
}
/**
* For updates a separate translation record is required.
* The output row is used for this.
*/
@Override
public Record createOutputRecord(EISInteraction interaction, AbstractRecord translationRow, EISAccessor accessor) {
if (((interaction.getInteractionSpec() != null) && ((MongoInteractionSpec)interaction.getInteractionSpec()).getOperation() == MongoOperation.UPDATE)
|| ((interaction.getProperty(OPERATION) != null)
&& ((interaction.getProperty(OPERATION) == MongoOperation.UPDATE) || (interaction.getProperty(OPERATION).equals(MongoOperation.UPDATE.name()))))) {
return (Record)interaction.createRecordElement(interaction.getInputRecordName(), translationRow, accessor);
} else {
return null;
}
}
/**
* INTERNAL:
* Allow the platform to initialize the CRUD queries to defaults.
* Configure the CRUD operations using GET/PUT and DELETE.
*/
@Override
public void initializeDefaultQueries(DescriptorQueryManager queryManager, AbstractSession session) {
// Insert
if (!queryManager.hasInsertQuery()) {
EISInteraction call = new MappedInteraction();
call.setProperty(MongoPlatform.OPERATION, MongoOperation.INSERT);
call.setProperty(MongoPlatform.COLLECTION, ((EISDescriptor)queryManager.getDescriptor()).getDataTypeName());
queryManager.setInsertCall(call);
}
// Update
if (!queryManager.hasUpdateQuery()) {
EISInteraction call = new MappedInteraction();
call.setProperty(MongoPlatform.OPERATION, MongoOperation.UPDATE);
call.setProperty(MongoPlatform.COLLECTION, ((EISDescriptor)queryManager.getDescriptor()).getDataTypeName());
queryManager.setUpdateCall(call);
}
// Read
if (!queryManager.hasReadObjectQuery()) {
MappedInteraction call = new MappedInteraction();
call.setProperty(MongoPlatform.OPERATION, MongoOperation.FIND);
call.setProperty(MongoPlatform.COLLECTION, ((EISDescriptor)queryManager.getDescriptor()).getDataTypeName());
for (DatabaseField field : queryManager.getDescriptor().getPrimaryKeyFields()) {
call.addArgument(field.getName());
}
queryManager.setReadObjectCall(call);
}
// Delete
if (!queryManager.hasDeleteQuery()) {
MappedInteraction call = new MappedInteraction();
call.setProperty(MongoPlatform.OPERATION, MongoOperation.REMOVE);
call.setProperty(MongoPlatform.COLLECTION, ((EISDescriptor)queryManager.getDescriptor()).getDataTypeName());
for (DatabaseField field : queryManager.getDescriptor().getPrimaryKeyFields()) {
call.addArgument(field.getName());
}
queryManager.setDeleteCall(call);
}
}
/**
* INTERNAL:
* Override this method to throw an exception by default.
* Platforms that support dynamic querying can override this to generate an EISInteraction.
*/
@Override
public DatasourceCall buildCallFromStatement(SQLStatement statement, DatabaseQuery query, AbstractSession session) {
if (query.isObjectLevelReadQuery()) {
ObjectLevelReadQuery readQuery = (ObjectLevelReadQuery)query;
MappedInteraction interaction = new MappedInteraction();
interaction.setProperty(OPERATION, MongoOperation.FIND);
interaction.setProperty(COLLECTION, ((EISDescriptor)query.getDescriptor()).getDataTypeName());
if (readQuery.getFirstResult() > 0) {
interaction.setProperty(SKIP, readQuery.getFirstResult());
}
if (readQuery.getMaxRows() > 0) {
interaction.setProperty(LIMIT, readQuery.getMaxRows());
}
if (readQuery.getFetchSize() > 0) {
interaction.setProperty(BATCH_SIZE, readQuery.getMaxRows());
}
DatabaseRecord row = new DatabaseRecord();
if (statement.getWhereClause() != null) {
appendExpressionToQueryRow(statement.getWhereClause(), row, query);
}
if (readQuery.hasOrderByExpressions()) {
DatabaseRecord sort = new DatabaseRecord();
for (Expression orderBy : readQuery.getOrderByExpressions()) {
appendExpressionToSortRow(orderBy, sort, query);
}
row.put(MongoRecord.SORT, sort);
}
if (readQuery.isReportQuery()) {
DatabaseRecord select = new DatabaseRecord();
for (Object field : ((SQLSelectStatement)statement).getFields()) {
if (field instanceof DatabaseField) {
select.put((DatabaseField)field, 1);
} else if (field instanceof Expression) {
Object value = extractValueFromExpression((Expression)field, readQuery);
if (!(value instanceof DatabaseField)) {
throw new EISException("Query too complex for Mongo translation, only field selects are supported in query: " + query);
}
select.put((DatabaseField)value, 1);
}
}
row.put("$select", select);
}
interaction.setInputRow(row);
return interaction;
}
throw new EISException("Query too complex for Mongo translation, only select queries are supported in query: " + query);
}
/**
* Append the expression and recursively to the query row.
*/
protected void appendExpressionToQueryRow(Expression expression, AbstractRecord row, DatabaseQuery query) {
if (expression.isRelationExpression()) {
RelationExpression relation = (RelationExpression)expression;
Object left = extractValueFromExpression(relation.getFirstChild(), query);
Object right = extractValueFromExpression(relation.getSecondChild(), query);
if (relation.getOperator().getSelector() == ExpressionOperator.Equal) {
row.put(left, right);
} else {
DatabaseRecord nested = new DatabaseRecord();
if (relation.getOperator().getSelector() == ExpressionOperator.GreaterThan) {
nested.put("$gt", right);
} else if (relation.getOperator().getSelector() == ExpressionOperator.LessThan) {
nested.put("$lt", right);
} else if (relation.getOperator().getSelector() == ExpressionOperator.LessThanEqual) {
nested.put("$lte", right);
} else if (relation.getOperator().getSelector() == ExpressionOperator.GreaterThanEqual) {
nested.put("$gte", right);
} else if (relation.getOperator().getSelector() == ExpressionOperator.NotEqual) {
nested.put("$ne", right);
} else if (relation.getOperator().getSelector() == ExpressionOperator.In) {
nested.put("$in", right);
row.put(left, nested);
} else if (relation.getOperator().getSelector() == ExpressionOperator.NotIn) {
nested.put("$nin", right);
row.put(left, nested);
} else {
throw new EISException("Query too complex for Mongo translation, relation [" + expression + "] not supported in query: " + query);
}
row.put(left, nested);
}
} else if (expression.isLogicalExpression()) {
LogicalExpression logic = (LogicalExpression)expression;
DatabaseRecord first = new DatabaseRecord();
DatabaseRecord second = new DatabaseRecord();
appendExpressionToQueryRow(logic.getFirstChild(), first, query);
appendExpressionToQueryRow(logic.getSecondChild(), second, query);
List nested = new Vector();
nested.add(first);
nested.add(second);
if (logic.getOperator().getSelector() == ExpressionOperator.And) {
row.put("$and", nested);
} else if (logic.getOperator().getSelector() == ExpressionOperator.Or) {
row.put("$or", nested);
} else {
throw new EISException("Query too complex for Mongo translation, logic [" + expression + "] not supported in query: " + query);
}
} else if (expression.isFunctionExpression()) {
FunctionExpression function = (FunctionExpression)expression;
if (function.getOperator().getSelector() == ExpressionOperator.Like) {
Object left = extractValueFromExpression(function.getChildren().get(0), query);
Object right = extractValueFromExpression(function.getChildren().get(1), query);
if (!(right instanceof String)) {
throw new EISException("Query too complex for Mongo translation, like with [" + right + "] not supported in query: " + query);
}
String pattern = (String)right;
DatabaseRecord nested = new DatabaseRecord();
if (!this.isLikeRegex) {
pattern = Helper.convertLikeToRegex(pattern);
}
nested.put("$regex", pattern);
row.put(left, nested);
} else if (function.getOperator().getSelector() == ExpressionOperator.Not) {
// Detect situation 'not(a = b)' and change it to '(a != b)'
Expression expr = function.getChildren().get(0);
if (expr.isRelationExpression()) {
RelationExpression relation = (RelationExpression)expr;
Object left = extractValueFromExpression(relation.getFirstChild(), query);
Object right = extractValueFromExpression(relation.getSecondChild(), query);
DatabaseRecord nested = new DatabaseRecord();
if (expr.getOperator().getSelector() == ExpressionOperator.Equal) {
nested.put("$ne", right);
} else {
nested.put("not", right);
}
row.put(left, nested);
} else {
throw new EISException("Query too complex for Mongo translation, function [" + expression + "] not supported in query: " + query);
}
} else {
throw new EISException("Query too complex for Mongo translation, function [" + expression + "] not supported in query: " + query);
}
} else {
throw new EISException("Query too complex for Mongo translation, expression [" + expression + "] not supported in query: " + query);
}
}
/**
* Append the order by expression to the sort row.
*/
protected void appendExpressionToSortRow(Expression expression, AbstractRecord row, DatabaseQuery query) {
if (expression.isFunctionExpression()) {
FunctionExpression function = (FunctionExpression)expression;
if (function.getOperator().getSelector() == ExpressionOperator.Ascending) {
Object field = extractValueFromExpression(function.getChildren().get(0), query);
row.put(field, 1);
} else if (function.getOperator().getSelector() == ExpressionOperator.Descending) {
Object field = extractValueFromExpression(function.getChildren().get(0), query);
row.put(field, -1);
} else {
throw new EISException("Query too complex for Mongo translation, order by [" + expression + "] not supported in query: " + query);
}
} else {
Object field = extractValueFromExpression(expression, query);
row.put(field, 1);
}
}
/**
* Extract the field or constant value from the comparison expression.
*/
protected Object extractValueFromExpression(Expression expression, DatabaseQuery query) {
Object value = null;
expression.getBuilder().setSession(query.getSession());
if (expression.isQueryKeyExpression()) {
QueryKeyExpression queryKeyExpression = (QueryKeyExpression)expression;
value = queryKeyExpression.getField();
if ((queryKeyExpression.getMapping() != null) && queryKeyExpression.getMapping().getDescriptor().isDescriptorTypeAggregate()) {
String name = queryKeyExpression.getField().getName();
while (queryKeyExpression.getBaseExpression().isQueryKeyExpression()
&& (((QueryKeyExpression)queryKeyExpression.getBaseExpression()).getMapping().isAbstractCompositeObjectMapping()
|| ((QueryKeyExpression)queryKeyExpression.getBaseExpression()).getMapping().isAbstractCompositeCollectionMapping()
|| ((QueryKeyExpression)queryKeyExpression.getBaseExpression()).getMapping().isAbstractCompositeDirectCollectionMapping())) {
queryKeyExpression = (QueryKeyExpression)queryKeyExpression.getBaseExpression();
if (queryKeyExpression.getMapping().isAbstractCompositeObjectMapping()) {
name = queryKeyExpression.getMapping().getField().getName() + "." + name;
} else if (queryKeyExpression.getMapping().isAbstractCompositeCollectionMapping()) {
name = queryKeyExpression.getMapping().getField().getName() + "." + name;
} else if (queryKeyExpression.getMapping().isAbstractCompositeDirectCollectionMapping()) {
name = queryKeyExpression.getMapping().getField().getName() + "." + name;
}
}
DatabaseField field = new DatabaseField();
field.setName(name);
value = field;
}
} else if (expression.isFieldExpression()) {
value = ((FieldExpression)expression).getField();
} else if (expression.isConstantExpression()) {
value = ((ConstantExpression)expression).getValue();
if (((ConstantExpression)expression).getLocalBase() != null) {
value = ((ConstantExpression)expression).getLocalBase().getFieldValue(value, query.getSession());
}
} else if (expression.isParameterExpression()) {
value = query.getTranslationRow().get(((ParameterExpression)expression).getField());
if (((ParameterExpression)expression).getLocalBase() != null) {
value = ((ParameterExpression)expression).getLocalBase().getFieldValue(value, query.getSession());
}
} else {
throw new EISException("Query too complex for Mongo translation, comparison of [" + expression + "] not supported in query: " + query);
}
if (value instanceof List) {
List values = (List)value;
for (int index = 0; index < values.size(); index++) {
Object element = values.get(index);
if (element instanceof Expression) {
element = extractValueFromExpression((Expression)element, query);
values.set(index, element);
}
}
}
return value;
}
/**
* Do not prepare dynamic queries, as the translation row is required.
*/
@Override
public boolean shouldPrepare(DatabaseQuery query) {
return (query.getDatasourceCall() instanceof EISInteraction);
}
/**
* INTERNAL:
* Create platform-default Sequence
*/
@Override
protected Sequence createPlatformDefaultSequence() {
return new OIDSequence();
}
}