| /* |
| * 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<DatabaseRecord> 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) { |
| @SuppressWarnings({"unchecked"}) |
| List<Object> values = (List<Object>)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(); |
| } |
| } |