/*
 * 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.resource.adapter.salesforce;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.GZIPInputStream;

import javax.resource.ResourceException;
import javax.security.auth.Subject;

import org.teiid.OAuthCredential;
import org.teiid.core.util.ObjectConverterUtil;
import org.teiid.core.util.PropertiesUtils;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.resource.adapter.salesforce.transport.SalesforceCXFTransport;
import org.teiid.resource.adapter.salesforce.transport.SalesforceConnectorConfig;
import org.teiid.resource.spi.BasicConnection;
import org.teiid.resource.spi.ConnectionContext;
import org.teiid.translator.DataNotAvailableException;
import org.teiid.translator.salesforce.SalesforceConnection;
import org.teiid.translator.salesforce.execution.DataPayload;
import org.teiid.translator.salesforce.execution.DeletedObject;
import org.teiid.translator.salesforce.execution.DeletedResult;
import org.teiid.translator.salesforce.execution.UpdatedResult;

import com.sforce.async.*;
import com.sforce.soap.partner.*;
import com.sforce.soap.partner.Error;
import com.sforce.soap.partner.fault.InvalidFieldFault;
import com.sforce.soap.partner.fault.InvalidIdFault;
import com.sforce.soap.partner.fault.InvalidQueryLocatorFault;
import com.sforce.soap.partner.fault.InvalidSObjectFault;
import com.sforce.soap.partner.fault.MalformedQueryFault;
import com.sforce.soap.partner.fault.UnexpectedErrorFault;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.transport.JdkHttpTransport;

public class SalesforceConnectionImpl extends BasicConnection implements SalesforceConnection {
	
	private static final String ID_FIELD_NAME = "id"; //$NON-NLS-1$
    private static final String PK_CHUNKING_HEADER = "Sforce-Enable-PKChunking"; //$NON-NLS-1$
    private static final int MAX_CHUNK_SIZE = 100000;
	private BulkConnection bulkConnection; 
	private PartnerConnection partnerConnection;
	private SalesforceConnectorConfig config;
	private String restEndpoint;
	private String apiVersion;
	private int pollingInterval = 500; //TODO: this could be configurable
	
	public SalesforceConnectionImpl(SalesForceManagedConnectionFactory mcf) throws ResourceException {
		login(mcf);
	}
	
	SalesforceConnectionImpl(PartnerConnection partnerConnection) {
		this.partnerConnection = partnerConnection;
	}
	
	private void login(SalesForceManagedConnectionFactory mcf) throws ResourceException {
	    config = new SalesforceConnectorConfig();
        String username = mcf.getUsername();
        String password = mcf.getPassword();

        // if security-domain is specified and caller identity is used; then use
        // credentials from subject
        boolean useCXFTransport = mcf.getConfigFile() != null;
        Subject subject = ConnectionContext.getSubject();
        if (subject != null) {
            OAuthCredential oauthCredential = ConnectionContext.getSecurityCredential(subject, OAuthCredential.class);
            if (oauthCredential != null) {
                config.setCredential(OAuthCredential.class.getName(), oauthCredential);
                useCXFTransport = true;
            } else {
                username = ConnectionContext.getUserName(subject, mcf, username);
                password = ConnectionContext.getPassword(subject, mcf, username, password);
            }
        }
		
		config.setCxfConfigFile(mcf.getConfigFile());
		if (useCXFTransport) {
		    config.setTransport(SalesforceCXFTransport.class);
		}
		
        config.setCompression(true);
        config.setTraceMessage(false);

		//set the catch all properties
		String props = mcf.getConfigProperties();
		if (props != null) {
			Properties p = new Properties();
			try {
				p.load(new StringReader(props));
			} catch (IOException e) {
				throw new ResourceException(e);
			}
			PropertiesUtils.setBeanProperties(config, p, null);
		}
		
        config.setUsername(username);
        config.setPassword(password);
        config.setAuthEndpoint(mcf.getURL());
        
        //set proxy if needed
        if (mcf.getProxyURL() != null) {
			try {
				URL proxyURL = new URL(mcf.getProxyURL());
				config.setProxy(proxyURL.getHost(), proxyURL.getPort());
		        config.setProxyUsername(mcf.getProxyUsername());
		        config.setProxyPassword(mcf.getProxyPassword());
			} catch (MalformedURLException e) {
				throw new ResourceException(e);
			}
        }
        if (mcf.getConnectTimeout() != null) {
        	config.setConnectionTimeout((int) Math.min(Integer.MAX_VALUE, mcf.getConnectTimeout()));
        }
        if (mcf.getRequestTimeout() != null) {
        	config.setReadTimeout((int) Math.min(Integer.MAX_VALUE, mcf.getRequestTimeout()));
        }
        
        try {
	        partnerConnection = new TeiidPartnerConnection(config);
	        
	        String endpoint = config.getServiceEndpoint();
	        // The endpoint for the Bulk API service is the same as for the normal
	        // SOAP uri until the /Soap/ part. From here it's '/async/versionNumber'
	        int index = endpoint.indexOf("Soap/u/"); //$NON-NLS-1$
	        int endIndex = endpoint.indexOf('/', index+7);
	        apiVersion = endpoint.substring(index+7,endIndex);
	        String bulkEndpoint = endpoint.substring(0, endpoint.indexOf("Soap/"))+ "async/" + apiVersion;//$NON-NLS-1$ //$NON-NLS-2$
	        config.setRestEndpoint(bulkEndpoint);
			// This value identifies Teiid as a SF certified solution.
			// It was provided by SF and should not be changed.
	        partnerConnection.setCallOptions("RedHat/MetaMatrix/", null); //$NON-NLS-1$
	        bulkConnection = new BulkConnection(config);
			// Test the connection.
			partnerConnection.getUserInfo();
			restEndpoint = endpoint.substring(0, endpoint.indexOf("Soap/"))+ "data/" + "v30.0";//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        } catch (AsyncApiException e) {
        	throw new ResourceException(e);
        } catch (ConnectionException e) {
        	throw new ResourceException(e);
		}
        
		LogManager.logTrace(LogConstants.CTX_CONNECTOR, "Login was successful for username", username); //$NON-NLS-1$
	}
	
	@Override
	public Long getCardinality(String sobject) throws ResourceException {
		InputStream is = null;
		try {
			is = doRestHttpGet(new URL(restEndpoint + "/query/?explain=select+id+from+" + URLEncoder.encode(sobject, "UTF-8"))); //$NON-NLS-1$ //$NON-NLS-2$
			String s = ObjectConverterUtil.convertToString(new InputStreamReader(is, Charset.forName("UTF-8"))); //$NON-NLS-1$
			//TODO: introduce a json parser
			int index = s.indexOf("cardinality"); //$NON-NLS-1$
			if (index < 0) {
				return null;
			}
			index = s.indexOf(":", index); //$NON-NLS-1$
			if (index < 0) {
				return null;
			}
			int end = s.indexOf(",", index); //$NON-NLS-1$
			if (end < 0) {
				end = s.indexOf("}", index); //$NON-NLS-1$
			}
			if (end < 0) {
				return null;
			}
			s = s.substring(index+1, end);
			return Long.valueOf(s);
		} catch (NumberFormatException e) {
			throw new ResourceException(e);
		} catch (MalformedURLException e) {
			throw new ResourceException(e);
		} catch (UnsupportedEncodingException e) {
			throw new ResourceException(e);
		} catch (IOException e) {
			throw new ResourceException(e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
				}
			}
		}
	}
	
	private InputStream doRestHttpGet(URL url) throws IOException {
		HttpURLConnection connection = JdkHttpTransport.createConnection(config, url, null); 
		connection.setRequestProperty("Authorization", "Bearer " + config.getSessionId()); //$NON-NLS-1$ //$NON-NLS-2$

		InputStream in = connection.getInputStream();

		String encoding = connection.getHeaderField("Content-Encoding"); //$NON-NLS-1$
		if ("gzip".equals(encoding)) { //$NON-NLS-1$
			in = new GZIPInputStream(in);
		}

		return in;
	}
	
	public boolean isValid() {
		if(partnerConnection != null) {
			try {
				partnerConnection.getServerTimestamp();
				return true;
			} catch (Throwable t) {
				LogManager.logTrace(LogConstants.CTX_CONNECTOR, "Caught Throwable in isAlive", t); //$NON-NLS-1$
			}
		}
		return false;
	}

	public QueryResult query(String queryString, int batchSize, boolean queryAll) throws ResourceException {
		
		if(batchSize > 2000) {
			batchSize = 2000;
			LogManager.logDetail(LogConstants.CTX_CONNECTOR, "reduced.batch.size"); //$NON-NLS-1$
		}
		
		QueryResult qr = null;
		partnerConnection.setQueryOptions(batchSize);
		try {
			if(queryAll) {
				qr = partnerConnection.queryAll(queryString);
			} else {
				partnerConnection.setMruHeader(false);
				qr = partnerConnection.query(queryString);
			}
		} catch (InvalidFieldFault e) {
			throw new ResourceException(e);
		} catch (MalformedQueryFault e) {
			throw new ResourceException(e);
		} catch (InvalidSObjectFault e) {
			throw new ResourceException(e);
		} catch (InvalidIdFault e) {
			throw new ResourceException(e);
		} catch (UnexpectedErrorFault e) {
			throw new ResourceException(e);
		} catch (InvalidQueryLocatorFault e) {
			throw new ResourceException(e);
		} catch (ConnectionException e) {
			throw new ResourceException(e);
		} finally {
			partnerConnection.clearMruHeader();
			partnerConnection.clearQueryOptions();
		}
		return qr;
	}

	public QueryResult queryMore(String queryLocator, int batchSize) throws ResourceException {
		if(batchSize > 2000) {
			batchSize = 2000;
			LogManager.logDetail(LogConstants.CTX_CONNECTOR, "reduced.batch.size"); //$NON-NLS-1$
		}

		partnerConnection.setQueryOptions(batchSize);
		try {
			return partnerConnection.queryMore(queryLocator);
		} catch (InvalidFieldFault e) {
			throw new ResourceException(e);
		} catch (UnexpectedErrorFault e) {
			throw new ResourceException(e);
		} catch (InvalidQueryLocatorFault e) {
			throw new ResourceException(e);
		} catch (ConnectionException e) {
			throw new ResourceException(e);
		} finally {
			partnerConnection.clearQueryOptions();
		}
		
	}

	public int delete(String[] ids) throws ResourceException {
		DeleteResult[] results = null;
		try {
			results = partnerConnection.delete(ids);
		} catch (UnexpectedErrorFault e) {
			throw new ResourceException(e);
		} catch (ConnectionException e) {
			throw new ResourceException(e);
		}
		
		boolean allGood = true;
		StringBuffer errorMessages = new StringBuffer();
		for(int i = 0; i < results.length; i++) {
			DeleteResult result = results[i];
			if(!result.isSuccess()) {
				if(allGood) {
					errorMessages.append("Error(s) executing DELETE: "); //$NON-NLS-1$
					allGood = false;
				}
				Error[] errors = result.getErrors();
				if(null != errors && errors.length > 0) {
					for(int x = 0; x < errors.length; x++) {
						Error error = errors[x];
						errorMessages.append(error.getMessage()).append(';');
					}
				}
				
			}
		}
		if(!allGood) {
			throw new ResourceException(errorMessages.toString());
		}
		return results.length;
	}
	
	public int upsert(DataPayload data) throws ResourceException {
	    SObject toCreate = new SObject();
        toCreate.setType(data.getType());
        for (DataPayload.Field field : data.getMessageElements()) {
            toCreate.addField(field.name, field.value);
        }
        SObject[] objects = new SObject[] {toCreate};
        UpsertResult[] results;
        try {
            results = partnerConnection.upsert(ID_FIELD_NAME, objects);
        } catch (InvalidFieldFault e) {
            throw new ResourceException(e);
        } catch (InvalidSObjectFault e) {
            throw new ResourceException(e);
        } catch (InvalidIdFault e) {
            throw new ResourceException(e);
        } catch (UnexpectedErrorFault e) {
            throw new ResourceException(e);
        } catch (ConnectionException e) {
            throw new ResourceException(e);
        }
        for (UpsertResult result : results) {
            if(!result.isSuccess()) {
                throw new ResourceException(result.getErrors()[0].getMessage());
            }
        }
        return results.length;
	}

	public int create(DataPayload data) throws ResourceException {
		SObject toCreate = new SObject();
		toCreate.setType(data.getType());
		for (DataPayload.Field field : data.getMessageElements()) {
			toCreate.addField(field.name, field.value);
		}
		SObject[] objects = new SObject[] {toCreate};
		SaveResult[] result;
		try {
			result = partnerConnection.create(objects);
		} catch (InvalidFieldFault e) {
			throw new ResourceException(e);
		} catch (InvalidSObjectFault e) {
			throw new ResourceException(e);
		} catch (InvalidIdFault e) {
			throw new ResourceException(e);
		} catch (UnexpectedErrorFault e) {
			throw new ResourceException(e);
		} catch (ConnectionException e) {
			throw new ResourceException(e);
		}
		return analyzeResult(result);
	}

	public int update(List<DataPayload> updateDataList) throws ResourceException {
		List<SObject> params = new ArrayList<SObject>(updateDataList.size());
		for(int i = 0; i < updateDataList.size(); i++) {
			DataPayload data = updateDataList.get(i);
			SObject toCreate = new SObject();
			toCreate.setType(data.getType());
			toCreate.setId(data.getID());
			for (DataPayload.Field field : data.getMessageElements()) {
				toCreate.addField(field.name, field.value);
			}
			params.add(i, toCreate);
		}
		SaveResult[] result;
			try {
				result = partnerConnection.update(params.toArray(new SObject[params.size()]));
			} catch (InvalidFieldFault e) {
				throw new ResourceException(e);
			} catch (InvalidSObjectFault e) {
				throw new ResourceException(e);
			} catch (InvalidIdFault e) {
				throw new ResourceException(e);
			} catch (UnexpectedErrorFault e) {
				throw new ResourceException(e);
			} catch (ConnectionException e) {
				throw new ResourceException(e);
			}
		return analyzeResult(result);
	}
	
	private int analyzeResult(SaveResult[] results) throws ResourceException {
		for (SaveResult result : results) {
			if(!result.isSuccess()) {
				throw new ResourceException(result.getErrors()[0].getMessage());
			}
		}
		return results.length;
	}

	public UpdatedResult getUpdated(String objectType, Calendar startDate, Calendar endDate) throws ResourceException {
			GetUpdatedResult updated;
			try {
				updated = partnerConnection.getUpdated(objectType, startDate, endDate);
			} catch (InvalidSObjectFault e) {
				throw new ResourceException(e);
			} catch (UnexpectedErrorFault e) {
				throw new ResourceException(e);
			} catch (ConnectionException e) {
				throw new ResourceException(e);
			}
			UpdatedResult result = new UpdatedResult(); 
			result.setLatestDateCovered(updated.getLatestDateCovered());
			result.setIDs(Arrays.asList(updated.getIds()));
			return result;
	}

	public DeletedResult getDeleted(String objectName, Calendar startCalendar,
			Calendar endCalendar) throws ResourceException {
			GetDeletedResult deleted;
			try {
				deleted = partnerConnection.getDeleted(objectName, startCalendar, endCalendar);
			} catch (InvalidSObjectFault e) {
				throw new ResourceException(e);
			} catch (UnexpectedErrorFault e) {
				throw new ResourceException(e);
			} catch (ConnectionException e) {
				throw new ResourceException(e);
			}
			DeletedResult result = new DeletedResult();
			result.setLatestDateCovered(deleted.getLatestDateCovered());
			result.setEarliestDateAvailable(deleted.getEarliestDateAvailable());
			DeletedRecord[] records = deleted.getDeletedRecords();
			List<DeletedObject> resultRecords = new ArrayList<DeletedObject>();
			if(records != null) {
				for (DeletedRecord record : records) {
					DeletedObject object = new DeletedObject();
					object.setID(record.getId());
					object.setDeletedDate(record.getDeletedDate());
					resultRecords.add(object);
				}
			}
			result.setResultRecords(resultRecords);
			return result;
	}
	
	public SObject[] retrieve(String fieldList, String sObjectType, List<String> ids) throws ResourceException {
		try {
			return partnerConnection.retrieve(fieldList, sObjectType, ids.toArray(new String[ids.size()]));
		} catch (InvalidFieldFault e) {
			throw new ResourceException(e);
		} catch (MalformedQueryFault e) {
			throw new ResourceException(e);
		} catch (InvalidSObjectFault e) {
			throw new ResourceException(e);
		} catch (InvalidIdFault e) {
			throw new ResourceException(e);
		} catch (UnexpectedErrorFault e) {
			throw new ResourceException(e);
		} catch (ConnectionException e) {
			throw new ResourceException(e);
		}
		
	}

	public DescribeGlobalResult getObjects() throws ResourceException {
		try {
			return partnerConnection.describeGlobal();
		} catch (UnexpectedErrorFault e) {
			throw new ResourceException(e);
		} catch (ConnectionException e) {
			throw new ResourceException(e);
		}
	}

	public DescribeSObjectResult[] getObjectMetaData(String... objectName) throws ResourceException {
		try {
			return partnerConnection.describeSObjects(objectName);
		} catch (InvalidSObjectFault e) {
			throw new ResourceException(e);
		} catch (UnexpectedErrorFault e) {
			throw new ResourceException(e);
		} catch (ConnectionException e) {
			throw new ResourceException(e);
		}
	}

	@Override
	public void close() throws ResourceException {
		
	}
	
	@Override
	public boolean isAlive() {
		return isValid();
	}

	@Override
	public JobInfo createBulkJob(String objectName, OperationEnum operation, boolean usePkChunking) throws ResourceException {
        try {
			JobInfo job = new JobInfo();
			job.setObject(objectName);
			job.setOperation(operation);
			job.setContentType((operation==OperationEnum.insert||operation==OperationEnum.upsert)?ContentType.XML:ContentType.CSV);
			if (operation==OperationEnum.upsert) {
			    job.setExternalIdFieldName(ID_FIELD_NAME);
			}
			job.setConcurrencyMode(ConcurrencyMode.Parallel);
			if (operation == OperationEnum.query && usePkChunking) {
			    this.bulkConnection.addHeader(PK_CHUNKING_HEADER, "chunkSize=" + MAX_CHUNK_SIZE); //$NON-NLS-1$
			}
			JobInfo info = this.bulkConnection.createJob(job);
			//reset the header
			if (operation == OperationEnum.query) {
			    this.bulkConnection = new BulkConnection(config);
			}
			return info;
		} catch (AsyncApiException e) {
			throw new ResourceException(e);
		}
	}

	@Override
	public String addBatch(List<com.sforce.async.SObject> payload, JobInfo job) throws ResourceException {
		try {
			BatchRequest request = this.bulkConnection.createBatch(job);
			request.addSObjects(payload.toArray(new com.sforce.async.SObject[payload.size()]));
			return request.completeRequest().getId();
		} catch (AsyncApiException e) {
			throw new ResourceException(e);
		}
	}
	
	@Override
	public BatchResultInfo addBatch(String query, JobInfo job) throws ResourceException {
		try {
			BatchInfo batch = this.bulkConnection.createBatchFromStream(job, new ByteArrayInputStream(query.getBytes(Charset.forName("UTF-8")))); //$NON-NLS-1$
			return new BatchResultInfo(batch.getId());
		} catch (AsyncApiException e) {
			throw new ResourceException(e);
		}
	}
	
	@Override
	public BulkBatchResult getBatchQueryResults(String jobId, BatchResultInfo info) throws ResourceException {
		if (info.getResultList() == null && info.getPkBatches() == null) {
			try {
				BatchInfo batch = this.bulkConnection.getBatchInfo(jobId, info.getBatchId());
                switch (batch.getState()) {
    				case NotProcessed:
                        // we need more checks to ensure that chunking is being used.  since
    				    // we don't know, then we'll explicitly check
    				    JobInfo jobStatus = this.bulkConnection.getJobStatus(jobId);
    				    if (jobStatus.getState() == JobStateEnum.Aborted) {
                            throw new ResourceException(JobStateEnum.Aborted.name());
                        }
    				    BatchInfoList batchInfoList = this.bulkConnection.getBatchInfoList(jobId);
    				    LogManager.logTrace(LogConstants.CTX_CONNECTOR, "Pk chunk batches", batchInfoList); //$NON-NLS-1$
    				    BatchInfo[] batchInfo = batchInfoList.getBatchInfo();
    				    LinkedHashMap<String, BatchInfo> pkBactches = new LinkedHashMap<String, BatchInfo>();
    				    boolean anyComplete = false;
    		            for (int i = 0; i < batchInfo.length; i++) {
    		                BatchInfo batchInfoItem = batchInfo[i];
                            if (batchInfoItem.getId().equals(info.getBatchId())) {
    		                    continue; //disregard the initial batch
    		                }
    		                switch (batchInfoItem.getState()) {
    		                case Failed:
    		                case NotProcessed:
                                throw new ResourceException(batchInfoItem.getStateMessage());
    		                case Completed:
    		                    anyComplete = true;
		                    default:
		                        pkBactches.put(batchInfoItem.getId(), batchInfoItem);
    		                }
    		            }
    		            info.setPkBatches(pkBactches);
    				    if (!anyComplete) {
    				        throwDataNotAvailable(info);
    				    }
    				    break;
    				case Completed:
                    {
				        QueryResultList list = this.bulkConnection.getQueryResultList(jobId, info.getBatchId());
                        info.setResultList(list.getResult());
                        break;
				    }
				    case InProgress:
				    case Queued:
                    throwDataNotAvailable(info);
	                 default:
	                    throw new ResourceException(batch.getStateMessage());
				}
			} catch (AsyncApiException e) {
				throw new ResourceException(e);
			}
		}
		try {
		    BulkBatchResult result = null;
		    if (info.getResultList() != null) {
		        result = nextBulkBatch(jobId, info);
		    }
		    if (result == null && info.getPkBatches() != null) {
    		    getNextPkChunkResultList(jobId, info);
    		    result = nextBulkBatch(jobId, info);
    		}
		    info.resetWaitCount();
		    return result;
		} catch (AsyncApiException e) {
		    throw new ResourceException(e);
		}
	}

    private void throwDataNotAvailable(BatchResultInfo info) {
        int waitCount = info.incrementAndGetWaitCount();
        LogManager.logTrace(LogConstants.CTX_CONNECTOR, "Waiting on queued/inprogress, wait number", waitCount); //$NON-NLS-1$
        throw new DataNotAvailableException(pollingInterval * Math.min(8, waitCount));
    }

    private void getNextPkChunkResultList(String jobId,
            BatchResultInfo info) throws AsyncApiException, ResourceException {
        Map<String, BatchInfo> batches = info.getPkBatches();
        if (batches.isEmpty()) {
            return; //terminal condition
        }
        for (Iterator<Map.Entry<String, BatchInfo>> iter = batches.entrySet().iterator(); iter.hasNext();) {
            Map.Entry<String, BatchInfo> entry = iter.next();
            if (entry.getValue().getState() != BatchStateEnum.Completed) {
                continue;
            }
            iter.remove();
            QueryResultList list = this.bulkConnection.getQueryResultList(jobId, entry.getKey());
            info.setResultList(list.getResult());
            return;
        }
        
        //update the batchInfo
        BatchInfoList batchInfoList = this.bulkConnection.getBatchInfoList(jobId);
        BatchInfo[] batchInfo = batchInfoList.getBatchInfo();
        
        String completedId = null;
        for (BatchInfo bi : batchInfo) {
            if (!batches.containsKey(bi.getId())) {
                continue;
            }
            switch (bi.getState()) {
            case Failed:
            case NotProcessed:
                throw new ResourceException(bi.getStateMessage());
            case Completed:
                if (completedId == null) {
                    completedId = bi.getId();
                    batches.remove(completedId);
                } else {
                    batches.put(bi.getId(), bi);
                }
                break;
            }
        }
        
        if (completedId == null) {
            throwDataNotAvailable(info);
        }
        QueryResultList list = this.bulkConnection.getQueryResultList(jobId, completedId);
        info.setResultList(list.getResult());
    }
	
    private BulkBatchResult nextBulkBatch(String jobId, BatchResultInfo info)
            throws AsyncApiException {
		int index = info.getAndIncrementResultNum();
		if (index < info.getResultList().length) {
            String resultId = info.getResultList()[index];
            final InputStream inputStream = bulkConnection.getQueryResultStream(jobId, info.getBatchId(), resultId);
            final CSVReader reader = new CSVReader(inputStream);
            //get rid of the limits as much as possible
            reader.setMaxRowsInFile(Integer.MAX_VALUE);
            reader.setMaxCharsInFile(Integer.MAX_VALUE);
            return new BulkBatchResult() {
                @Override
                public List<String> nextRecord() throws IOException {
                    return reader.nextRecord();
                }
                @Override
                public void close() {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                    }
                }
            };
		}
		return null;
    }

	@Override 
	public JobInfo closeJob(String jobId) throws ResourceException {
		try {
			return this.bulkConnection.closeJob(jobId);
		} catch (AsyncApiException e) {
			throw new ResourceException(e);
		}
	}

	@Override
	public BatchResult[] getBulkResults(JobInfo job, List<String> ids) throws ResourceException {
		try {
			JobInfo info = this.bulkConnection.getJobStatus(job.getId());
			if (info.getNumberBatchesTotal() != info.getNumberBatchesFailed() + info.getNumberBatchesCompleted()) {
				throw new DataNotAvailableException(pollingInterval);
			}
			BatchResult[] results = new BatchResult[ids.size()];
			for (int i = 0; i < ids.size(); i++) {
				results[i] = this.bulkConnection.getBatchResult(job.getId(), ids.get(i));	
			}
			return results;
		} catch (AsyncApiException e) {
			throw new ResourceException(e);
		}
	}
	
	@Override
	public void cancelBulkJob(JobInfo job) throws ResourceException {
		try {
			this.bulkConnection.abortJob(job.getId());
		} catch (AsyncApiException e) {
			throw new ResourceException(e);
		}
	}	
	
	@Override
	public String getVersion() {
		return apiVersion;
	}
	
}
