/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */
package org.teiid.translator.mongodb;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;

import javax.resource.cci.ConnectionFactory;

import org.bson.types.Binary;
import org.teiid.core.types.BinaryType;
import org.teiid.core.types.BlobImpl;
import org.teiid.core.types.ClobImpl;
import org.teiid.core.types.DataTypeManager;
import org.teiid.core.types.GeometryType;
import org.teiid.core.types.InputStreamFactory;
import org.teiid.core.types.SQLXMLImpl;
import org.teiid.core.types.Transform;
import org.teiid.core.types.TransformationException;
import org.teiid.language.Argument;
import org.teiid.language.Call;
import org.teiid.language.Command;
import org.teiid.language.Expression;
import org.teiid.language.Function;
import org.teiid.language.LanguageFactory;
import org.teiid.language.QueryExpression;
import org.teiid.language.visitor.SQLStringVisitor;
import org.teiid.metadata.FunctionMethod;
import org.teiid.metadata.RuntimeMetadata;
import org.teiid.mongodb.MongoDBConnection;
import org.teiid.translator.*;
import org.teiid.translator.jdbc.AliasModifier;
import org.teiid.translator.jdbc.FunctionModifier;
import org.teiid.util.Version;

import com.mongodb.AggregationOptions;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBRef;
import com.mongodb.gridfs.GridFS;
import com.mongodb.gridfs.GridFSDBFile;
import com.mongodb.gridfs.GridFSInputFile;

@Translator(name="mongodb", description="MongoDB Translator, reads and writes the data to MongoDB")
public class MongoDBExecutionFactory extends ExecutionFactory<ConnectionFactory, MongoDBConnection> {
	private static final String MONGO = "mongo"; //$NON-NLS-1$
	public static final Version TWO_4 = Version.getVersion("2.4"); //$NON-NLS-1$
    public static final Version TWO_6 = Version.getVersion("2.6"); //$NON-NLS-1$
    public static final Version THREE_0 = Version.getVersion("3.0"); //$NON-NLS-1$
    
    public static final String FUNC_GEO_WITHIN = "geoWithin"; //$NON-NLS-1$
	public static final String FUNC_GEO_INTERSECTS = "geoIntersects"; //$NON-NLS-1$
	public static final String FUNC_GEO_NEAR = "geoNear"; //$NON-NLS-1$
	public static final String FUNC_GEO_NEAR_SPHERE = "geoNearSphere"; //$NON-NLS-1$
    public static final String FUNC_GEO_POLYGON_WITHIN = "geoPolygonWithin"; //$NON-NLS-1$
	public static final String FUNC_GEO_POLYGON_INTERSECTS = "geoPolygonIntersects"; //$NON-NLS-1$
	
	public static final String[] GEOSPATIAL_FUNCTIONS = {FUNC_GEO_WITHIN, FUNC_GEO_INTERSECTS, FUNC_GEO_NEAR, FUNC_GEO_NEAR_SPHERE, FUNC_GEO_POLYGON_WITHIN, FUNC_GEO_POLYGON_INTERSECTS};
	public static final String AVOID_PROJECTION = "AVOID_PROJECTION"; //$NON-NLS-1$
    
	protected Map<String, FunctionModifier> functionModifiers = new TreeMap<String, FunctionModifier>(String.CASE_INSENSITIVE_ORDER);
	private Version version = TWO_6;
	private boolean useDisk = true;
	private boolean supportsAggregatesCount = true;
	
	public MongoDBExecutionFactory() {
		setSupportsOrderBy(true);
		setSupportsSelectDistinct(true);
		setSupportsDirectQueryProcedure(false);
		setSourceRequiredForMetadata(false);
		setSupportsInnerJoins(true);
		setSupportsOuterJoins(true);
		setSupportedJoinCriteria(SupportedJoinCriteria.KEY);
		setTransactionSupport(TransactionSupport.NONE);
	}

