/*******************************************************************************
* 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 edu.stanford.slac.archiverappliance.PBOverHTTP;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
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.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.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;
import edu.stanford.slac.archiverappliance.PB.EPICSEvent;
/**
* A read-only storage plugin that gets data using the PB/http protocol from a server.
* @author mshankar
*
*/
public class PBOverHTTPStoragePlugin implements StoragePlugin {
private static Logger logger = Logger.getLogger(PBOverHTTPStoragePlugin.class.getName());
private String accessURL = null;
private String desc = "A event stream backed by a .raw response from a remote server.";
private String name;
private boolean skipExternalServers = false;
@Override
public List<Callable<EventStream>> getDataForPV(BasicContext context, String pvName, Timestamp startTime,
Timestamp endTime, PostProcessor postProcessor) throws IOException {
String getURL = accessURL + "?pv=" + pvName
+ "&from=" + TimeUtils.convertToISO8601String(startTime)
+ "&to=" + TimeUtils.convertToISO8601String(endTime)
+ (postProcessor != null ? "&pp="+postProcessor.getExtension() : "")
+ (skipExternalServers ? "skipExternalServers=true" : "");
logger.info("URL to fetch data is " + getURL);
return getDataBehindURL(getURL, startTime, postProcessor);
}
public List<Callable<EventStream>> getDataForMultiPVs(BasicContext context, List<String> pvNames, Timestamp startTime,
Timestamp endTime, PostProcessor postProcessor) throws IOException {
String getURL = accessURL;
for (int i = 0; i < pvNames.size(); i++)
if (i == 0) getURL += "?pv=" + pvNames.get(i);
else getURL += "&pv=" + pvNames.get(i);
getURL += "&from=" + TimeUtils.convertToISO8601String(startTime)
+ "&to=" + TimeUtils.convertToISO8601String(endTime)
+ (postProcessor != null ? "&pp="+postProcessor.getExtension() : "")
+ (skipExternalServers ? "skipExternalServers=true" : "");
logger.info("URL to fetch data is " + getURL);
return getDataBehindURL(getURL, startTime, postProcessor);
}
private List<Callable<EventStream>> getDataBehindURL(String getURL, Timestamp startTime, PostProcessor postProcessor) {
try {
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet getMethod = new HttpGet(getURL);
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();
if (entity != null) {
logger.debug("Obtained a HTTP entity of length " + entity.getContentLength());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
entity.writeTo(bos);
bos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
InputStreamBackedEventStream isStream = new InputStreamBackedEventStream(bis, startTime);
if(isStream.getDescription() != null) {
isStream.getDescription().setSource(this.getName());
} else {
logger.warn("No desc attached to input stream for url " + getURL);
}
return CallableEventStream.makeOneStreamCallableList(isStream, postProcessor, true);
} else {
logger.debug("Obtained empty HTTP entity from " + getURL);
}
} else {
logger.warn("Invalid status code " + response.getStatusLine().getStatusCode() + " when connecting to URL " + getURL);
HttpEntity entity = response.getEntity();
if (entity != null) {
ByteArrayOutputStream sbuf = new ByteArrayOutputStream();
entity.writeTo(sbuf);
logger.warn(sbuf.toString("UTF-8"));
}
}
}
} catch (FileNotFoundException fex) {
logger.debug("No data from remote site " + getURL);
return null;
} catch(Throwable t) {
logger.warn("Exception fetching data from URL " + getURL, t);
}
return null;
}
@Override
public boolean appendData(BasicContext context, String pvName, EventStream stream) {
throw new RuntimeException("Append Data is not available for HTTP streams");
}
@Override
public String getDescription() {
return desc;
}
@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("rawURL")) {
this.setAccessURL(queryNVPairs.get("rawURL"));
} else {
throw new IOException("Cannot initialize the pbraw plugin; this needs the URL to the engine/Raw over HTTP to be specified " + configURL);
}
if(queryNVPairs.containsKey("name")) {
name = queryNVPairs.get("name");
} else {
name = new URL(this.getAccessURL()).getHost();
logger.debug("Using the default name of " + name + " for this plain pb engine");
}
if(queryNVPairs.containsKey("skipExternalServers")) {
logger.debug("Telling the remote server to skip all data from external (potentially ChannelArchiver) servers");
this.skipExternalServers = Boolean.parseBoolean(queryNVPairs.get("skipExternalServers"));
}
} catch(URISyntaxException ex) {
throw new IOException(ex);
}
}
public String getAccessURL() {
return accessURL;
}
public void setAccessURL(String aURL) {
this.accessURL = aURL;
this.setDesc("PB over HTTP from URL " + aURL);
loadPBclasses();
}
private static void loadPBclasses() {
try {
EPICSEvent.ScalarDouble.newBuilder()
.setSecondsintoyear(0)
.setNano(0)
.setVal(0)
.setSeverity(0)
.setStatus(0)
.build().toByteArray();
} catch(Exception ex) {
logger.error(ex.getMessage(), ex);
}
}
public void setDesc(String newDesc) {
this.desc = newDesc;
}
@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.
}
}