org.apache.manifoldcf.agents.output.amazoncloudsearch.AmazonCloudSearchConnector.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.manifoldcf.agents.output.amazoncloudsearch.AmazonCloudSearchConnector.java

Source

/* $Id$ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.agents.output.amazoncloudsearch;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.StringReader;
import java.io.BufferedReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Set;
import java.util.HashSet;
import java.util.Date;

import org.apache.commons.io.input.ReaderInputStream;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.manifoldcf.agents.interfaces.IOutputAddActivity;
import org.apache.manifoldcf.agents.interfaces.IOutputNotifyActivity;
import org.apache.manifoldcf.agents.interfaces.IOutputRemoveActivity;
import org.apache.manifoldcf.agents.interfaces.IOutputCheckActivity;
import org.apache.manifoldcf.agents.interfaces.IOutputHistoryActivity;
import org.apache.manifoldcf.agents.interfaces.RepositoryDocument;
import org.apache.manifoldcf.agents.interfaces.ServiceInterruption;
import org.apache.manifoldcf.agents.output.BaseOutputConnector;
import org.apache.manifoldcf.core.interfaces.Specification;
import org.apache.manifoldcf.core.interfaces.ConfigParams;
import org.apache.manifoldcf.core.interfaces.ConfigurationNode;
import org.apache.manifoldcf.core.interfaces.DBInterfaceFactory;
import org.apache.manifoldcf.core.interfaces.IDBInterface;
import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
import org.apache.manifoldcf.core.interfaces.IThreadContext;
import org.apache.manifoldcf.core.interfaces.IHTTPOutput;
import org.apache.manifoldcf.core.interfaces.IPostParameters;
import org.apache.manifoldcf.core.interfaces.IPasswordMapperActivity;
import org.apache.manifoldcf.core.interfaces.SpecificationNode;
import org.apache.manifoldcf.core.interfaces.BinaryInput;
import org.apache.manifoldcf.core.interfaces.TempFileInput;
import org.apache.manifoldcf.core.interfaces.VersionContext;
import org.apache.manifoldcf.core.common.DateParser;
import org.apache.manifoldcf.agents.system.ManifoldCF;
import org.apache.manifoldcf.agents.system.Logging;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.apache.manifoldcf.connectorcommon.jsongen.*;

public class AmazonCloudSearchConnector extends BaseOutputConnector {

    /** Ingestion activity */
    public final static String INGEST_ACTIVITY = "document ingest";
    /** Document removal activity */
    public final static String REMOVE_ACTIVITY = "document deletion";

    /** Forward to the javascript to check the configuration parameters */
    private static final String EDIT_CONFIGURATION_JS = "editConfiguration.js";

    /** Forward to the HTML template to edit the configuration parameters */
    private static final String EDIT_CONFIGURATION_HTML = "editConfiguration.html";

    /** Forward to the HTML template to view the configuration parameters */
    private static final String VIEW_CONFIGURATION_HTML = "viewConfiguration.html";

    /** Local connection */
    protected HttpPost poster = null;

    // What we need for database keys
    protected String serverHost = null;
    protected String serverPath = null;

    /** Document Chunk Manager */
    private DocumentChunkManager documentChunkManager = null;

    /** cloudsearch field name for file body text. */
    private static final String FILE_BODY_TEXT_FIELDNAME = "f_bodytext";

    /** Field name we use for document's URI. */
    private static final String DOCUMENT_URI_FIELDNAME = "document_URI";

    /** Constructor.
     */
    public AmazonCloudSearchConnector() {
    }

    /** Clear out any state information specific to a given thread.
    * This method is called when this object is returned to the connection pool.
    */
    @Override
    public void clearThreadContext() {
        super.clearThreadContext();
        documentChunkManager = null;
    }

    @Override
    public void install(IThreadContext threadContext) throws ManifoldCFException {
        IDBInterface mainDatabase = DBInterfaceFactory.make(threadContext, ManifoldCF.getMasterDatabaseName(),
                ManifoldCF.getMasterDatabaseUsername(), ManifoldCF.getMasterDatabasePassword());

        DocumentChunkManager dcmanager = new DocumentChunkManager(mainDatabase);
        dcmanager.install();
    }

    @Override
    public void deinstall(IThreadContext threadContext) throws ManifoldCFException {
        IDBInterface mainDatabase = DBInterfaceFactory.make(threadContext, ManifoldCF.getMasterDatabaseName(),
                ManifoldCF.getMasterDatabaseUsername(), ManifoldCF.getMasterDatabasePassword());

        DocumentChunkManager dcmanager = new DocumentChunkManager(mainDatabase);
        dcmanager.deinstall();
    }

    /** Return the list of activities that this connector supports (i.e. writes into the log).
    *@return the list.
    */
    @Override
    public String[] getActivitiesList() {
        return new String[] { INGEST_ACTIVITY, REMOVE_ACTIVITY };
    }

    /** Connect.
    *@param configParameters is the set of configuration parameters, which
    * in this case describe the target appliance, basic auth configuration, etc.  (This formerly came
    * out of the ini file.)
    */
    @Override
    public void connect(ConfigParams configParameters) {
        super.connect(configParameters);
    }

    /** This method is called to assess whether to count this connector instance should
    * actually be counted as being connected.
    *@return true if the connector instance is actually connected.
    */
    @Override
    public boolean isConnected() {
        return poster != null;
    }

    /** Close the connection.  Call this before discarding the connection.
    */
    @Override
    public void disconnect() throws ManifoldCFException {
        serverHost = null;
        serverPath = null;
        poster = null;
        super.disconnect();
    }

    /** Set up a session */
    protected void getSession() throws ManifoldCFException {
        if (documentChunkManager == null) {
            IDBInterface databaseHandle = DBInterfaceFactory.make(currentContext,
                    ManifoldCF.getMasterDatabaseName(), ManifoldCF.getMasterDatabaseUsername(),
                    ManifoldCF.getMasterDatabasePassword());
            documentChunkManager = new DocumentChunkManager(databaseHandle);
        }

        serverHost = params.getParameter(AmazonCloudSearchConfig.SERVER_HOST);
        if (serverHost == null)
            throw new ManifoldCFException("Server host parameter required");
        serverPath = params.getParameter(AmazonCloudSearchConfig.SERVER_PATH);
        if (serverPath == null)
            throw new ManifoldCFException("Server path parameter required");
        String proxyProtocol = params.getParameter(AmazonCloudSearchConfig.PROXY_PROTOCOL);
        String proxyHost = params.getParameter(AmazonCloudSearchConfig.PROXY_HOST);
        String proxyPort = params.getParameter(AmazonCloudSearchConfig.PROXY_PORT);

        // Https is OK here without a custom trust store because we know we are talking to an Amazon instance, which has certs that
        // are presumably non-custom.
        String urlStr = "https://" + serverHost + serverPath;
        poster = new HttpPost(urlStr);

        //set proxy
        if (proxyHost != null && proxyHost.length() > 0) {
            try {
                HttpHost proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort), proxyProtocol);
                RequestConfig config = RequestConfig.custom().setProxy(proxy).build();
                poster.setConfig(config);
            } catch (NumberFormatException e) {
                throw new ManifoldCFException("Number format exception: " + e.getMessage(), e);
            }
        }

        poster.addHeader("Content-Type", "application/json");
    }

    /** Test the connection.  Returns a string describing the connection integrity.
    *@return the connection's status as a displayable string.
    */
    @Override
    public String check() throws ManifoldCFException {
        try {
            getSession();
            String responsbody = postData(new ReaderInputStream(new StringReader("[]"), Consts.UTF_8));
            String status = "";

            try {
                status = getStatusFromJsonResponse(responsbody);
            } catch (ManifoldCFException e) {
                Logging.ingest.debug(e);
                return "Could not get status from response body. Check Access Policy setting of your domain of Amazon CloudSearch.: "
                        + e.getMessage();
            }

            if ("error".equalsIgnoreCase(status)) {
                return "Connection working. responsbody : " + responsbody;
            }
            return "Connection NOT working. responsbody : " + responsbody;

        } catch (ServiceInterruption e) {
            Logging.ingest.debug(e);
            return "Transient exception: " + e.getMessage();
        }
    }

    private String getStatusFromJsonResponse(String responsbody) throws ManifoldCFException {
        try {
            JsonParser parser = new JsonFactory().createJsonParser(responsbody);
            while (parser.nextToken() != JsonToken.END_OBJECT) {
                String name = parser.getCurrentName();
                if ("status".equalsIgnoreCase(name)) {
                    parser.nextToken();
                    return parser.getText();
                }
            }
        } catch (JsonParseException e) {
            throw new ManifoldCFException(e);
        } catch (IOException e) {
            throw new ManifoldCFException(e);
        }
        return null;
    }

    private String parseMessage(JsonParser parser) throws JsonParseException, IOException {
        while (parser.nextToken() != JsonToken.END_ARRAY) {
            String name = parser.getCurrentName();
            if ("message".equalsIgnoreCase(name)) {
                parser.nextToken();
                return parser.getText();
            }
        }
        return null;
    }

    private final static Set<String> acceptableMimeTypes = new HashSet<String>();
    static {
        // We presume input can be decoded using UTF-8, so we can accept only UTF-8 and others for which this also applies
        acceptableMimeTypes.add("text/plain;charset=utf-8");
        acceptableMimeTypes.add("text/plain;charset=ascii");
        acceptableMimeTypes.add("text/plain;charset=us-ascii");
        acceptableMimeTypes.add("text/plain");
    }

    /** Detect if a mime type is indexable or not.  This method is used by participating repository connectors to pre-filter the number of
    * unusable documents that will be passed to this output connector.
    *@param outputDescription is the document's output version.
    *@param mimeType is the mime type of the document.
    *@return true if the mime type is indexable by this connector.
    */
    @Override
    public boolean checkMimeTypeIndexable(VersionContext outputDescription, String mimeType,
            IOutputCheckActivity activities) throws ManifoldCFException, ServiceInterruption {
        if (mimeType == null)
            return false;
        return acceptableMimeTypes.contains(mimeType.toLowerCase(Locale.ROOT));
    }

    /** Add (or replace) a document in the output data store using the connector.
    * This method presumes that the connector object has been configured, and it is thus able to communicate with the output data store should that be
    * necessary.
    * The OutputSpecification is *not* provided to this method, because the goal is consistency, and if output is done it must be consistent with the
    * output description, since that was what was partly used to determine if output should be taking place.  So it may be necessary for this method to decode
    * an output description string in order to determine what should be done.
    *@param documentURI is the URI of the document.  The URI is presumed to be the unique identifier which the output data store will use to process
    * and serve the document.  This URI is constructed by the repository connector which fetches the document, and is thus universal across all output connectors.
    *@param outputDescription is the description string that was constructed for this document by the getOutputDescription() method.
    *@param document is the document data to be processed (handed to the output data store).
    *@param authorityNameString is the name of the authority responsible for authorizing any access tokens passed in with the repository document.  May be null.
    *@param activities is the handle to an object that the implementer of an output connector may use to perform operations, such as logging processing activity.
    *@return the document status (accepted or permanently rejected).
    */
    @Override
    public int addOrReplaceDocumentWithException(String documentURI, VersionContext outputDescription,
            RepositoryDocument document, String authorityNameString, IOutputAddActivity activities)
            throws ManifoldCFException, ServiceInterruption, IOException {
        // Establish a session
        getSession();

        String uid = ManifoldCF.hash(documentURI);

        // Build a JSON generator
        JSONObjectReader objectReader = new JSONObjectReader();
        // Build the metadata field part
        JSONObjectReader fieldReader = new JSONObjectReader();
        // Add the type and ID
        objectReader
                .addNameValuePair(new JSONNameValueReader(new JSONStringReader("id"), new JSONStringReader(uid)))
                .addNameValuePair(
                        new JSONNameValueReader(new JSONStringReader("type"), new JSONStringReader("add")))
                .addNameValuePair(new JSONNameValueReader(new JSONStringReader("fields"), fieldReader));

        // Populate the fields...
        Iterator<String> itr = document.getFields();
        while (itr.hasNext()) {
            String fieldName = itr.next();
            Object[] fieldValues = document.getField(fieldName);
            JSONReader[] elements = new JSONReader[fieldValues.length];
            if (fieldValues instanceof Reader[]) {
                for (int i = 0; i < elements.length; i++) {
                    elements[i] = new JSONStringReader((Reader) fieldValues[i]);
                }
            } else if (fieldValues instanceof Date[]) {
                for (int i = 0; i < elements.length; i++) {
                    elements[i] = new JSONStringReader(DateParser.formatISO8601Date((Date) fieldValues[i]));
                }
            } else if (fieldValues instanceof String[]) {
                for (int i = 0; i < elements.length; i++) {
                    elements[i] = new JSONStringReader((String) fieldValues[i]);
                }
            } else
                throw new IllegalStateException("Unexpected metadata type: " + fieldValues.getClass().getName());

            fieldReader.addNameValuePair(
                    new JSONNameValueReader(new JSONStringReader(fieldName), new JSONArrayReader(elements)));
        }

        // Add in the original URI
        fieldReader.addNameValuePair(new JSONNameValueReader(new JSONStringReader(DOCUMENT_URI_FIELDNAME),
                new JSONStringReader(documentURI)));

        // Add the primary content data in.
        fieldReader.addNameValuePair(new JSONNameValueReader(new JSONStringReader(FILE_BODY_TEXT_FIELDNAME),
                new JSONStringReader(new InputStreamReader(document.getBinaryStream(), Consts.UTF_8))));

        documentChunkManager.recordDocument(uid, serverHost, serverPath, documentURI, INGEST_ACTIVITY,
                new Long(document.getBinaryLength()), new ReaderInputStream(objectReader, Consts.UTF_8));
        conditionallyFlushDocuments(activities);
        return DOCUMENTSTATUS_ACCEPTED;
    }

    /** Remove a document using the connector.
    * Note that the last outputDescription is included, since it may be necessary for the connector to use such information to know how to properly remove the document.
    *@param documentURI is the URI of the document.  The URI is presumed to be the unique identifier which the output data store will use to process
    * and serve the document.  This URI is constructed by the repository connector which fetches the document, and is thus universal across all output connectors.
    *@param outputDescription is the last description string that was constructed for this document by the getOutputDescription() method above.
    *@param activities is the handle to an object that the implementer of an output connector may use to perform operations, such as logging processing activity.
    */
    @Override
    public void removeDocument(String documentURI, String outputDescription, IOutputRemoveActivity activities)
            throws ManifoldCFException, ServiceInterruption {
        // Establish a session
        getSession();

        String uid = ManifoldCF.hash(documentURI);

        // Build a JSON generator
        JSONObjectReader objectReader = new JSONObjectReader();
        // Add the type and ID
        objectReader
                .addNameValuePair(new JSONNameValueReader(new JSONStringReader("id"), new JSONStringReader(uid)))
                .addNameValuePair(
                        new JSONNameValueReader(new JSONStringReader("type"), new JSONStringReader("delete")));

        try {
            documentChunkManager.recordDocument(uid, serverHost, serverPath, documentURI, REMOVE_ACTIVITY, null,
                    new ReaderInputStream(objectReader, Consts.UTF_8));
        } catch (IOException e) {
            handleIOException(e);
        }
        conditionallyFlushDocuments(activities);
    }

    @Override
    public void noteJobComplete(IOutputNotifyActivity activities) throws ManifoldCFException, ServiceInterruption {
        getSession();
        flushDocuments(activities);
    }

    protected static final int CHUNK_SIZE = 1000;

    protected void conditionallyFlushDocuments(IOutputHistoryActivity activities)
            throws ManifoldCFException, ServiceInterruption {
        if (documentChunkManager.equalOrMoreThan(serverHost, serverPath, CHUNK_SIZE))
            flushDocuments(activities);
    }

    protected void flushDocuments(IOutputHistoryActivity activities)
            throws ManifoldCFException, ServiceInterruption {
        Logging.ingest.info("AmazonCloudSearch: Starting flush to Amazon");

        // Repeat until we are empty of cached stuff
        int chunkNumber = 0;
        while (true) {
            DocumentRecord[] records = documentChunkManager.readChunk(serverHost, serverPath, CHUNK_SIZE);
            try {
                if (records.length == 0)
                    break;
                // The records consist of up to 1000 individual input streams, which must be all concatenated together into the post
                // To do that, we go into and out of Reader space once again...
                JSONArrayReader arrayReader = new JSONArrayReader();
                for (DocumentRecord dr : records) {
                    arrayReader.addArrayElement(
                            new JSONValueReader(new InputStreamReader(dr.getDataStream(), Consts.UTF_8)));
                }

                //post data..
                String responsbody = postData(new ReaderInputStream(arrayReader, Consts.UTF_8));
                // check status
                String status = getStatusFromJsonResponse(responsbody);
                if ("success".equals(status)) {
                    // Activity-log the individual documents we sent
                    for (DocumentRecord dr : records) {
                        activities.recordActivity(null, dr.getActivity(), dr.getDataSize(), dr.getUri(), "OK",
                                null);
                    }
                    Logging.ingest.info("AmazonCloudSearch: Successfully sent document chunk " + chunkNumber);
                    //remove documents from table..
                    documentChunkManager.deleteChunk(records);
                } else {
                    // Activity-log the individual documents that failed
                    for (DocumentRecord dr : records) {
                        activities.recordActivity(null, dr.getActivity(), dr.getDataSize(), dr.getUri(), "FAILED",
                                responsbody);
                    }
                    Logging.ingest.error("AmazonCloudSearch: Error sending document chunk " + chunkNumber + ": '"
                            + responsbody + "'");
                    throw new ManifoldCFException(
                            "Received error status from service after feeding document.  Response body: '"
                                    + responsbody + "'");
                }
            } catch (ManifoldCFException e) {
                if (e.getErrorCode() == ManifoldCFException.INTERRUPTED)
                    throw e;
                for (DocumentRecord dr : records) {
                    activities.recordActivity(null, dr.getActivity(), dr.getDataSize(), dr.getUri(),
                            e.getClass().getSimpleName().toUpperCase(Locale.ROOT), e.getMessage());
                }
                throw e;
            } catch (ServiceInterruption e) {
                for (DocumentRecord dr : records) {
                    activities.recordActivity(null, dr.getActivity(), dr.getDataSize(), dr.getUri(),
                            e.getClass().getSimpleName().toUpperCase(Locale.ROOT), e.getMessage());
                }
                throw e;
            } finally {
                Throwable exception = null;
                for (DocumentRecord dr : records) {
                    try {
                        dr.close();
                    } catch (Throwable e) {
                        exception = e;
                    }
                }
                if (exception != null) {
                    if (exception instanceof ManifoldCFException)
                        throw (ManifoldCFException) exception;
                    else if (exception instanceof Error)
                        throw (Error) exception;
                    else if (exception instanceof RuntimeException)
                        throw (RuntimeException) exception;
                    else
                        throw new RuntimeException("Unknown exception class thrown: "
                                + exception.getClass().getName() + ": " + exception.getMessage(), exception);
                }
            }
        }
    }

    /**
     * Fill in a Server tab configuration parameter map for calling a Velocity
     * template.
     *
     * @param newMap is the map to fill in
     * @param parameters is the current set of configuration parameters
     */
    private static void fillInServerConfigurationMap(Map<String, Object> newMap, IPasswordMapperActivity mapper,
            ConfigParams parameters) {
        String serverhost = parameters.getParameter(AmazonCloudSearchConfig.SERVER_HOST);
        String serverpath = parameters.getParameter(AmazonCloudSearchConfig.SERVER_PATH);
        String proxyprotocol = parameters.getParameter(AmazonCloudSearchConfig.PROXY_PROTOCOL);
        String proxyhost = parameters.getParameter(AmazonCloudSearchConfig.PROXY_HOST);
        String proxyport = parameters.getParameter(AmazonCloudSearchConfig.PROXY_PORT);

        if (serverhost == null)
            serverhost = AmazonCloudSearchConfig.SERVER_HOST_DEFAULT;
        if (serverpath == null)
            serverpath = AmazonCloudSearchConfig.SERVER_PATH_DEFAULT;
        if (proxyprotocol == null)
            proxyprotocol = AmazonCloudSearchConfig.PROXY_PROTOCOL_DEFAULT;
        if (proxyhost == null)
            proxyhost = AmazonCloudSearchConfig.PROXY_HOST_DEFAULT;
        if (proxyport == null)
            proxyport = AmazonCloudSearchConfig.PROXY_PORT_DEFAULT;

        newMap.put("SERVERHOST", serverhost);
        newMap.put("SERVERPATH", serverpath);
        newMap.put("PROXYPROTOCOL", proxyprotocol);
        newMap.put("PROXYHOST", proxyhost);
        newMap.put("PROXYPORT", proxyport);
    }

    /**
     * View configuration. This method is called in the body section of the
     * connector's view configuration page. Its purpose is to present the
     * connection information to the user. The coder can presume that the HTML
     * that is output from this configuration will be within appropriate <html>
     * and <body> tags.
     *
     * @param threadContext is the local thread context.
     * @param out is the output to which any HTML should be sent.
     * @param parameters are the configuration parameters, as they currently
     * exist, for this connection being configured.
     */
    @Override
    public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out, Locale locale,
            ConfigParams parameters) throws ManifoldCFException, IOException {
        Map<String, Object> paramMap = new HashMap<String, Object>();

        // Fill in map from each tab
        fillInServerConfigurationMap(paramMap, out, parameters);

        Messages.outputResourceWithVelocity(out, locale, VIEW_CONFIGURATION_HTML, paramMap);
    }

    /**
     *
     * Output the configuration header section. This method is called in the
     * head section of the connector's configuration page. Its purpose is to add
     * the required tabs to the list, and to output any javascript methods that
     * might be needed by the configuration editing HTML.
     *
     * @param threadContext is the local thread context.
     * @param out is the output to which any HTML should be sent.
     * @param parameters are the configuration parameters, as they currently
     * exist, for this connection being configured.
     * @param tabsArray is an array of tab names. Add to this array any tab
     * names that are specific to the connector.
     */
    @Override
    public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out, Locale locale,
            ConfigParams parameters, List<String> tabsArray) throws ManifoldCFException, IOException {
        // Add the Server tab
        tabsArray.add(Messages.getString(locale, "AmazonCloudSearchOutputConnector.ServerTabName"));
        // Map the parameters
        Map<String, Object> paramMap = new HashMap<String, Object>();

        // Fill in the parameters from each tab
        fillInServerConfigurationMap(paramMap, out, parameters);

        // Output the Javascript - only one Velocity template for all tabs
        Messages.outputResourceWithVelocity(out, locale, EDIT_CONFIGURATION_JS, paramMap);
    }

    @Override
    public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out, Locale locale,
            ConfigParams parameters, String tabName) throws ManifoldCFException, IOException {

        // Call the Velocity templates for each tab
        Map<String, Object> paramMap = new HashMap<String, Object>();

        // Set the tab name
        paramMap.put("TABNAME", tabName);

        // Fill in the parameters
        fillInServerConfigurationMap(paramMap, out, parameters);

        // Server tab
        Messages.outputResourceWithVelocity(out, locale, EDIT_CONFIGURATION_HTML, paramMap);
    }

    /**
     * Process a configuration post. This method is called at the start of the
     * connector's configuration page, whenever there is a possibility that form
     * data for a connection has been posted. Its purpose is to gather form
     * information and modify the configuration parameters accordingly. The name
     * of the posted form is "editconnection".
     *
     * @param threadContext is the local thread context.
     * @param variableContext is the set of variables available from the post,
     * including binary file post information.
     * @param parameters are the configuration parameters, as they currently
     * exist, for this connection being configured.
     * @return null if all is well, or a string error message if there is an
     * error that should prevent saving of the connection (and cause a
     * redirection to an error page).
     *
     */
    @Override
    public String processConfigurationPost(IThreadContext threadContext, IPostParameters variableContext,
            ConfigParams parameters) throws ManifoldCFException {

        // Server tab parameters
        String serverhost = variableContext.getParameter("serverhost");
        if (serverhost != null)
            parameters.setParameter(AmazonCloudSearchConfig.SERVER_HOST, serverhost);
        String serverpath = variableContext.getParameter("serverpath");
        if (serverpath != null)
            parameters.setParameter(AmazonCloudSearchConfig.SERVER_PATH, serverpath);
        String proxyprotocol = variableContext.getParameter("proxyprotocol");
        if (proxyprotocol != null)
            parameters.setParameter(AmazonCloudSearchConfig.PROXY_PROTOCOL, proxyprotocol);
        String proxyhost = variableContext.getParameter("proxyhost");
        if (proxyhost != null)
            parameters.setParameter(AmazonCloudSearchConfig.PROXY_HOST, proxyhost);
        String proxyport = variableContext.getParameter("proxyport");
        if (proxyport != null)
            parameters.setParameter(AmazonCloudSearchConfig.PROXY_PORT, proxyport);

        return null;
    }

    private String postData(InputStream jsonData) throws ServiceInterruption, ManifoldCFException {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            BinaryInput bi = new TempFileInput(jsonData);
            try {
                poster.setEntity(new InputStreamEntity(bi.getStream(), bi.getLength()));
                HttpResponse res = httpclient.execute(poster);

                HttpEntity resEntity = res.getEntity();
                return EntityUtils.toString(resEntity);
            } finally {
                bi.discard();
            }
        } catch (ClientProtocolException e) {
            throw new ManifoldCFException(e);
        } catch (IOException e) {
            handleIOException(e);
        } finally {
            try {
                httpclient.close();
            } catch (IOException e) {
                //do nothing
            }
        }
        return null;
    }

    private static void handleIOException(IOException e) throws ManifoldCFException, ServiceInterruption {
        if (!(e instanceof java.net.SocketTimeoutException) && (e instanceof InterruptedIOException)) {
            throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED);
        }
        Logging.ingest.warn("Amazon CloudSearch: IO exception: " + e.getMessage(), e);
        long currentTime = System.currentTimeMillis();
        throw new ServiceInterruption("IO exception: " + e.getMessage(), e, currentTime + 300000L,
                currentTime + 3 * 60 * 60000L, -1, false);
    }

}