	@SuppressWarnings("nls")
	@Override
	public void start() throws TranslatorException {
		super.start();
		
        registerFunctionModifier("+", new AliasModifier("$add"));//$NON-NLS-1$ //$NON-NLS-2$
        registerFunctionModifier("-", new AliasModifier("$subtract"));//$NON-NLS-1$ //$NON-NLS-2$
        registerFunctionModifier("*", new AliasModifier("$multiply"));//$NON-NLS-1$ //$NON-NLS-2$
        registerFunctionModifier("/", new AliasModifier("$divide"));//$NON-NLS-1$ //$NON-NLS-2$

        registerFunctionModifier(SourceSystemFunctions.CONCAT, new AliasModifier("$concat"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.SUBSTRING, new AliasModifier("$substr"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.SUBSTRING, new FunctionModifier() {
            @Override
            public List<?> translate(Function function) {
                function.setName("$substr"); //$NON-NLS-1$

                ArrayList<Expression> params = new ArrayList<Expression>();
                
                params.add(function.getParameters().get(0));
                        
                // MongoDB is zero base index; Teiid is 1 based;
                params.add(LanguageFactory.INSTANCE.createFunction("-", new Expression[] { function.getParameters().get(1),
                        LanguageFactory.INSTANCE.createLiteral(1, TypeFacility.RUNTIME_TYPES.INTEGER) },
                        TypeFacility.RUNTIME_TYPES.INTEGER));
                
                if (function.getParameters().size() == 2) {
                    function.getParameters().add(LanguageFactory.INSTANCE.createLiteral(DataTypeManager.MAX_STRING_LENGTH,
                            TypeFacility.RUNTIME_TYPES.INTEGER));
                }
                
                params.add(function.getParameters().get(2));
                
                function.getParameters().clear();
                function.getParameters().addAll(params);
                return null;
            }
        });
        
        registerFunctionModifier(SourceSystemFunctions.LCASE, new AliasModifier("$toLower"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.UCASE, new AliasModifier("$toUpper"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.DAYOFYEAR, new AliasModifier("$dayOfYear"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.DAYOFMONTH, new AliasModifier("$dayOfMonth"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.DAYOFWEEK, new AliasModifier("$dayOfWeek"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.YEAR, new AliasModifier("$year"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.MONTH, new AliasModifier("$month"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.WEEK, new AliasModifier("$week"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.HOUR, new AliasModifier("$hour"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.MINUTE, new AliasModifier("$minute"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.SECOND, new AliasModifier("$second"));//$NON-NLS-1$
        registerFunctionModifier(SourceSystemFunctions.IFNULL, new AliasModifier("$ifNull")); //$NON-NLS-1$		
		
        FunctionMethod method = null;
		method = addPushDownFunction(MONGO, FUNC_GEO_INTERSECTS, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.STRING,
				TypeFacility.RUNTIME_NAMES.DOUBLE + "[][]"); //$NON-NLS-1$
        method.setProperty(AVOID_PROJECTION, "true");
		
        method = addPushDownFunction(MONGO, FUNC_GEO_INTERSECTS, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.GEOMETRY); // $NON-NLS-1$ 
        method.setProperty(AVOID_PROJECTION, "true");

		method = addPushDownFunction(MONGO, FUNC_GEO_WITHIN, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.STRING,
				TypeFacility.RUNTIME_NAMES.DOUBLE + "[][]"); //$NON-NLS-1$
        method.setProperty(AVOID_PROJECTION, "true");
        
		method = addPushDownFunction(MONGO, FUNC_GEO_WITHIN, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.GEOMETRY); // $NON-NLS-1$ 
        method.setProperty(AVOID_PROJECTION, "true");
        
        if (getVersion().compareTo(MongoDBExecutionFactory.TWO_6) >= 0) {
			method = addPushDownFunction(MONGO, FUNC_GEO_NEAR, TypeFacility.RUNTIME_NAMES.BOOLEAN,
					TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.DOUBLE + "[]", //$NON-NLS-1$
					TypeFacility.RUNTIME_NAMES.INTEGER, TypeFacility.RUNTIME_NAMES.INTEGER); 
            method.setProperty(AVOID_PROJECTION, "true");
            
			method = addPushDownFunction(MONGO, FUNC_GEO_NEAR, TypeFacility.RUNTIME_NAMES.BOOLEAN,
					TypeFacility.RUNTIME_NAMES.GEOMETRY,
					TypeFacility.RUNTIME_NAMES.INTEGER, TypeFacility.RUNTIME_NAMES.INTEGER); 
            method.setProperty(AVOID_PROJECTION, "true");            
            
			method = addPushDownFunction(MONGO, FUNC_GEO_NEAR_SPHERE, TypeFacility.RUNTIME_NAMES.BOOLEAN,
					TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.DOUBLE + "[]", //$NON-NLS-1$
					TypeFacility.RUNTIME_NAMES.INTEGER, TypeFacility.RUNTIME_NAMES.INTEGER); 
            method.setProperty(AVOID_PROJECTION, "true");
            
			method = addPushDownFunction(MONGO, FUNC_GEO_NEAR_SPHERE, TypeFacility.RUNTIME_NAMES.BOOLEAN,
					TypeFacility.RUNTIME_NAMES.GEOMETRY,
					TypeFacility.RUNTIME_NAMES.INTEGER, TypeFacility.RUNTIME_NAMES.INTEGER); 
            method.setProperty(AVOID_PROJECTION, "true");            
        }
        else {
			method = addPushDownFunction(MONGO, FUNC_GEO_NEAR, TypeFacility.RUNTIME_NAMES.BOOLEAN,
					TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.DOUBLE + "[]", //$NON-NLS-1$
					TypeFacility.RUNTIME_NAMES.INTEGER); 
            method.setProperty(AVOID_PROJECTION, "true");
            
			method = addPushDownFunction(MONGO, FUNC_GEO_NEAR_SPHERE, TypeFacility.RUNTIME_NAMES.BOOLEAN,
					TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.DOUBLE + "[]", //$NON-NLS-1$
					TypeFacility.RUNTIME_NAMES.INTEGER); 
            method.setProperty(AVOID_PROJECTION, "true");            
        }
        
		method = addPushDownFunction(MONGO, FUNC_GEO_POLYGON_INTERSECTS, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.DOUBLE, TypeFacility.RUNTIME_NAMES.DOUBLE,
				TypeFacility.RUNTIME_NAMES.DOUBLE, TypeFacility.RUNTIME_NAMES.DOUBLE);
        method.setProperty(AVOID_PROJECTION, "true");
        
		method = addPushDownFunction(MONGO, FUNC_GEO_POLYGON_INTERSECTS, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.GEOMETRY);
        method.setProperty(AVOID_PROJECTION, "true");        

		method = addPushDownFunction(MONGO, FUNC_GEO_POLYGON_WITHIN, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.STRING, TypeFacility.RUNTIME_NAMES.DOUBLE, TypeFacility.RUNTIME_NAMES.DOUBLE,
				TypeFacility.RUNTIME_NAMES.DOUBLE, TypeFacility.RUNTIME_NAMES.DOUBLE); 
        method.setProperty(AVOID_PROJECTION, "true");

		method = addPushDownFunction(MONGO, FUNC_GEO_POLYGON_WITHIN, TypeFacility.RUNTIME_NAMES.BOOLEAN,
				TypeFacility.RUNTIME_NAMES.GEOMETRY); 
        method.setProperty(AVOID_PROJECTION, "true");
        
        registerFunctionModifier(FUNC_GEO_NEAR, new AliasModifier("$near"));//$NON-NLS-1$
        registerFunctionModifier(FUNC_GEO_NEAR_SPHERE, new AliasModifier("$nearSphere"));//$NON-NLS-1$
        registerFunctionModifier(FUNC_GEO_WITHIN, new AliasModifier("$geoWithin"));//$NON-NLS-1$
        registerFunctionModifier(FUNC_GEO_INTERSECTS, new AliasModifier("$geoIntersects"));//$NON-NLS-1$
        registerFunctionModifier(FUNC_GEO_POLYGON_INTERSECTS, new GeoPolygonFunctionModifier("$geoIntersects"));//$NON-NLS-1$
        registerFunctionModifier(FUNC_GEO_POLYGON_WITHIN, new GeoPolygonFunctionModifier("$geoWithin"));//$NON-NLS-1$
	}
	
	private static class GeoPolygonFunctionModifier extends FunctionModifier {
		private String functionName;
		
		public GeoPolygonFunctionModifier(String name) {
			this.functionName = name;
		}
		
		@Override
		public List<?> translate(Function function) {
			List<Expression> args = function.getParameters();
			Expression north = args.get(1);
			Expression east = args.get(2);
			Expression west = args.get(3);
			Expression south = args.get(4);
			
			ArrayList<Expression> points = new ArrayList<Expression>();
			points.add(new org.teiid.language.Array(TypeFacility.RUNTIME_TYPES.DOUBLE, Arrays.asList(west, north)));
			points.add(new org.teiid.language.Array(TypeFacility.RUNTIME_TYPES.DOUBLE, Arrays.asList(east, north)));
			points.add(new org.teiid.language.Array(TypeFacility.RUNTIME_TYPES.DOUBLE, Arrays.asList(east, south)));
			points.add(new org.teiid.language.Array(TypeFacility.RUNTIME_TYPES.DOUBLE, Arrays.asList(west, south)));
			points.add(new org.teiid.language.Array(TypeFacility.RUNTIME_TYPES.DOUBLE, Arrays.asList(west, north)));
			
			Expression coordinates = new org.teiid.language.Array(TypeFacility.RUNTIME_TYPES.DOUBLE,  points);			
			
			Function func = LanguageFactory.INSTANCE.createFunction(this.functionName,
					Arrays.asList(args.get(0), 
							LanguageFactory.INSTANCE.createLiteral("Polygon", TypeFacility.RUNTIME_TYPES.STRING), //$NON-NLS-1$
							coordinates
					),
                    Boolean.class);
			return Arrays.asList(func);  
		}
	}
	
    @TranslatorProperty(display="Database Version", description= "Database Version")
    public String getDatabaseVersion() {
        return this.version.toString();
    }

    Version getVersion() {
        return this.version;
    }
    
    @TranslatorProperty(display="Use Disk", description="Use disk for aggregation processing in MongoDB",advanced=true)
    public boolean useDisk() {
        return this.useDisk;
    }

    public void setUseDisk(boolean useDisk) {
        this.useDisk = useDisk;
    }
    
    /**
     * Sets the database version.  See also {@link #getVersion()}
     * @param version
     */
    public void setDatabaseVersion(String version) {
        this.version = Version.getVersion(version);
    }
    
    public void setDatabaseVersion(Version version) {
        this.version = version;
    }	
	
	@Override
    public MetadataProcessor<MongoDBConnection> getMetadataProcessor() {
	    return new MongoDBMetadataProcessor();
	}

    public void registerFunctionModifier(String name, FunctionModifier modifier) {
    	this.functionModifiers.put(name, modifier);
    }

    public Map<String, FunctionModifier> getFunctionModifiers() {
    	return this.functionModifiers;
    }

	@Override
	public ResultSetExecution createResultSetExecution(QueryExpression command, ExecutionContext executionContext, RuntimeMetadata metadata, MongoDBConnection connection) throws TranslatorException {
		return new MongoDBQueryExecution(this, command, executionContext, metadata, connection);
	}

	@Override
	public ProcedureExecution createProcedureExecution(Call command, ExecutionContext executionContext, RuntimeMetadata metadata, MongoDBConnection connection) throws TranslatorException {
		String nativeQuery = command.getMetadataObject().getProperty(SQLStringVisitor.TEIID_NATIVE_QUERY, false);
		if (nativeQuery != null) {
			return new MongoDBDirectQueryExecution(command.getArguments(), command, executionContext, metadata, connection, nativeQuery, false, this);
		}
		throw new TranslatorException(MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18011));
	}

	@Override
	public UpdateExecution createUpdateExecution(Command command, ExecutionContext executionContext, RuntimeMetadata metadata, MongoDBConnection connection) throws TranslatorException {
		return new MongoDBUpdateExecution(this, command, executionContext, metadata, connection);
	}

	@Override
	public ProcedureExecution createDirectExecution(List<Argument> arguments, Command command, ExecutionContext executionContext, RuntimeMetadata metadata, MongoDBConnection connection) throws TranslatorException {
		return new MongoDBDirectQueryExecution(arguments.subList(1, arguments.size()), command, executionContext, metadata, connection, (String)arguments.get(0).getArgumentValue().getValue(), true, this);
	}

    @Override
	public boolean useAnsiJoin() {
    	return true;
    }

	@Override
	public boolean supportsSelfJoins() {
		return true;
	}

    @Override
	public boolean supportsCompareCriteriaEquals() {
    	return true;
    }

    @Override
	public boolean supportsCompareCriteriaOrdered() {
    	return true;
    }

    @Override
	public boolean supportsLikeCriteria() {
    	return true;
    }

    @Override
	public boolean supportsOrCriteria() {
    	return true;
    }

    @Override
	public boolean supportsOrderByUnrelated() {
    	return true;
    }

    @Override
	public boolean supportsGroupBy() {
    	return true;
    }

    @Override
	public boolean supportsHaving() {
    	return true;
    }

    @Override
	public boolean supportsAggregatesSum() {
    	return true;
    }

    @Override
	public boolean supportsAggregatesAvg() {
    	return true;
    }

    @Override
	public boolean supportsAggregatesMin() {
    	return true;
    }

    @Override
	public boolean supportsAggregatesMax() {
    	return true;
    }
    
    public void setSupportsAggregatesCount(boolean value) {
        this.supportsAggregatesCount = value;
    }
    
    @Override
    @TranslatorProperty(display="Supports Count(expr)", description="Supports Aggregate function count with expression", advanced=true)
	public boolean supportsAggregatesCount() {
    	return supportsAggregatesCount;
    }

    @Override
	public boolean supportsAggregatesCountStar() {
    	return true;
    }

    @Override
	public List<String> getSupportedFunctions() {
        List<String> supportedFunctions = new ArrayList<String>();
        supportedFunctions.addAll(getDefaultSupportedFunctions());

        supportedFunctions.add(SourceSystemFunctions.CONCAT);
        supportedFunctions.add(SourceSystemFunctions.SUBSTRING);
        supportedFunctions.add(SourceSystemFunctions.LCASE);
        supportedFunctions.add(SourceSystemFunctions.UCASE);
        supportedFunctions.add(SourceSystemFunctions.DAYOFYEAR);
        supportedFunctions.add(SourceSystemFunctions.DAYOFMONTH);
        supportedFunctions.add(SourceSystemFunctions.DAYOFWEEK);
        supportedFunctions.add(SourceSystemFunctions.YEAR);
        supportedFunctions.add(SourceSystemFunctions.MONTH);
        supportedFunctions.add(SourceSystemFunctions.WEEK);
        supportedFunctions.add(SourceSystemFunctions.HOUR);
        supportedFunctions.add(SourceSystemFunctions.MINUTE);
        supportedFunctions.add(SourceSystemFunctions.SECOND);
        supportedFunctions.add(SourceSystemFunctions.IFNULL);

        return supportedFunctions;
    }

	public List<String> getDefaultSupportedFunctions(){
		return Arrays.asList(new String[] { "+", "-", "*", "/", "%"}); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
	}

    @Override
	public boolean supportsInCriteria() {
    	return true;
    }

    @Override
	public boolean supportsNotCriteria() {
    	return true;
    }

    @Override
	public boolean supportsRowLimit() {
    	return true;
    }

    @Override
	public boolean supportsIsNullCriteria() {
    	return true;
    }

    @Override
	public boolean supportsRowOffset() {
    	return true;
    }

    @Override
	public boolean supportsBulkUpdate() {
    	return false;
    }

	@Override
	public boolean supportsLikeRegex() {
		return true;
	}

    @Override
	public boolean supportsSelectExpression() {
    	return true;
    }

    @Override
	public boolean supportsOnlyLiteralComparison() {
		return true;
	}

	/**
	 * @param field
	 * @param expectedClass
	 * @return
	 * @throws TranslatorException 
	 */
    public Object retrieveValue(Object value, Class<?> expectedClass, DB mongoDB, String fqn, String colName)
            throws TranslatorException {
		if (value == null) {
			return null;
		}

		if (value.getClass().equals(expectedClass)) {
			return value;
		}
		
		if (value instanceof DBRef) {
			Object obj = ((DBRef)value).getId();
			if (obj instanceof BasicDBObject) {
				BasicDBObject bdb = (BasicDBObject)obj;
				return bdb.get(colName);
			}
			return obj;
		}
		else if (value instanceof java.util.Date && expectedClass.equals(java.sql.Date.class)) {
			return new java.sql.Date(((java.util.Date) value).getTime());
		}
		else if (value instanceof java.util.Date && expectedClass.equals(java.sql.Timestamp.class)) {
			return new java.sql.Timestamp(((java.util.Date) value).getTime());
		}
		else if (value instanceof java.util.Date && expectedClass.equals(java.sql.Time.class)) {
			return new java.sql.Time(((java.util.Date) value).getTime());
		}
		else if (value instanceof String && expectedClass.equals(BigDecimal.class)) {
			return new BigDecimal((String)value);
		}
		else if (value instanceof String && expectedClass.equals(BigInteger.class)) {
			return new BigInteger((String)value);
		}
		else if (value instanceof String && expectedClass.equals(Character.class)) {
			return new Character(((String)value).charAt(0));
		}
		else if (value instanceof String && expectedClass.equals(BinaryType.class)) {
			return new BinaryType(((String)value).getBytes());
		}
		else if (value instanceof String && expectedClass.equals(Blob.class)) {
			GridFS gfs = new GridFS(mongoDB, fqn);
			final GridFSDBFile resource = gfs.findOne((String)value);
			if (resource == null) {
				return null;
			}
			return new BlobImpl(new InputStreamFactory() {
				@Override
				public InputStream getInputStream() throws IOException {
					return resource.getInputStream();
				}
			});
		}
		else if (value instanceof String && expectedClass.equals(Clob.class)) {
			GridFS gfs = new GridFS(mongoDB, fqn);
			final GridFSDBFile resource = gfs.findOne((String)value);
			if (resource == null) {
				return null;
			}
			return new ClobImpl(new InputStreamFactory() {
				@Override
				public InputStream getInputStream() throws IOException {
					return resource.getInputStream();
				}
			}, -1);
		}
		else if (value instanceof String && expectedClass.equals(SQLXML.class)) {
			GridFS gfs = new GridFS(mongoDB, fqn);
			final GridFSDBFile resource = gfs.findOne((String)value);
			if (resource == null) {
				return null;
			}
			return new SQLXMLImpl(new InputStreamFactory() {
				@Override
				public InputStream getInputStream() throws IOException {
					return resource.getInputStream();
				}
			});
		}
		else if (value instanceof BasicDBList) {
		    BasicDBList arrayValues = (BasicDBList)value;
            //array
		    if (expectedClass.isArray() && !(arrayValues.get(0) instanceof BasicDBObject)) {
		        Class arrayType = expectedClass.getComponentType();
		        Object array = Array.newInstance(arrayType, arrayValues.size());		        
                for (int i = 0; i < arrayValues.size(); i++) {
                    Object arrayItem = retrieveValue(arrayValues.get(i), arrayType, mongoDB, fqn, colName);
                    Array.set(array, i, arrayItem);
                }		        
                value = array;
		    }
		}
		else if (value instanceof org.bson.types.ObjectId) {
		    org.bson.types.ObjectId id = (org.bson.types.ObjectId) value;
		    value = id.toHexString();
		}
		else {
		    Transform transform = DataTypeManager.getTransform(value.getClass(), expectedClass);
		    if (transform != null) {
		        try {
                    value = transform.transform(value, expectedClass);
                } catch (TransformationException e) {
                    throw new TranslatorException(e);
                }
		    }
		}
		return value;
	}

	/**
	 * Mongodb only supports certain data types, Teiid need to serialize them in other compatible
	 * formats, and convert them back while reading them.
	 * @param value
	 * @return
	 */
	public Object convertToMongoType(Object value, DB mongoDB, String fqn) throws TranslatorException {
		if (value == null) {
			return null;
		}

		try {
			if (value instanceof BigDecimal) {
				return ((BigDecimal)value).doubleValue();
			}
			else if (value instanceof BigInteger) {
				return ((BigInteger)value).doubleValue();
			}
			else if (value instanceof Character) {
				return ((Character)value).toString();
			}
			else if (value instanceof java.sql.Date) {
				return new java.util.Date(((java.sql.Date)value).getTime());
			}
			else if (value instanceof java.sql.Time) {
				return new java.util.Date(((java.sql.Time)value).getTime());
			}
			else if (value instanceof java.sql.Timestamp) {
				return new java.util.Date(((java.sql.Timestamp)value).getTime());
			}
			else if (value instanceof BinaryType) {
				return new Binary(((BinaryType)value).getBytes());
			}
			else if (value instanceof byte[]) {
				return new Binary((byte[])value);
			}			
			else if (!(value instanceof GeometryType) && value instanceof Blob) {
				String uuid = UUID.randomUUID().toString();
				GridFS gfs = new GridFS(mongoDB, fqn);
				GridFSInputFile gfsFile = gfs.createFile(((Blob)value).getBinaryStream());
				gfsFile.setFilename(uuid);
				gfsFile.save();
				return uuid;
			}
			else if (value instanceof Clob) {
				String uuid = UUID.randomUUID().toString();
				GridFS gfs = new GridFS(mongoDB, fqn);
				GridFSInputFile gfsFile = gfs.createFile(((Clob)value).getAsciiStream());
				gfsFile.setFilename(uuid);
				gfsFile.save();
				return uuid;
			}
			else if (value instanceof SQLXML) {
				String uuid = UUID.randomUUID().toString();
				GridFS gfs = new GridFS(mongoDB, fqn);
				GridFSInputFile gfsFile = gfs.createFile(((SQLXML)value).getBinaryStream());
				gfsFile.setFilename(uuid);
				gfsFile.save();
				return uuid;
			} 
			else if (value instanceof Object[]) {
			    BasicDBList list = new BasicDBList();
			    for (Object obj:(Object[])value) {
			        list.add(obj);
			    }
			    return list;
			}
			return value;
		} catch (SQLException e) {
			throw new TranslatorException(e);
		}
	}
	
	public AggregationOptions getOptions(int batchSize) {
	    if (this.version.compareTo(TWO_4) < 0) {
	        return AggregationOptions.builder().batchSize(batchSize).outputMode(AggregationOptions.OutputMode.INLINE).build();
	    }
        return AggregationOptions.builder().batchSize(batchSize).outputMode(AggregationOptions.OutputMode.CURSOR)
                .allowDiskUse(useDisk()).build();
	}
}
