org.epics.archiverappliance.retrieval.DataRetrievalServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.epics.archiverappliance.retrieval.DataRetrievalServlet.java

Source

/*******************************************************************************
 * Copyright (c) 2011 The Board of Trustees of the Leland Stanford Junior University
 * as Operator of the SLAC National Accelerator Laboratory.
 * Copyright (c) 2011 Brookhaven National Laboratory.
 * EPICS archiver appliance is distributed subject to a Software License Agreement found
 * in file LICENSE that is included with this distribution.
 *******************************************************************************/
package org.epics.archiverappliance.retrieval;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.sql.Timestamp;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.log4j.Logger;
import org.epics.archiverappliance.ByteArray;
import org.epics.archiverappliance.Event;
import org.epics.archiverappliance.EventStream;
import org.epics.archiverappliance.EventStreamDesc;
import org.epics.archiverappliance.StoragePlugin;
import org.epics.archiverappliance.common.BasicContext;
import org.epics.archiverappliance.common.PoorMansProfiler;
import org.epics.archiverappliance.common.TimeSpan;
import org.epics.archiverappliance.common.TimeUtils;
import org.epics.archiverappliance.config.ApplianceInfo;
import org.epics.archiverappliance.config.ArchDBRTypes;
import org.epics.archiverappliance.config.ChannelArchiverDataServerPVInfo;
import org.epics.archiverappliance.config.ConfigService;
import org.epics.archiverappliance.config.ConfigService.STARTUP_SEQUENCE;
import org.epics.archiverappliance.config.PVNames;
import org.epics.archiverappliance.config.PVTypeInfo;
import org.epics.archiverappliance.config.StoragePluginURLParser;
import org.epics.archiverappliance.data.ScalarValue;
import org.epics.archiverappliance.etl.ETLDest;
import org.epics.archiverappliance.mgmt.policy.PolicyConfig.SamplingMethod;
import org.epics.archiverappliance.retrieval.mimeresponses.FlxXMLResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.JPlotResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.JSONResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.MatlabResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.MimeResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.PBRAWResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.QWResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.SVGResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.SinglePVCSVResponse;
import org.epics.archiverappliance.retrieval.mimeresponses.TextResponse;
import org.epics.archiverappliance.retrieval.postprocessors.AfterAllStreams;
import org.epics.archiverappliance.retrieval.postprocessors.DefaultRawPostProcessor;
import org.epics.archiverappliance.retrieval.postprocessors.ExtraFieldsPostProcessor;
import org.epics.archiverappliance.retrieval.postprocessors.FirstSamplePP;
import org.epics.archiverappliance.retrieval.postprocessors.PostProcessor;
import org.epics.archiverappliance.retrieval.postprocessors.PostProcessorWithConsolidatedEventStream;
import org.epics.archiverappliance.retrieval.postprocessors.PostProcessors;
import org.epics.archiverappliance.retrieval.workers.CurrentThreadExecutorService;
import org.epics.archiverappliance.utils.simulation.SimulationEvent;
import org.epics.archiverappliance.utils.ui.GetUrlContent;
import org.json.simple.JSONObject;

import edu.stanford.slac.archiverappliance.PB.EPICSEvent.PayloadInfo;
import edu.stanford.slac.archiverappliance.PB.EPICSEvent.PayloadInfo.Builder;
import edu.stanford.slac.archiverappliance.PB.utils.LineEscaper;

/**
 * Main servlet for retrieval of data.
 * All data retrieval is funneled thru here.
 * @author mshankar
 *
 */
@SuppressWarnings("serial")
public class DataRetrievalServlet extends HttpServlet {
    public static final int SERIAL_PARALLEL_MEMORY_CUTOFF_MB = 60;
    private static final String ARCH_APPL_PING_PV = "ArchApplPingPV";
    private static Logger logger = Logger.getLogger(DataRetrievalServlet.class.getName());

    static class MimeMappingInfo {
        Class<? extends MimeResponse> mimeresponseClass;
        String contentType;

        public MimeMappingInfo(Class<? extends MimeResponse> mimeresponseClass, String contentType) {
            super();
            this.mimeresponseClass = mimeresponseClass;
            this.contentType = contentType;
        }
    }

    private static HashMap<String, MimeMappingInfo> mimeresponses = new HashMap<String, MimeMappingInfo>();
    static {
        mimeresponses.put("raw", new MimeMappingInfo(PBRAWResponse.class, "application/x-protobuf"));
        mimeresponses.put("svg", new MimeMappingInfo(SVGResponse.class, "image/svg+xml"));
        mimeresponses.put("json", new MimeMappingInfo(JSONResponse.class, "application/json"));
        mimeresponses.put("qw", new MimeMappingInfo(QWResponse.class, "application/json"));
        mimeresponses.put("jplot", new MimeMappingInfo(JPlotResponse.class, "application/json"));
        mimeresponses.put("csv", new MimeMappingInfo(SinglePVCSVResponse.class, "text/csv"));
        mimeresponses.put("flx", new MimeMappingInfo(FlxXMLResponse.class, "text/xml"));
        mimeresponses.put("txt", new MimeMappingInfo(TextResponse.class, "text/plain"));
        mimeresponses.put("mat", new MimeMappingInfo(MatlabResponse.class, "application/matlab"));
    }

    private ConfigService configService = null;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String[] pathnameSplit = req.getPathInfo().split("/");
        String requestName = (pathnameSplit[pathnameSplit.length - 1].split("\\."))[0];

