/******************************************************************************* * 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.channelarchiver; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import java.util.concurrent.Callable; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.log4j.Logger; import org.epics.archiverappliance.Event; import org.epics.archiverappliance.EventStream; import org.epics.archiverappliance.StoragePlugin; import org.epics.archiverappliance.common.BasicContext; import org.epics.archiverappliance.common.TimeUtils; import org.epics.archiverappliance.config.ConfigService; import org.epics.archiverappliance.etl.ConversionFunction; import org.epics.archiverappliance.retrieval.CallableEventStream; import org.epics.archiverappliance.retrieval.postprocessors.PostProcessor; import org.epics.archiverappliance.utils.ui.URIUtils; /** * A storage plugin that can front a Channel Archiver Data Server. * Only reads are supported. * This has the ability to support reduced data sets like LCLS_SPARSE but this is optional. * If a sparse key is not specified, we default to using the un-reduced archive key. * Integration test plan for this. Try to include * <ol> * <li>A PV that is archived in both the appliance and ChannelArchiver. Date ranges should be appliance only, overlap and ChannelArchiver only.</li> * <li>A PV that is archived in only the ChannelArchiver.</li> * </ol> * @author mshankar * */ public class ChannelArchiverReadOnlyPlugin implements StoragePlugin { private static Logger logger = Logger.getLogger(ChannelArchiverReadOnlyPlugin.class.getName()); private String serverURL; private int archiveKey; private int reducedArchiveKey = -1; private String description; private String name; private int valuesRequested = Integer.MAX_VALUE; // private String howStr = "0"; public ChannelArchiverReadOnlyPlugin() { } public ChannelArchiverReadOnlyPlugin(String serverURL, String index) { this.serverURL = serverURL; this.archiveKey = Integer.parseInt(index); this.setDescription("ChannelArchiverReadOnlyPlugin plugin with serverURL " + serverURL + " and archiveKey " + archiveKey + ((reducedArchiveKey != -1) ? (" and a reducedArchiveKey of " + reducedArchiveKey) : (" and no reducedArchiveKey"))); } public ChannelArchiverReadOnlyPlugin(String serverURL, String index, int valuesRequested, String howStr) { this.serverURL = serverURL; this.archiveKey = Integer.parseInt(index); this.valuesRequested = valuesRequested; // this.howStr = howStr; this.setDescription("ChannelArchiverReadOnlyPlugin plugin with serverURL " + serverURL + " and archiveKey " + archiveKey + ((reducedArchiveKey != -1) ? (" and a reducedArchiveKey of " + reducedArchiveKey) : (" and no reducedArchiveKey"))); } @Override public List<Callable<EventStream>> getDataForPV(BasicContext context, String pvName, Timestamp startTime, Timestamp endTime, PostProcessor postProcessor) throws IOException { if(reducedArchiveKey != -1) { return getDataForPV(context, pvName, startTime, endTime, reducedArchiveKey, postProcessor); } else { return getDataForPV(context, pvName, startTime, endTime, archiveKey, postProcessor); } } private List<Callable<EventStream>> getDataForPV(BasicContext context, String pvName, Timestamp startTime, Timestamp endTime, int archiveKey, PostProcessor postProcessor) throws IOException { try { // TODO the only thing that seems to get similar charts in ArchiveViewer for production data is using plot-binning. // This is hardcoded somewhere in the Data server or the ArchiveViewer code.... // Need to figure out where it and and how to address it. String howStr = "3"; String pvNameForCall = pvName; if(context.getPvNameFromRequest() != null) { logger.info("Using pvName from request " + context.getPvNameFromRequest() + " when making a call to the ChannelArchiver for pv " + pvName); pvNameForCall = context.getPvNameFromRequest(); } String archiveValuesStr = new String("<?xml version=\"1.0\"?>\n" + "<methodCall>\n" + "<methodName>archiver.values</methodName>\n" + "<params>\n" + "<param><value><i4>" + archiveKey + "</i4></value></param>\n" + "<param><value><array><data><value><string>" + pvNameForCall + "</string></value></data></array></value></param>\n" + "<param><value><i4>" + TimeUtils.convertToEpochSeconds(startTime)+ "</i4></value></param>\n" + "<param><value><i4>" + startTime.getNanos() + "</i4></value></param>\n" + "<param><value><i4>" + TimeUtils.convertToEpochSeconds(endTime) + "</i4></value></param>\n" + "<param><value><i4>" + endTime.getNanos() + "</i4></value></param>\n" + "<param><value><i4>" + valuesRequested + "</i4></value></param>\n" + "<param><value><i4>" + howStr + "</i4></value></param>\n" + "</params>\n" + "</methodCall>\n"); URI serverURI = new URI(serverURL); if(serverURI.getScheme().equals("file")) { logger.info("Using a file provider for Channel Archiver data - this better be a unit test."); // We use the file scheme for unit testing... Yeah, the extensions are hardcoded... InputStream is = new BufferedInputStream(new FileInputStream(new File(serverURI.getPath() + File.separator + pvName + ".xml"))); // ArchiverValuesHandler takes over the burden of closing the input stream. ArchiverValuesHandler handler = new ArchiverValuesHandler(pvName, is, serverURL.toString() + "\n" + archiveValuesStr, context.getRetrievalExpectedDBRType()); if(postProcessor != null) { return CallableEventStream.makeOneStreamCallableList(handler, postProcessor, true); } else { return CallableEventStream.makeOneStreamCallableList(handler); } } else { StringEntity archiverValues = new StringEntity(archiveValuesStr, ContentType.APPLICATION_XML); if(logger.isDebugEnabled()) { logger.debug(getDescription() + " making call to channel archiver with " + archiveValuesStr); } CloseableHttpClient httpclient = HttpClients.createDefault(); HttpPost postMethod = new HttpPost(serverURL); postMethod.addHeader("Content-Type", "text/xml"); postMethod.setEntity(archiverValues); if(logger.isDebugEnabled()) { logger.debug("About to make a POST with " + archiveValuesStr); } HttpResponse response = httpclient.execute(postMethod); int statusCode = response.getStatusLine().getStatusCode(); if(statusCode >= 200 && statusCode <= 206) { HttpEntity entity = response.getEntity(); if (entity != null) { logger.debug("Obtained a HTTP entity of length " + entity.getContentLength()); // ArchiverValuesHandler takes over the burden of closing the input stream. InputStream is = entity.getContent(); ArchiverValuesHandler handler = new ArchiverValuesHandler(pvName, is, serverURL.toString() + "\n" + archiveValuesStr, context.getRetrievalExpectedDBRType()); if(postProcessor != null) { return CallableEventStream.makeOneStreamCallableList(handler, postProcessor, true); } else { return CallableEventStream.makeOneStreamCallableList(handler); } } else { throw new IOException("HTTP response did not have an entity associated with it"); } } else { logger.error("Got an invalid status code " + statusCode + " from the server " + serverURL + " for PV " + pvName + " so returning null"); return null; } } } catch(UnsupportedEncodingException ex) { throw new IOException("Exception making call to Channel Archiver", ex); } catch (URISyntaxException e) { throw new IOException("Invalid URL " + serverURL, e); } } @Override public boolean appendData(BasicContext context, String pvName, EventStream stream) throws IOException { throw new IOException("The ChannelArchiverReadOnlyPlugin does not support the Writer interface"); } @Override public String getDescription() { return description; } @Override public void initialize(String configURL, ConfigService configService) throws IOException { try { URI srcURI = new URI(configURL); HashMap<String, String> queryNVPairs = URIUtils.parseQueryString(srcURI); if(queryNVPairs.containsKey("serverURL")) { this.setServerURL(queryNVPairs.get("serverURL")); } else { throw new IOException("Cannot initialize the plugin; this needs the serverURL to be specified"); } if(queryNVPairs.containsKey("archiveKey")) { this.setArchiveKey(Integer.parseInt(queryNVPairs.get("archiveKey"))); } else { throw new IOException("Cannot initialize the plugin; this needs the archiver key to be specified"); } if(queryNVPairs.containsKey("reducedArchiveKey")) { this.setReducedArchiveKey(Integer.parseInt(queryNVPairs.get("reducedArchiveKey"))); } if(queryNVPairs.containsKey("name")) { name = queryNVPairs.get("name"); } else { name = new URL(this.getServerURL()).getHost(); logger.debug("Using the default name of " + name + " for this channel archiver engine"); } this.setDescription("ChannelArchiverReadOnlyPlugin plugin with serverURL " + serverURL + " and archiveKey " + archiveKey + ((reducedArchiveKey != -1) ? (" and a reducedArchiveKey of " + reducedArchiveKey) : (" and no reducedArchiveKey"))); } catch(URISyntaxException ex) { throw new IOException(ex); } } public String getServerURL() { return serverURL; } public void setServerURL(String serverURL) { this.serverURL = serverURL; } public int getArchiveKey() { return archiveKey; } public void setArchiveKey(int archiveKey) { this.archiveKey = archiveKey; } public void setDescription(String description) { this.description = description; } public int getReducedArchiveKey() { return reducedArchiveKey; } public void setReducedArchiveKey(int reducedArchiveKey) { this.reducedArchiveKey = reducedArchiveKey; } @Override public Event getLastKnownEvent(BasicContext context, String pvName) throws IOException { throw new UnsupportedOperationException(); } @Override public Event getFirstKnownEvent(BasicContext context, String pvName) throws IOException { throw new UnsupportedOperationException(); } @Override public String getName() { return name; } @Override public void renamePV(BasicContext context, String oldName, String newName) throws IOException { // Nothing to do here. } @Override public void convert(BasicContext context, String pvName, ConversionFunction conversionFuntion) throws IOException { // Nothing to do here. } }