        if (requestName.equals("getData")) {
            logger.info("User requesting data for single PV");
            doGetSinglePV(req, resp);
        } else if (requestName.equals("getDataForPVs")) {
            logger.info("User requesting data for multiple PVs");
            doGetMultiPV(req, resp);
        } else {
            String msg = "\"" + requestName + "\" is not a valid API method.";
            resp.setHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, msg);
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
        }

        return;

    }

    private void doGetSinglePV(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        PoorMansProfiler pmansProfiler = new PoorMansProfiler();
        String pvName = req.getParameter("pv");

        if (configService.getStartupState() != STARTUP_SEQUENCE.STARTUP_COMPLETE) {
            String msg = "Cannot process data retrieval requests for PV " + pvName
                    + " until the appliance has completely started up.";
            logger.error(msg);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, msg);
            return;
        }

        String startTimeStr = req.getParameter("from");
        String endTimeStr = req.getParameter("to");
        boolean useReduced = false;
        String useReducedStr = req.getParameter("usereduced");
        if (useReducedStr != null && !useReducedStr.equals("")) {
            try {
                useReduced = Boolean.parseBoolean(useReducedStr);
            } catch (Exception ex) {
                logger.error("Exception parsing usereduced", ex);
                useReduced = false;
            }
        }
        String extension = req.getPathInfo().split("\\.")[1];
        logger.info("Mime is " + extension);

        boolean useChunkedEncoding = true;
        String doNotChunkStr = req.getParameter("donotchunk");
        if (doNotChunkStr != null && !doNotChunkStr.equals("false")) {
            logger.info("Turning off HTTP chunked encoding");
            useChunkedEncoding = false;
        }

        boolean fetchLatestMetadata = false;
        String fetchLatestMetadataStr = req.getParameter("fetchLatestMetadata");
        if (fetchLatestMetadataStr != null && fetchLatestMetadataStr.equals("true")) {
            logger.info("Adding a call to the engine to fetch the latest metadata");
            fetchLatestMetadata = true;
        }

        // For data retrieval we need a PV info. However, in case of PV's that have long since retired, we may not want to have PVTypeInfo's in the system.
        // So, we support a template PV that lays out the data sources.
        // During retrieval, you can pass in the PV as a template and we'll clone this and make a temporary copy.
        String retiredPVTemplate = req.getParameter("retiredPVTemplate");

        if (pvName == null) {
            String msg = "PV name is null.";
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            return;
        }

        if (pvName.equals(ARCH_APPL_PING_PV)) {
            logger.debug("Processing ping PV - this is used to validate the connection with the client.");
            processPingPV(req, resp);
            return;
        }

        if (pvName.endsWith(".VAL")) {
            int len = pvName.length();
            pvName = pvName.substring(0, len - 4);
            logger.info("Removing .VAL from pvName for request giving " + pvName);
        }

        // ISO datetimes are of the form "2011-02-02T08:00:00.000Z"
        Timestamp end = TimeUtils.plusHours(TimeUtils.now(), 1);
        if (endTimeStr != null) {
            try {
                end = TimeUtils.convertFromISO8601String(endTimeStr);
            } catch (IllegalArgumentException ex) {
                try {
                    end = TimeUtils.convertFromDateTimeStringWithOffset(endTimeStr);
                } catch (IllegalArgumentException ex2) {
                    String msg = "Cannot parse time" + endTimeStr;
                    logger.warn(msg, ex2);
                    resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                    return;
                }
            }
        }

        // We get one day by default
        Timestamp start = TimeUtils.minusDays(end, 1);
        if (startTimeStr != null) {
            try {
                start = TimeUtils.convertFromISO8601String(startTimeStr);
            } catch (IllegalArgumentException ex) {
                try {
                    start = TimeUtils.convertFromDateTimeStringWithOffset(startTimeStr);
                } catch (IllegalArgumentException ex2) {
                    String msg = "Cannot parse time " + startTimeStr;
                    logger.warn(msg, ex2);
                    resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                    return;
                }
            }
        }

        if (end.before(start)) {
            String msg = "For request, end " + end.toString() + " is before start " + start.toString() + " for pv "
                    + pvName;
            logger.error(msg);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        LinkedList<TimeSpan> requestTimes = new LinkedList<TimeSpan>();

        // We can specify a list of time stamp pairs using the optional timeranges parameter
        String timeRangesStr = req.getParameter("timeranges");
        if (timeRangesStr != null) {
            boolean continueWithRequest = parseTimeRanges(resp, pvName, requestTimes, timeRangesStr);
            if (!continueWithRequest) {
                // Cannot parse the time ranges properly; we so abort the request.
                return;
            }

            // Override the start and the end so that the mergededup consumer works correctly.
            start = requestTimes.getFirst().getStartTime();
            end = requestTimes.getLast().getEndTime();

        } else {
            requestTimes.add(new TimeSpan(start, end));
        }

        assert (requestTimes.size() > 0);

        String postProcessorUserArg = req.getParameter("pp");
        if (pvName.contains("(")) {
            if (!pvName.contains(")")) {
                logger.error("Unbalanced paran " + pvName);
                resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
            String[] components = pvName.split("[(,)]");
            postProcessorUserArg = components[0];
            pvName = components[1];
            if (components.length > 2) {
                for (int i = 2; i < components.length; i++) {
                    postProcessorUserArg = postProcessorUserArg + "_" + components[i];
                }
            }
            logger.info("After parsing the function call syntax pvName is " + pvName
                    + " and postProcessorUserArg is " + postProcessorUserArg);
        }

        PostProcessor postProcessor = PostProcessors.findPostProcessor(postProcessorUserArg);

        PVTypeInfo typeInfo = PVNames.determineAppropriatePVTypeInfo(pvName, configService);
        pmansProfiler.mark("After PVTypeInfo");

        if (typeInfo == null && RetrievalState.includeExternalServers(req)) {
            logger.debug("Checking to see if pv " + pvName + " is served by a external Archiver Server");
            typeInfo = checkIfPVisServedByExternalServer(pvName, start, req, resp, useChunkedEncoding);
        }

        if (typeInfo == null) {
            if (resp.isCommitted()) {
                logger.debug("Proxied the data thru an external server for PV " + pvName);
                return;
            }
        }

        if (typeInfo == null) {
            if (retiredPVTemplate != null) {
                PVTypeInfo templateTypeInfo = PVNames.determineAppropriatePVTypeInfo(retiredPVTemplate,
                        configService);
                if (templateTypeInfo != null) {
                    typeInfo = new PVTypeInfo(pvName, templateTypeInfo);
                    typeInfo.setPaused(true);
                    typeInfo.setApplianceIdentity(configService.getMyApplianceInfo().getIdentity());
                    // Somehow tell the code downstream that this is a fake typeInfo.
                    typeInfo.setSamplingMethod(SamplingMethod.DONT_ARCHIVE);
                    logger.debug("Using a template PV for " + pvName + " Need to determine the actual DBR type.");
                    setActualDBRTypeFromData(pvName, typeInfo, configService);
                }
            }
        }

        if (typeInfo == null) {
            logger.error("Unable to find typeinfo for pv " + pvName);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        if (postProcessor == null) {
            if (useReduced) {
                String defaultPPClassName = configService.getInstallationProperties().getProperty(
                        "org.epics.archiverappliance.retrieval.DefaultUseReducedPostProcessor",
                        FirstSamplePP.class.getName());
                logger.debug("Using the default usereduced preprocessor " + defaultPPClassName);
                try {
                    postProcessor = (PostProcessor) Class.forName(defaultPPClassName).newInstance();
                } catch (Exception ex) {
                    logger.error("Exception constructing new instance of post processor " + defaultPPClassName, ex);
                    postProcessor = null;
                }
            }
        }

        if (postProcessor == null) {
            logger.debug("Using the default raw preprocessor");
            postProcessor = new DefaultRawPostProcessor();
        }

        ApplianceInfo applianceForPV = configService.getApplianceForPV(pvName);
        if (applianceForPV == null) {
            // TypeInfo cannot be null here...
            assert (typeInfo != null);
            applianceForPV = configService.getAppliance(typeInfo.getApplianceIdentity());
        }

        if (!applianceForPV.equals(configService.getMyApplianceInfo())) {
            // Data for pv is elsewhere. Proxy/redirect and return.
            proxyRetrievalRequest(req, resp, pvName, useChunkedEncoding,
                    applianceForPV.getRetrievalURL() + "/../data");
            return;
        }

        pmansProfiler.mark("After Appliance Info");

        String pvNameFromRequest = pvName;

        String fieldName = PVNames.getFieldName(pvName);
        if (fieldName != null && !fieldName.equals("") && !pvName.equals(typeInfo.getPvName())) {
            logger.debug("We reset the pvName " + pvName + " to one from the typeinfo " + typeInfo.getPvName()
                    + " as that determines the name of the stream. Also using ExtraFieldsPostProcessor");
            pvName = typeInfo.getPvName();
            postProcessor = new ExtraFieldsPostProcessor(fieldName);
        }

        try {
            // Postprocessors get their mandatory arguments from the request.
            // If user does not pass in the expected request, throw an exception.
            postProcessor.initialize(postProcessorUserArg, pvName);
        } catch (Exception ex) {
            logger.error("Postprocessor threw an exception during initialization for " + pvName, ex);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        try (BasicContext retrievalContext = new BasicContext(typeInfo.getDBRType(), pvNameFromRequest);
                MergeDedupConsumer mergeDedupCountingConsumer = createMergeDedupConsumer(resp, extension,
                        useChunkedEncoding);
                RetrievalExecutorResult executorResult = determineExecutorForPostProcessing(pvName, typeInfo,
                        requestTimes, req, postProcessor)) {
            HashMap<String, String> engineMetadata = null;
            if (fetchLatestMetadata) {
                // Make a call to the engine to fetch the latest metadata.
                engineMetadata = fetchLatestMedataFromEngine(pvName, applianceForPV);
            }

            LinkedList<Future<RetrievalResult>> retrievalResultFutures = resolveAllDataSources(pvName, typeInfo,
                    postProcessor, applianceForPV, retrievalContext, executorResult, req, resp);
            pmansProfiler.mark("After data source resolution");

            long s1 = System.currentTimeMillis();
            String currentlyProcessingPV = null;

            List<Future<EventStream>> eventStreamFutures = getEventStreamFuturesFromRetrievalResults(executorResult,
                    retrievalResultFutures);

            logger.debug(
                    "Done with the RetrievalResult's; moving onto the individual event stream from each source for "
                            + pvName);
            pmansProfiler.mark("After retrieval results");

            for (Future<EventStream> future : eventStreamFutures) {
                EventStreamDesc sourceDesc = null;
                try (EventStream eventStream = future.get()) {
                    sourceDesc = null; // Reset it for each loop iteration.
                    sourceDesc = eventStream.getDescription();
                    if (sourceDesc == null) {
                        logger.warn("Skipping event stream without a desc for pv " + pvName);
                        continue;
                    }

                    logger.debug("Processing event stream for pv " + pvName + " from source "
                            + ((eventStream.getDescription() != null) ? eventStream.getDescription().getSource()
                                    : " unknown"));

                    try {
                        mergeTypeInfo(typeInfo, sourceDesc, engineMetadata);
                    } catch (MismatchedDBRTypeException mex) {
                        logger.error(mex.getMessage(), mex);
                        continue;
                    }

                    if (currentlyProcessingPV == null || !currentlyProcessingPV.equals(pvName)) {
                        logger.debug("Switching to new PV " + pvName
                                + " In some mime responses we insert special headers at the beginning of the response. Calling the hook for that");
                        currentlyProcessingPV = pvName;
                        mergeDedupCountingConsumer.processingPV(currentlyProcessingPV, start, end,
                                (eventStream != null) ? sourceDesc : null);
                    }

                    try {
                        // If the postProcessor does not have a consolidated event stream, we send each eventstream across as we encounter it.
                        // Else we send the consolidatedEventStream down below.
                        if (!(postProcessor instanceof PostProcessorWithConsolidatedEventStream)) {
                            mergeDedupCountingConsumer.consumeEventStream(eventStream);
                            resp.flushBuffer();
                        }
                    } catch (Exception ex) {
                        if (ex != null && ex.toString() != null && ex.toString().contains("ClientAbortException")) {
                            // We check for ClientAbortException etc this way to avoid including tomcat jars in the build path.
                            logger.debug(
                                    "Exception when consuming and flushing data from " + sourceDesc.getSource(),
                                    ex);
                        } else {
                            logger.error("Exception when consuming and flushing data from " + sourceDesc.getSource()
                                    + "-->" + ex.toString(), ex);
                        }
                    }
                    pmansProfiler.mark("After event stream " + eventStream.getDescription().getSource());
                } catch (Exception ex) {
                    if (ex != null && ex.toString() != null && ex.toString().contains("ClientAbortException")) {
                        // We check for ClientAbortException etc this way to avoid including tomcat jars in the build path.
                        logger.debug("Exception when consuming and flushing data from "
                                + (sourceDesc != null ? sourceDesc.getSource() : "N/A"), ex);
                    } else {
                        logger.error("Exception when consuming and flushing data from "
                                + (sourceDesc != null ? sourceDesc.getSource() : "N/A") + "-->" + ex.toString(),
                                ex);
                    }
                }
            }

            if (postProcessor instanceof PostProcessorWithConsolidatedEventStream) {
                try (EventStream eventStream = ((PostProcessorWithConsolidatedEventStream) postProcessor)
                        .getConsolidatedEventStream()) {
                    EventStreamDesc sourceDesc = eventStream.getDescription();
                    if (sourceDesc == null) {
                        logger.error("Skipping event stream without a desc for pv " + pvName
                                + " and post processor " + postProcessor.getExtension());
                    } else {
                        mergeDedupCountingConsumer.consumeEventStream(eventStream);
                        resp.flushBuffer();
                    }
                }
            }

            // If the postProcessor needs to send final data across, give it a chance now...
            if (postProcessor instanceof AfterAllStreams) {
                EventStream finalEventStream = ((AfterAllStreams) postProcessor).anyFinalData();
                if (finalEventStream != null) {
                    mergeDedupCountingConsumer.consumeEventStream(finalEventStream);
                    resp.flushBuffer();
                }
            }

            pmansProfiler.mark("After writing all eventstreams to response");

            long s2 = System.currentTimeMillis();
            logger.info("For the complete request, found a total of "
                    + mergeDedupCountingConsumer.totalEventsForAllPVs + " in " + (s2 - s1) + "(ms)" + " skipping "
                    + mergeDedupCountingConsumer.skippedEventsForAllPVs + " events" + " deduping involved "
                    + mergeDedupCountingConsumer.comparedEventsForAllPVs + " compares.");
        } catch (Exception ex) {
            if (ex != null && ex.toString() != null && ex.toString().contains("ClientAbortException")) {
                // We check for ClientAbortException etc this way to avoid including tomcat jars in the build path.
                logger.debug("Exception when retrieving data ", ex);
            } else {
                logger.error("Exception when retrieving data " + "-->" + ex.toString(), ex);
            }
        }
        pmansProfiler.mark("After all closes and flushing all buffers");

        // Till we determine all the if conditions where we log this, we log sparingly..
        if (pmansProfiler.totalTimeMS() > 5000) {
            logger.error("Retrieval time for " + pvName + " from " + startTimeStr + " to " + endTimeStr
                    + pmansProfiler.toString());
        }
    }

    private void doGetMultiPV(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        PoorMansProfiler pmansProfiler = new PoorMansProfiler();

        // Gets the list of PVs specified by the `pv` parameter
        // String arrays might be inefficient for retrieval. In any case, they are sorted, which is essential later on.
        List<String> pvNames = Arrays.asList(req.getParameterValues("pv"));

        // Ensuring that the AA has finished starting up before requests are accepted.
        if (configService.getStartupState() != STARTUP_SEQUENCE.STARTUP_COMPLETE) {
            String msg = "Cannot process data retrieval requests for specified PVs ("
                    + StringUtils.join(pvNames, ", ") + ") until the appliance has completely started up.";
            logger.error(msg);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, msg);
            return;
        }

        // Getting various fields from arguments
        String startTimeStr = req.getParameter("from");
        String endTimeStr = req.getParameter("to");
        boolean useReduced = false;
        String useReducedStr = req.getParameter("usereduced");
        if (useReducedStr != null && !useReducedStr.equals("")) {
            try {
                useReduced = Boolean.parseBoolean(useReducedStr);
            } catch (Exception ex) {
                logger.error("Exception parsing usereduced", ex);
                useReduced = false;
            }
        }

        // Getting MIME type
        String extension = req.getPathInfo().split("\\.")[1];
        logger.info("Mime is " + extension);

        if (!extension.equals("json") && !extension.equals("raw") && !extension.equals("jplot")
                && !extension.equals("qw")) {
            String msg = "Mime type " + extension + " is not supported. Please use \"json\", \"jplot\" or \"raw\".";
            resp.setHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            return;
        }

        boolean useChunkedEncoding = true;
        String doNotChunkStr = req.getParameter("donotchunk");
        if (doNotChunkStr != null && !doNotChunkStr.equals("false")) {
            logger.info("Turning off HTTP chunked encoding");
            useChunkedEncoding = false;
        }

        boolean fetchLatestMetadata = false;
        String fetchLatestMetadataStr = req.getParameter("fetchLatestMetadata");
        if (fetchLatestMetadataStr != null && fetchLatestMetadataStr.equals("true")) {
            logger.info("Adding a call to the engine to fetch the latest metadata");
            fetchLatestMetadata = true;
        }

        // For data retrieval we need a PV info. However, in case of PV's that have long since retired, we may not want to have PVTypeInfo's in the system.
        // So, we support a template PV that lays out the data sources.
        // During retrieval, you can pass in the PV as a template and we'll clone this and make a temporary copy.
        String retiredPVTemplate = req.getParameter("retiredPVTemplate");

        // Goes through given PVs and returns bad request error.
        int nullPVs = 0;
        for (String pvName : pvNames) {
            if (pvName == null) {
                nullPVs++;
            }
            if (nullPVs > 0) {
                logger.warn("Some PVs are null in the request.");
                resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
        }

        if (pvNames.toString().matches("^.*" + ARCH_APPL_PING_PV + ".*$")) {
            logger.debug("Processing ping PV - this is used to validate the connection with the client.");
            processPingPV(req, resp);
            return;
        }

        for (String pvName : pvNames)
            if (pvName.endsWith(".VAL")) {
                int len = pvName.length();
                pvName = pvName.substring(0, len - 4);
                logger.info("Removing .VAL from pvName for request giving " + pvName);
            }

        // ISO datetimes are of the form "2011-02-02T08:00:00.000Z"
        Timestamp end = TimeUtils.plusHours(TimeUtils.now(), 1);
        if (endTimeStr != null) {
            try {
                end = TimeUtils.convertFromISO8601String(endTimeStr);
            } catch (IllegalArgumentException ex) {
                try {
                    end = TimeUtils.convertFromDateTimeStringWithOffset(endTimeStr);
                } catch (IllegalArgumentException ex2) {
                    String msg = "Cannot parse time " + endTimeStr;
                    logger.warn(msg, ex2);
                    resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                    return;
                }
            }
        }

        // We get one day by default
        Timestamp start = TimeUtils.minusDays(end, 1);
        if (startTimeStr != null) {
            try {
                start = TimeUtils.convertFromISO8601String(startTimeStr);
            } catch (IllegalArgumentException ex) {
                try {
                    start = TimeUtils.convertFromDateTimeStringWithOffset(startTimeStr);
                } catch (IllegalArgumentException ex2) {
                    String msg = "Cannot parse time " + startTimeStr;
                    logger.warn(msg, ex2);
                    resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                    return;
                }
            }
        }

        if (end.before(start)) {
            String msg = "For request, end " + end.toString() + " is before start " + start.toString() + " for pvs "
                    + StringUtils.join(pvNames, ", ");
            logger.error(msg);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            return;
        }

        LinkedList<TimeSpan> requestTimes = new LinkedList<TimeSpan>();

        // We can specify a list of time stamp pairs using the optional timeranges parameter
        String timeRangesStr = req.getParameter("timeranges");
        if (timeRangesStr != null) {
            boolean continueWithRequest = parseTimeRanges(resp, "[" + StringUtils.join(pvNames, ", ") + "]",
                    requestTimes, timeRangesStr);
            if (!continueWithRequest) {
                // Cannot parse the time ranges properly; we so abort the request.
                String msg = "The specified time ranges could not be processed appropriately. Aborting.";
                logger.info(msg);
                resp.setHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                return;
            }

            // Override the start and the end so that the mergededup consumer works correctly.
            start = requestTimes.getFirst().getStartTime();
            end = requestTimes.getLast().getEndTime();

        } else {
            requestTimes.add(new TimeSpan(start, end));
        }

        assert (requestTimes.size() > 0);

        // Get a post processor for each PV specified in pvNames
        // If PV in the form <pp>(<pv>), process it
        String postProcessorUserArg = req.getParameter("pp");
        List<String> postProcessorUserArgs = new ArrayList<>(pvNames.size());
        List<PostProcessor> postProcessors = new ArrayList<>(pvNames.size());
        for (int i = 0; i < pvNames.size(); i++) {
            postProcessorUserArgs.add(postProcessorUserArg);

            if (pvNames.get(i).contains("(")) {
                if (!pvNames.get(i).contains(")")) {
                    String msg = "Unbalanced paren " + pvNames.get(i);
                    logger.error(msg);
                    resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                    return;
                }
                String[] components = pvNames.get(i).split("[(,)]");
                postProcessorUserArg = components[0];
                postProcessorUserArgs.set(i, postProcessorUserArg);
                pvNames.set(i, components[1]);
                if (components.length > 2) {
                    for (int j = 2; j < components.length; j++) {
                        postProcessorUserArgs.set(i, postProcessorUserArgs.get(i) + "_" + components[j]);
                    }
                }
                logger.info("After parsing the function call syntax pvName is " + pvNames.get(i)
                        + " and postProcessorUserArg is " + postProcessorUserArg);
            }
            postProcessors.add(PostProcessors.findPostProcessor(postProcessorUserArg));
        }

        List<PVTypeInfo> typeInfos = new ArrayList<PVTypeInfo>(pvNames.size());
        for (int i = 0; i < pvNames.size(); i++) {
            typeInfos.add(PVNames.determineAppropriatePVTypeInfo(pvNames.get(i), configService));
        }
        pmansProfiler.mark("After PVTypeInfo");

        for (int i = 0; i < pvNames.size(); i++)
            if (typeInfos.get(i) == null && RetrievalState.includeExternalServers(req)) {
                logger.debug(
                        "Checking to see if pv " + pvNames.get(i) + " is served by a external Archiver Server");
                typeInfos.set(i,
                        checkIfPVisServedByExternalServer(pvNames.get(i), start, req, resp, useChunkedEncoding));
            }

        for (int i = 0; i < pvNames.size(); i++) {
            if (typeInfos.get(i) == null) {
                // TODO Only needed if we're forwarding the request to another server.
                if (resp.isCommitted()) {
                    logger.debug("Proxied the data thru an external server for PV " + pvNames.get(i));
                    return;
                }

                if (retiredPVTemplate != null) {
                    PVTypeInfo templateTypeInfo = PVNames.determineAppropriatePVTypeInfo(retiredPVTemplate,
                            configService);
                    if (templateTypeInfo != null) {
                        typeInfos.set(i, new PVTypeInfo(pvNames.get(i), templateTypeInfo));
                        typeInfos.get(i).setPaused(true);
                        typeInfos.get(i).setApplianceIdentity(configService.getMyApplianceInfo().getIdentity());
                        // Somehow tell the code downstream that this is a fake typeInfos.
                        typeInfos.get(i).setSamplingMethod(SamplingMethod.DONT_ARCHIVE);
                        logger.debug("Using a template PV for " + pvNames.get(i)
                                + " Need to determine the actual DBR type.");
                        setActualDBRTypeFromData(pvNames.get(i), typeInfos.get(i), configService);
                    }
                }
            }

            if (typeInfos.get(i) == null) {
                String msg = "Unable to find typeinfo for pv " + pvNames.get(i);
                logger.error(msg);
                resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                resp.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
                return;
            }

            if (postProcessors.get(i) == null) {
                if (useReduced) {
                    String defaultPPClassName = configService.getInstallationProperties().getProperty(
                            "org.epics.archiverappliance.retrieval.DefaultUseReducedPostProcessor",
                            FirstSamplePP.class.getName());
                    logger.debug("Using the default usereduced preprocessor " + defaultPPClassName);
                    try {
                        postProcessors.set(i, (PostProcessor) Class.forName(defaultPPClassName).newInstance());
                    } catch (Exception ex) {
                        logger.error("Exception constructing new instance of post processor " + defaultPPClassName,
                                ex);
                        postProcessors.set(i, null);
                    }
                }
            }

            if (postProcessors.get(i) == null) {
                logger.debug("Using the default raw preprocessor");
                postProcessors.set(i, new DefaultRawPostProcessor());
            }
        }

        // Get the appliances for each of the PVs
        List<ApplianceInfo> applianceForPVs = new ArrayList<ApplianceInfo>(pvNames.size());
        for (int i = 0; i < pvNames.size(); i++) {
            applianceForPVs.add(configService.getApplianceForPV(pvNames.get(i)));
            if (applianceForPVs.get(i) == null) {
                // TypeInfo cannot be null here...
                assert (typeInfos.get(i) != null);
                applianceForPVs.set(i, configService.getAppliance(typeInfos.get(i).getApplianceIdentity()));
            }
        }

        /*
         * Retrieving the external appliances if the current appliance has not got the PV assigned to it, and
         * storing the associated information of the PVs in that appliance.
         */
        Map<String, ArrayList<PVInfoForClusterRetrieval>> applianceToPVs = new HashMap<String, ArrayList<PVInfoForClusterRetrieval>>();
        for (int i = 0; i < pvNames.size(); i++) {
            if (!applianceForPVs.get(i).equals(configService.getMyApplianceInfo())) {

                ArrayList<PVInfoForClusterRetrieval> appliancePVs = applianceToPVs
                        .get(applianceForPVs.get(i).getMgmtURL());
                appliancePVs = (appliancePVs == null) ? new ArrayList<>() : appliancePVs;
                PVInfoForClusterRetrieval pvInfoForRetrieval = new PVInfoForClusterRetrieval(pvNames.get(i),
                        typeInfos.get(i), postProcessors.get(i), applianceForPVs.get(i));
                appliancePVs.add(pvInfoForRetrieval);
                applianceToPVs.put(applianceForPVs.get(i).getRetrievalURL(), appliancePVs);
            }
        }

        List<List<Future<EventStream>>> listOfEventStreamFuturesLists = new ArrayList<List<Future<EventStream>>>();
        Set<String> retrievalURLs = applianceToPVs.keySet();
        if (retrievalURLs.size() > 0) {
            // Get list of PVs and redirect them to appropriate appliance to be retrieved.
            String retrievalURL;
            ArrayList<PVInfoForClusterRetrieval> pvInfos;
            while (!((retrievalURL = retrievalURLs.iterator().next()) != null)) {
                // Get array list of PVs for appliance
                pvInfos = applianceToPVs.get(retrievalURL);
                try {
                    List<List<Future<EventStream>>> resultFromForeignAppliances = retrieveEventStreamFromForeignAppliance(
                            req, resp, pvInfos, requestTimes, useChunkedEncoding,
                            retrievalURL + "/../data/getDataForPVs.raw", start, end);
                    listOfEventStreamFuturesLists.addAll(resultFromForeignAppliances);
                } catch (Exception ex) {
                    logger.error("Failed to retrieve " + StringUtils.join(pvNames, ", ") + " from " + retrievalURL
                            + ".");
                    return;
                }
            }
        }

        pmansProfiler.mark("After Appliance Info");

        // Setting post processor for PVs, taking into account whether there is a field in the PV name
        List<String> pvNamesFromRequests = new ArrayList<String>(pvNames.size());
        for (int i = 0; i < pvNames.size(); i++) {
            String pvName = pvNames.get(i);
            pvNamesFromRequests.add(pvName);
            PVTypeInfo typeInfo = typeInfos.get(i);
            postProcessorUserArg = postProcessorUserArgs.get(i);

            // If a field is specified in a PV name, it will create a post processor for that
            String fieldName = PVNames.getFieldName(pvName);
            if (fieldName != null && !fieldName.equals("") && !pvName.equals(typeInfo.getPvName())) {
                logger.debug("We reset the pvName " + pvName + " to one from the typeinfo " + typeInfo.getPvName()
                        + " as that determines the name of the stream. " + "Also using ExtraFieldsPostProcessor.");
                pvNames.set(i, typeInfo.getPvName());
                postProcessors.set(i, new ExtraFieldsPostProcessor(fieldName));
            }

            try {
                // Postprocessors get their mandatory arguments from the request.
                // If user does not pass in the expected request, throw an exception.
                postProcessors.get(i).initialize(postProcessorUserArg, pvName);
            } catch (Exception ex) {
                String msg = "Postprocessor threw an exception during initialization for " + pvName;
                logger.error(msg, ex);
                resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                resp.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
                return;
            }
        }

        /*
         * MergeDedupConsumer is what writes PB data in its respective format to the HTML response.
         * The response, after the MergeDedupConsumer is created, contains the following:
         * 
         * 1) The content type for the response.
         * 2) Any additional headers for the particular MIME response.
         * 
         * Additionally, the MergeDedupConsumer instance holds a reference to the output stream
         * that is used to write to the HTML response. It is stored under the name `os`.
         */
        MergeDedupConsumer mergeDedupCountingConsumer;
        try {
            mergeDedupCountingConsumer = createMergeDedupConsumer(resp, extension, useChunkedEncoding);
        } catch (ServletException se) {
            String msg = "Exception when retrieving data " + "-->" + se.toString();
            logger.error(msg, se);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, msg);
            return;
        }

        /* 
         * BasicContext contains the PV name and the expected return type. Used to access PB files.
         * RetrievalExecutorResult contains a thread service class and the time spans Presumably, the 
         * thread service is what retrieves the data, and the BasicContext is the context in which it 
         * works.
         */
        List<HashMap<String, String>> engineMetadatas = new ArrayList<HashMap<String, String>>();
        try {
            List<BasicContext> retrievalContexts = new ArrayList<BasicContext>(pvNames.size());
            List<RetrievalExecutorResult> executorResults = new ArrayList<RetrievalExecutorResult>(pvNames.size());
            for (int i = 0; i < pvNames.size(); i++) {
                if (fetchLatestMetadata) {
                    // Make a call to the engine to fetch the latest metadata.
                    engineMetadatas.add(fetchLatestMedataFromEngine(pvNames.get(i), applianceForPVs.get(i)));
                }
                retrievalContexts.add(new BasicContext(typeInfos.get(i).getDBRType(), pvNamesFromRequests.get(i)));
                executorResults.add(determineExecutorForPostProcessing(pvNames.get(i), typeInfos.get(i),
                        requestTimes, req, postProcessors.get(i)));
            }

            /*
             * There are as many Future objects in the eventStreamFutures List as there are periods over 
             * which to fetch data. Retrieval of data happen here in parallel.
             */
            List<LinkedList<Future<RetrievalResult>>> listOfRetrievalResultFuturesLists = new ArrayList<LinkedList<Future<RetrievalResult>>>();
            for (int i = 0; i < pvNames.size(); i++) {
                listOfRetrievalResultFuturesLists.add(resolveAllDataSources(pvNames.get(i), typeInfos.get(i),
                        postProcessors.get(i), applianceForPVs.get(i), retrievalContexts.get(i),
                        executorResults.get(i), req, resp));
            }
            pmansProfiler.mark("After data source resolution");

            for (int i = 0; i < pvNames.size(); i++) {
                // Data is retrieved here
                List<Future<EventStream>> eventStreamFutures = getEventStreamFuturesFromRetrievalResults(
                        executorResults.get(i), listOfRetrievalResultFuturesLists.get(i));
                listOfEventStreamFuturesLists.add(eventStreamFutures);
            }

        } catch (Exception ex) {
            if (ex != null && ex.toString() != null && ex.toString().contains("ClientAbortException")) {
                // We check for ClientAbortException etc this way to avoid including tomcat jars in the build path.
                logger.debug("Exception when retrieving data ", ex);
            } else {
                logger.error("Exception when retrieving data " + "-->" + ex.toString(), ex);
            }
        }

        long s1 = System.currentTimeMillis();
        String currentlyProcessingPV = null;

        /*
         * The following try bracket goes through each of the streams in the list of event stream futures.
         * 
         * It is intended that the process goes through one PV at a time.
         */
        try {
            for (int i = 0; i < pvNames.size(); i++) {
                List<Future<EventStream>> eventStreamFutures = listOfEventStreamFuturesLists.get(i);
                String pvName = pvNames.get(i);
                PVTypeInfo typeInfo = typeInfos.get(i);
                HashMap<String, String> engineMetadata = fetchLatestMetadata ? engineMetadatas.get(i) : null;
                PostProcessor postProcessor = postProcessors.get(i);

                logger.debug("Done with the RetrievalResults; moving onto the individual event stream "
                        + "from each source for " + StringUtils.join(pvNames, ", "));
                pmansProfiler.mark("After retrieval results");
                for (Future<EventStream> future : eventStreamFutures) {
                    EventStreamDesc sourceDesc = null;

                    // Gets the result of a data retrieval
                    try (EventStream eventStream = future.get()) {
                        sourceDesc = null; // Reset it for each loop iteration.
                        sourceDesc = eventStream.getDescription();
                        if (sourceDesc == null) {
                            logger.warn("Skipping event stream without a desc for pv " + pvName);
                            continue;
                        }

                        logger.debug("Processing event stream for pv " + pvName + " from source "
                                + ((eventStream.getDescription() != null) ? eventStream.getDescription().getSource()
                                        : " unknown"));

                        try {
                            mergeTypeInfo(typeInfo, sourceDesc, engineMetadata);
                        } catch (MismatchedDBRTypeException mex) {
                            logger.error(mex.getMessage(), mex);
                            continue;
                        }

                        if (currentlyProcessingPV == null || !currentlyProcessingPV.equals(pvName)) {
                            logger.debug("Switching to new PV " + pvName + " In some mime responses we insert "
                                    + "special headers at the beginning of the response. Calling the hook for "
                                    + "that");
                            currentlyProcessingPV = pvName;
                            /*
                             * Goes through the PB data stream over a period of time. The relevant MIME response
                             * actually deal with the processing of the PV. `start` and `end` refer to the very
                             * beginning and very end of the time period being retrieved over, regardless of
                             * whether it is divided up or not.
                             */
                            mergeDedupCountingConsumer.processingPV(currentlyProcessingPV, start, end,
                                    (eventStream != null) ? sourceDesc : null);
                        }

                        try {
                            // If the postProcessor does not have a consolidated event stream, we send each eventstream across as we encounter it.
                            // Else we send the consolidatedEventStream down below.
                            if (!(postProcessor instanceof PostProcessorWithConsolidatedEventStream)) {
                                /*
                                 * The eventStream object contains all the data over the current period.
                                 */
                                mergeDedupCountingConsumer.consumeEventStream(eventStream);
                                resp.flushBuffer();
                            }
                        } catch (Exception ex) {
                            if (ex != null && ex.toString() != null
                                    && ex.toString().contains("ClientAbortException")) {
                                // We check for ClientAbortException etc this way to avoid including tomcat jars in the build path.
                                logger.debug(
                                        "Exception when consuming and flushing data from " + sourceDesc.getSource(),
                                        ex);
                            } else {
                                logger.error("Exception when consuming and flushing data from "
                                        + sourceDesc.getSource() + "-->" + ex.toString(), ex);
                            }
                        }
                        pmansProfiler.mark("After event stream " + eventStream.getDescription().getSource());
                    } catch (Exception ex) {
                        if (ex != null && ex.toString() != null && ex.toString().contains("ClientAbortException")) {
                            // We check for ClientAbortException etc this way to avoid including tomcat jars in the build path.
                            logger.debug("Exception when consuming and flushing data from "
                                    + (sourceDesc != null ? sourceDesc.getSource() : "N/A"), ex);
                        } else {
                            logger.error("Exception when consuming and flushing data from "
                                    + (sourceDesc != null ? sourceDesc.getSource() : "N/A") + "-->" + ex.toString(),
                                    ex);
                        }
                    }
                }

                // TODO Go through data from other appliances here

                if (postProcessor instanceof PostProcessorWithConsolidatedEventStream) {
                    try (EventStream eventStream = ((PostProcessorWithConsolidatedEventStream) postProcessor)
                            .getConsolidatedEventStream()) {
                        EventStreamDesc sourceDesc = eventStream.getDescription();
                        if (sourceDesc == null) {
                            logger.error("Skipping event stream without a desc for pv " + pvName
                                    + " and post processor " + postProcessor.getExtension());
                        } else {
                            mergeDedupCountingConsumer.consumeEventStream(eventStream);
                            resp.flushBuffer();
                        }
                    }
                }

                // If the postProcessor needs to send final data across, give it a chance now...
                if (postProcessor instanceof AfterAllStreams) {
                    EventStream finalEventStream = ((AfterAllStreams) postProcessor).anyFinalData();
                    if (finalEventStream != null) {
                        mergeDedupCountingConsumer.consumeEventStream(finalEventStream);
                        resp.flushBuffer();
                    }
                }

                pmansProfiler.mark("After writing all eventstreams to response");
            }
        } catch (Exception ex) {
            if (ex != null && ex.toString() != null && ex.toString().contains("ClientAbortException")) {
                // We check for ClientAbortException etc this way to avoid including tomcat jars in the build path.
                logger.debug("Exception when retrieving data ", ex);
            } else {
                logger.error("Exception when retrieving data " + "-->" + ex.toString(), ex);
            }
        }

        long s2 = System.currentTimeMillis();
        logger.info("For the complete request, found a total of " + mergeDedupCountingConsumer.totalEventsForAllPVs
                + " in " + (s2 - s1) + "(ms)" + " skipping " + mergeDedupCountingConsumer.skippedEventsForAllPVs
                + " events" + " deduping involved " + mergeDedupCountingConsumer.comparedEventsForAllPVs
                + " compares.");

        pmansProfiler.mark("After all closes and flushing all buffers");

        // Till we determine all the if conditions where we log this, we log sparingly..
        if (pmansProfiler.totalTimeMS() > 5000) {
            logger.error("Retrieval time for " + StringUtils.join(pvNames, ", ") + " from " + startTimeStr + " to "
                    + endTimeStr + ": " + pmansProfiler.toString());
        }

        mergeDedupCountingConsumer.close();
    }

    /**
     * Given a list of retrievalResult futures, we loop thru these; execute them (basically calling the reader getData) and then sumbit the returned callables to the executorResult's executor.
     * We return a list of eventstream futures.
     * @param executorResult
     * @param retrievalResultFutures
     * @return
     * @throws InterruptedException
     * @throws ExecutionException
     */
    private List<Future<EventStream>> getEventStreamFuturesFromRetrievalResults(
            RetrievalExecutorResult executorResult, LinkedList<Future<RetrievalResult>> retrievalResultFutures)
            throws InterruptedException, ExecutionException {
        // List containing the result
        List<Future<EventStream>> eventStreamFutures = new LinkedList<Future<EventStream>>();

        // Loop thru the retrievalResultFutures one by one in sequence; get all the event streams from the plugins and consolidate them into a sequence of eventStream futures.
        for (Future<RetrievalResult> retrievalResultFuture : retrievalResultFutures) {
            // This call blocks until the future is complete.
            // For now, we use a simple get as opposed to a get with a timeout.
            RetrievalResult retrievalresult = retrievalResultFuture.get();
            if (retrievalresult.hasNoData()) {
                logger.debug("Skipping as we have not data from "
                        + retrievalresult.getRetrievalRequest().getDescription() + " for pv "
                        + retrievalresult.getRetrievalRequest().getPvName());
                continue;
            }

            // Process the data retrieval calls.
            List<Callable<EventStream>> callables = retrievalresult.getResultStreams();
            for (Callable<EventStream> wrappedCallable : callables) {
                Future<EventStream> submit = executorResult.executorService.submit(wrappedCallable);
                eventStreamFutures.add(submit);
            }
        }
        return eventStreamFutures;
    }

    /**
     * Resolve all data sources and submit them to the executor in the executorResult
     * This returns a list of futures of retrieval results.
     * @param pvName
     * @param typeInfo
     * @param postProcessor
     * @param applianceForPV
     * @param retrievalContext
     * @param executorResult
     * @param req
     * @param resp
     * @return
     * @throws IOException
     */
    private LinkedList<Future<RetrievalResult>> resolveAllDataSources(String pvName, PVTypeInfo typeInfo,
            PostProcessor postProcessor, ApplianceInfo applianceForPV, BasicContext retrievalContext,
            RetrievalExecutorResult executorResult, HttpServletRequest req, HttpServletResponse resp)
            throws IOException {

        LinkedList<Future<RetrievalResult>> retrievalResultFutures = new LinkedList<Future<RetrievalResult>>();

        /*
         * Gets the object responsible for resolving data sources (e.g., where data is stored
         * for this appliance.
         */
        DataSourceResolution datasourceresolver = new DataSourceResolution(configService);

        for (TimeSpan timespan : executorResult.requestTimespans) {
            // Resolve data sources for the given PV and the given time frames
            LinkedList<UnitOfRetrieval> unitsofretrieval = datasourceresolver.resolveDataSources(pvName,
                    timespan.getStartTime(), timespan.getEndTime(), typeInfo, retrievalContext, postProcessor, req,
                    resp, applianceForPV);
            // Submit the units of retrieval to the executor service. This will give us a bunch of Futures.
            for (UnitOfRetrieval unitofretrieval : unitsofretrieval) {
                // unitofretrieval implements a call() method as it extends Callable<?>
                retrievalResultFutures.add(executorResult.executorService.submit(unitofretrieval));
            }
        }
        return retrievalResultFutures;
    }

    /**
     * Create a merge dedup consumer that will merge/dedup multiple event streams.
     * This basically makes sure that we are serving up events in monotonically increasing timestamp order.
     * @param resp
     * @param extension
     * @param useChunkedEncoding
     * @return
     * @throws ServletException
     */
    private MergeDedupConsumer createMergeDedupConsumer(HttpServletResponse resp, String extension,
            boolean useChunkedEncoding) throws ServletException {
        MergeDedupConsumer mergeDedupCountingConsumer = null;
        MimeMappingInfo mimemappinginfo = mimeresponses.get(extension);
        if (mimemappinginfo == null) {
            StringWriter supportedextensions = new StringWriter();
            for (String supportedextension : mimeresponses.keySet()) {
                supportedextensions.append(supportedextension).append(" ");
            }
            throw new ServletException("Cannot generate response of mime-type " + extension
                    + ". Supported extensions are " + supportedextensions.toString());
        } else {
            try {
                String ctype = mimeresponses.get(extension).contentType;
                resp.setContentType(ctype);
                //            if(useChunkedEncoding) { 
                //               resp.addHeader("Transfer-Encoding", "chunked");
                //            }
                logger.info("Using " + mimemappinginfo.mimeresponseClass.getName()
                        + " as the mime response sending " + ctype);
                MimeResponse mimeresponse = (MimeResponse) mimemappinginfo.mimeresponseClass.newInstance();
                HashMap<String, String> extraHeaders = mimeresponse.getExtraHeaders();
                if (extraHeaders != null) {
                    for (Entry<String, String> kv : extraHeaders.entrySet()) {
                        resp.addHeader(kv.getKey(), kv.getValue());
                    }
                }
                OutputStream os = resp.getOutputStream();
                mergeDedupCountingConsumer = new MergeDedupConsumer(mimeresponse, os);
            } catch (Exception ex) {
                throw new ServletException(ex);
            }
        }
        return mergeDedupCountingConsumer;
    }

    /**
     * Check to see if the PV is served up by an external server. 
     * If it is, make a typeInfo up and set the appliance as this appliance.
     * We need the start time of the request as the ChannelArchiver does not serve up data if the starttime is much later than the last event in the dataset.
     * For external EPICS Archiver Appliances, we simply proxy the data right away. Use the response isCommited to see if we have already processed the request
     * @param pvName
     * @param start
     * @param req
     * @param resp
     * @param useChunkedEncoding
     * @return
     * @throws IOException
     */
    private PVTypeInfo checkIfPVisServedByExternalServer(String pvName, Timestamp start, HttpServletRequest req,
            HttpServletResponse resp, boolean useChunkedEncoding) throws IOException {
        PVTypeInfo typeInfo = null;
        // See if external EPICS archiver appliances have this PV.
        Map<String, String> externalServers = configService.getExternalArchiverDataServers();
        if (externalServers != null) {
            for (String serverUrl : externalServers.keySet()) {
                String index = externalServers.get(serverUrl);
                if (index.equals("pbraw")) {
                    logger.debug("Asking external EPICS Archiver Appliance " + serverUrl + " if it has data for pv "
                            + pvName);
                    JSONObject areWeArchivingPVObj = GetUrlContent.getURLContentAsJSONObject(
                            serverUrl + "/bpl/areWeArchivingPV?pv=" + URLEncoder.encode(pvName, "UTF-8"), false);
                    if (areWeArchivingPVObj != null) {
                        @SuppressWarnings("unchecked")
                        Map<String, String> areWeArchivingPV = (Map<String, String>) areWeArchivingPVObj;
                        if (areWeArchivingPV.containsKey("status")
                                && Boolean.parseBoolean(areWeArchivingPV.get("status"))) {
                            logger.info("Proxying data retrieval for pv " + pvName + " to " + serverUrl);
                            proxyRetrievalRequest(req, resp, pvName, useChunkedEncoding, serverUrl + "/data");
                        }
                        return null;
                    }
                }
            }
        }

        List<ChannelArchiverDataServerPVInfo> caServers = configService.getChannelArchiverDataServers(pvName);
        if (caServers != null && !caServers.isEmpty()) {
            try (BasicContext context = new BasicContext()) {
                for (ChannelArchiverDataServerPVInfo caServer : caServers) {
                    logger.debug(pvName + " is being server by " + caServer.toString()
                            + " and typeinfo is null. Trying to make a typeinfo up...");
                    List<Callable<EventStream>> callables = caServer.getServerInfo().getPlugin()
                            .getDataForPV(context, pvName, TimeUtils.minusHours(start, 1), start, null);
                    if (callables != null && !callables.isEmpty()) {
                        try (EventStream strm = callables.get(0).call()) {
                            if (strm != null) {
                                Event e = strm.iterator().next();
                                if (e != null) {
                                    ArchDBRTypes dbrType = strm.getDescription().getArchDBRType();
                                    typeInfo = new PVTypeInfo(pvName, dbrType, !dbrType.isWaveForm(),
                                            e.getSampleValue().getElementCount());
                                    typeInfo.setApplianceIdentity(configService.getMyApplianceInfo().getIdentity());
                                    // Somehow tell the code downstream that this is a fake typeInfo.
                                    typeInfo.setSamplingMethod(SamplingMethod.DONT_ARCHIVE);
                                    logger.debug("Done creating a temporary typeinfo for pv " + pvName);
                                    return typeInfo;
                                }
                            }
                        } catch (Exception ex) {
                            logger.error("Exception trying to determine typeinfo for pv " + pvName + " from CA "
                                    + caServer.toString(), ex);
                            typeInfo = null;
                        }
                    }
                }
            }
            logger.warn("Unable to determine typeinfo from CA for pv " + pvName);
            return typeInfo;
        }

        logger.debug("Cannot find the PV anywhere " + pvName);
        return null;
    }

    /**
     * Merges info from pvTypeTnfo that comes from the config database into the remote description that gets sent over the wire.
     * @param typeInfo
     * @param eventDesc
     * @param engineMetaData - Latest from the engine - could be null
     * @return
     * @throws IOException
     */
    private void mergeTypeInfo(PVTypeInfo typeInfo, EventStreamDesc eventDesc,
            HashMap<String, String> engineMetaData) throws IOException {
        if (typeInfo != null && eventDesc != null && eventDesc instanceof RemotableEventStreamDesc) {
            logger.debug("Merging typeinfo into remote desc for pv " + eventDesc.getPvName() + " into source "
                    + eventDesc.getSource());
            RemotableEventStreamDesc remoteDesc = (RemotableEventStreamDesc) eventDesc;
            remoteDesc.mergeFrom(typeInfo, engineMetaData);
        }
    }

    private static void processPingPV(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // resp.addHeader("Transfer-Encoding", "chunked");
        final OutputStream os = resp.getOutputStream();
        try {
            short currYear = TimeUtils.getCurrentYear();
            Builder builder = PayloadInfo.newBuilder().setPvname(ARCH_APPL_PING_PV)
                    .setType(ArchDBRTypes.DBR_SCALAR_DOUBLE.getPBPayloadType()).setYear(currYear);
            byte[] headerBytes = LineEscaper.escapeNewLines(builder.build().toByteArray());
            os.write(headerBytes);
            os.write(LineEscaper.NEWLINE_CHAR);

            for (int i = 0; i < 10; i++) {
                ByteArray val = new SimulationEvent(0, currYear, ArchDBRTypes.DBR_SCALAR_DOUBLE,
                        new ScalarValue<Double>(0.1 * i)).getRawForm();
                os.write(val.data, val.off, val.len);
                os.write(LineEscaper.NEWLINE_CHAR);
            }
        } finally {
            try {
                os.flush();
                os.close();
            } catch (Throwable t) {
            }
        }
    }

    @Override
    public void init() throws ServletException {
        this.configService = (ConfigService) this.getServletContext()
                .getAttribute(ConfigService.CONFIG_SERVICE_NAME);
    }

    /**
     * Based on the post processor, we make a call on where we can process the request in parallel
     * Either way, we return the result of this decision as two components
     * One is an executor to use
     * The other is a list of timespans that we have broken  the request into - the timespans will most likely be the time spans of the individual bins in the request.
     * @author mshankar
     *
     */
    private static class RetrievalExecutorResult implements AutoCloseable {
        ExecutorService executorService;
        LinkedList<TimeSpan> requestTimespans;

        RetrievalExecutorResult(ExecutorService executorService, LinkedList<TimeSpan> requestTimepans) {
            this.executorService = executorService;
            this.requestTimespans = requestTimepans;
        }

        @Override
        public void close() {
            try {
                this.executorService.shutdown();
            } catch (Throwable t) {
                logger.debug("Exception shutting down executor", t);
            }
        }
    }

    /**
     * Determine the thread pool to be used for post processing based on some characteristics of the request
     * The plugins will yield a list of callables that could potentially be evaluated in parallel 
     * Whether we evaluate in parallel is made here.
     * @param pvName
     * @param postProcessor
     * @return
     */
    private static RetrievalExecutorResult determineExecutorForPostProcessing(String pvName, PVTypeInfo typeInfo,
            LinkedList<TimeSpan> requestTimes, HttpServletRequest req, PostProcessor postProcessor) {
        long memoryConsumption = postProcessor.estimateMemoryConsumption(pvName, typeInfo,
                requestTimes.getFirst().getStartTime(), requestTimes.getLast().getEndTime(), req);
        double memoryConsumptionInMB = (double) memoryConsumption / (1024 * 1024);
        DecimalFormat twoSignificantDigits = new DecimalFormat("###,###,###,###,###,###.##");
        logger.debug("Memory consumption estimate from postprocessor for pv " + pvName + " is " + memoryConsumption
                + "(bytes) ~= " + twoSignificantDigits.format(memoryConsumptionInMB) + "(MB)");

        // For now, we only use the current thread to execute in serial.
        // Once we get the unit tests for the post processors in a more rigorous shape, we can start using the ForkJoinPool.
        // There are some complexities in using the ForkJoinPool - in this case, we need to convert to using synchronized versions of the SummaryStatistics and DescriptiveStatistics
        // We also still have the issue where we can add a sample twice because of the non-transactional nature of ETL.
        // However, there is a lot of work done by the PostProcessors in estimateMemoryConsumption so leave this call in place.
        return new RetrievalExecutorResult(new CurrentThreadExecutorService(), requestTimes);
    }

    /**
     * Make a call to the engine to fetch the latest metadata and then add it to the mergeConsumer
     * @param pvName
     * @param applianceForPV
     */
    @SuppressWarnings("unchecked")
    private HashMap<String, String> fetchLatestMedataFromEngine(String pvName, ApplianceInfo applianceForPV) {
        try {
            String metadataURL = applianceForPV.getEngineURL() + "/getMetadata?pv="
                    + URLEncoder.encode(pvName, "UTF-8");
            logger.debug("Getting metadata from the engine using " + metadataURL);
            JSONObject metadata = GetUrlContent.getURLContentAsJSONObject(metadataURL);
            return (HashMap<String, String>) metadata;
        } catch (Exception ex) {
            logger.warn("Exception fetching latest metadata for pv " + pvName, ex);
        }
        return null;
    }

    /**
     * If the pv is hosted on another appliance, proxy retrieval requests from that appliance
     * We expect to return immediately after this method. 
     * @param req
     * @param resp
     * @param pvName
     * @param useChunkedEncoding
     * @param dataRetrievalURLForPV
     * @throws IOException
     */
    private void proxyRetrievalRequest(HttpServletRequest req, HttpServletResponse resp, String pvName,
            boolean useChunkedEncoding, String dataRetrievalURLForPV) throws IOException {
        try {
            // TODO add some intelligent business logic to determine if redirect/proxy. 
            // It may be beneficial to support both and choose based on where the client in calling from or perhaps from a header?
            boolean redirect = false;
            if (redirect) {
                logger.debug("Data for pv " + pvName + "is elsewhere. Redirecting to appliance "
                        + dataRetrievalURLForPV);
                URI redirectURI = new URI(dataRetrievalURLForPV + "/" + req.getPathInfo());
                String redirectURIStr = redirectURI.normalize().toString() + "?" + req.getQueryString();
                logger.debug("URI for redirect is " + redirectURIStr);
                resp.sendRedirect(redirectURIStr);
                return;
            } else {
                logger.debug("Data for pv " + pvName + "is elsewhere. Proxying appliance " + dataRetrievalURLForPV);
                URI redirectURI = new URI(dataRetrievalURLForPV + "/" + req.getPathInfo());
                String redirectURIStr = redirectURI.normalize().toString() + "?" + req.getQueryString();
                logger.debug("URI for proxying is " + redirectURIStr);

                //            if(useChunkedEncoding) { 
                //               resp.addHeader("Transfer-Encoding", "chunked");
                //            }

                CloseableHttpClient httpclient = HttpClients.createDefault();
                HttpGet getMethod = new HttpGet(redirectURIStr);
                getMethod.addHeader("Connection", "close"); // https://www.nuxeo.com/blog/using-httpclient-properly-avoid-closewait-tcp-connections/
                try (CloseableHttpResponse response = httpclient.execute(getMethod)) {
                    if (response.getStatusLine().getStatusCode() == 200) {
                        HttpEntity entity = response.getEntity();
                        HashSet<String> proxiedHeaders = new HashSet<String>();
                        proxiedHeaders.addAll(Arrays.asList(MimeResponse.PROXIED_HEADERS));
                        Header[] headers = response.getAllHeaders();
                        for (Header header : headers) {
                            if (proxiedHeaders.contains(header.getName())) {
                                logger.debug("Adding headerName " + header.getName() + " and value "
                                        + header.getValue() + " when proxying request");
                                resp.addHeader(header.getName(), header.getValue());
                            }
                        }

                        if (entity != null) {
                            logger.debug("Obtained a HTTP entity of length " + entity.getContentLength());
                            try (OutputStream os = resp.getOutputStream();
                                    InputStream is = new BufferedInputStream(entity.getContent())) {
                                byte buf[] = new byte[10 * 1024];
                                int bytesRead = is.read(buf);
                                while (bytesRead > 0) {
                                    os.write(buf, 0, bytesRead);
                                    resp.flushBuffer();
                                    bytesRead = is.read(buf);
                                }
                            }
                        } else {
                            throw new IOException("HTTP response did not have an entity associated with it");
                        }
                    } else {
                        logger.error("Invalid status code " + response.getStatusLine().getStatusCode()
                                + " when connecting to URL " + redirectURIStr + ". Sending the errorstream across");
                        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
                            try (InputStream is = new BufferedInputStream(response.getEntity().getContent())) {
                                byte buf[] = new byte[10 * 1024];
                                int bytesRead = is.read(buf);
                                while (bytesRead > 0) {
                                    os.write(buf, 0, bytesRead);
                                    bytesRead = is.read(buf);
                                }
                            }
                            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                            resp.sendError(response.getStatusLine().getStatusCode(), new String(os.toByteArray()));
                        }
                    }
                }
            }
            return;
        } catch (URISyntaxException ex) {
            throw new IOException(ex);
        }
    }

    /**
     * If multiple pvs are hosted on another appliance, a retrieval request is made to that appliance and
     * the event stream is returned.
     * @param req
     * @param resp
     * @param requestTimes 
     * @param pvInfo
     * @param useChunkedEncoding
     * @param dataRetrievalURLForPV
     * @param start
     * @param end
     * @throws IOException
     * @throws ExecutionException 
     * @throws InterruptedException 
     */
    private List<List<Future<EventStream>>> retrieveEventStreamFromForeignAppliance(HttpServletRequest req,
            HttpServletResponse resp, ArrayList<PVInfoForClusterRetrieval> pvInfos,
            LinkedList<TimeSpan> requestTimes, boolean useChunkedEncoding, String dataRetrievalURLForPV,
            Timestamp start, Timestamp end) throws IOException, InterruptedException, ExecutionException {

        // Get the executors for the PVs in other clusters
        List<RetrievalExecutorResult> executorResults = new ArrayList<RetrievalExecutorResult>(pvInfos.size());
        for (int i = 0; i < pvInfos.size(); i++) {
            PVInfoForClusterRetrieval pvInfo = pvInfos.get(i);
            executorResults.add(determineExecutorForPostProcessing(pvInfo.getPVName(), pvInfo.getTypeInfo(),
                    requestTimes, req, pvInfo.getPostProcessor()));
        }

        // Get list of lists of futures of retrieval results. Basically, this is setting up the data sources for retrieval.
        List<LinkedList<Future<RetrievalResult>>> listOfRetrievalResultsFutures = new ArrayList<LinkedList<Future<RetrievalResult>>>();
        for (int i = 0; i < pvInfos.size(); i++) {
            PVInfoForClusterRetrieval pvInfo = pvInfos.get(i);
            listOfRetrievalResultsFutures
                    .add(resolveAllDataSources(pvInfo.getPVName(), pvInfo.getTypeInfo(), pvInfo.getPostProcessor(),
                            pvInfo.getApplianceInfo(), new BasicContext(), executorResults.get(i), req, resp));
        }

        // Now the data is being retrieved, producing a list of lists of futures of event streams.
        List<List<Future<EventStream>>> listOfEventStreamFutures = new ArrayList<List<Future<EventStream>>>();
        for (int i = 0; i < pvInfos.size(); i++) {
            listOfEventStreamFutures.add(getEventStreamFuturesFromRetrievalResults(executorResults.get(i),
                    listOfRetrievalResultsFutures.get(i)));
        }

        return listOfEventStreamFutures;
    }

    /**
     * Parse the timeranges parameter and generate a list of TimeSpans.
     * @param resp
     * @param pvName
     * @param requestTimes - list of timespans that we add the valid times to.
     * @param timeRangesStr
     * @return
     * @throws IOException
     */
    private boolean parseTimeRanges(HttpServletResponse resp, String pvName, LinkedList<TimeSpan> requestTimes,
            String timeRangesStr) throws IOException {
        String[] timeRangesStrList = timeRangesStr.split(",");
        if (timeRangesStrList.length % 2 != 0) {
            String msg = "Need to specify an even number of times in timeranges for pv " + pvName + ". We have "
                    + timeRangesStrList.length + " times";
            logger.error(msg);
            resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            return false;
        }

        LinkedList<Timestamp> timeRangesList = new LinkedList<Timestamp>();
        for (String timeRangesStrItem : timeRangesStrList) {
            try {
                Timestamp ts = TimeUtils.convertFromISO8601String(timeRangesStrItem);
                timeRangesList.add(ts);
            } catch (IllegalArgumentException ex) {
                try {
                    Timestamp ts = TimeUtils.convertFromDateTimeStringWithOffset(timeRangesStrItem);
                    timeRangesList.add(ts);
                } catch (IllegalArgumentException ex2) {
                    String msg = "Cannot parse time " + timeRangesStrItem;
                    logger.warn(msg, ex2);
                    resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                    return false;
                }
            }
        }

        assert (timeRangesList.size() % 2 == 0);
        Timestamp prevEnd = null;
        while (!timeRangesList.isEmpty()) {
            Timestamp t0 = timeRangesList.pop();
            Timestamp t1 = timeRangesList.pop();

            if (t1.before(t0)) {
                String msg = "For request, end " + t1.toString() + " is before start " + t0.toString() + " for pv "
                        + pvName;
                logger.error(msg);
                resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                return false;
            }

            if (prevEnd != null) {
                if (t0.before(prevEnd)) {
                    String msg = "For request, start time " + t0.toString() + " is before previous end time "
                            + prevEnd.toString() + " for pv " + pvName;
                    logger.error(msg);
                    resp.addHeader(MimeResponse.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                    return false;
                }
            }
            prevEnd = t1;
            requestTimes.add(new TimeSpan(t0, t1));
        }
        return true;
    }

    /**
     * Used when we are constructing a TypeInfo from a template. We want to look at the actual data and see if we can set the DBR type correctly.
     * Return true if we are able to do this.
     * @param typeInfo
     * @return
     * @throws IOException
     */
    private boolean setActualDBRTypeFromData(String pvName, PVTypeInfo typeInfo, ConfigService configService)
            throws IOException {
        String[] dataStores = typeInfo.getDataStores();
        for (String dataStore : dataStores) {
            StoragePlugin plugin = StoragePluginURLParser.parseStoragePlugin(dataStore, configService);
            if (plugin instanceof ETLDest) {
                ETLDest etlDest = (ETLDest) plugin;
                try (BasicContext context = new BasicContext()) {
                    Event e = etlDest.getLastKnownEvent(context, pvName);
                    if (e != null) {
                        typeInfo.setDBRType(e.getDBRType());
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /** 
     * <p> 
     * This class should be used to store the PV name and type info of data that will be 
     * retrieved from neighbouring nodes in a cluster, to be returned in a response from 
     * the source cluster. 
     * </p> 
     * <p> 
     * PVTypeInfo maintains a PV name field, too. At first the PV name field in this object 
     * seems superfluous. But the field is necessary, as it contains the unprocessed PV 
     * name as opposed to the PV name stored by the PVTypeInfo object, which has been 
     * processed. 
     * </p> 
     *  
     * @author Michael Kenning 
     * 
     */
    private class PVInfoForClusterRetrieval {

        private String pvName;
        private PVTypeInfo typeInfo;
        private PostProcessor postProcessor;
        private ApplianceInfo applianceInfo;

        private PVInfoForClusterRetrieval(String pvName, PVTypeInfo typeInfo, PostProcessor postProcessor,
                ApplianceInfo applianceInfo) {
            this.pvName = pvName;
            this.typeInfo = typeInfo;
            this.postProcessor = postProcessor;
            this.applianceInfo = applianceInfo;

            assert (this.pvName != null);
            assert (this.typeInfo != null);
            assert (this.postProcessor != null);
            assert (this.applianceInfo != null);
        }

        public String getPVName() {
            return pvName;
        }

        public PVTypeInfo getTypeInfo() {
            return typeInfo;
        }

        public PostProcessor getPostProcessor() {
            return postProcessor;
        }

        public ApplianceInfo getApplianceInfo() {
            return applianceInfo;
        }

    }
}