/*-
*******************************************************************************
* Copyright (c) 2011, 2016 Diamond Light Source Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthew Gerring - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.dawnsci.remotedataset.client;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.january.DatasetException;
import org.eclipse.january.dataset.DataEvent;
import org.eclipse.january.dataset.Dataset;
import org.eclipse.january.dataset.IDataListener;
import org.eclipse.january.dataset.IDatasetConnector;
import org.eclipse.january.dataset.IDynamicDataset;
import org.eclipse.january.dataset.LazyWriteableDataset;
import org.eclipse.january.dataset.ShapeUtils;
import org.eclipse.january.metadata.DynamicConnectionInfo;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class manages a remote connection to data and
* allows datasets to work seamlessly remotely as if
* they were ILazyDatasets.
*
* To do this they require to know the data server's name and port.
* They also need a path on the server for the file which they are linked to.
*
* The DataServer is a standard HTTP with web sockets for events. This makes
* RemoteDataset able to pass over HTTP with port 80 open for external data
* viewing too.
*
*
<usage><code>
final IRemoteDatasetService service = ...
final IRemoteDataset data = service.createRemoteDataset("localhost", 8080);<br>
data.setPath(h5File.getAbsolutePath());
data.setDatasetName("image"); // We just get the first image in the PNG file.
data.connect();
try {
// Use it the same way as ILazyDataset
} finally {
data.disconnect();
}
</code></usage>
*
* @author Matthew Gerring
*
*/
class RemoteDataset extends LazyWriteableDataset implements IDatasetConnector {
private static final Logger logger = LoggerFactory.getLogger(RemoteDataset.class);
private final URLBuilder urlBuilder;
// Web socket stuff
private Session connection;
private boolean dynamicShape = true;
private int[] transShape;
private Executor exec;
private WebSocketClient client;
/**
*
*/
private static final long serialVersionUID = -9031675045219778735L;
/**
*
<usage><code>
final IRemoteDatasetService service = ...
final IRemoteDataset data = service.createRemoteDataset("localhost", 8080);<br>
data.setPath(h5File.getAbsolutePath());<br>
data.setDataset("image"); // We just get the first image in the PNG file.<br>
data.connect();<br>
<br>
try {<br>
// Use it the same way as ILazyDataset<br>
} finally {<br>
data.disconnect();<br>
}<br>
</code></usage>
* @param serverName
* @param port
*/
public RemoteDataset(String serverName, int port, Executor exec) {
super("unknown", Dataset.INT, new int[]{1}, new int[]{IDynamicDataset.UNLIMITED}, null, null);
this.urlBuilder = new URLBuilder(serverName, port);
urlBuilder.setWritingExpected(true);
this.exec = exec;
this.loader = new RemoteLoader(urlBuilder);
}
@Override
public String connect() throws DatasetException {
return connect(500, TimeUnit.MILLISECONDS);
}
/**
* Call to read the dataset, set current shape and create event connnection for
* IDynamicDataset part of the dataset
*/
@Override
public String connect(long time, TimeUnit unit) throws DatasetException {
try {
createInfo();
if (eventDelegate.hasDataListeners()) {
createFileListener();
}
} catch (Exception e) {
throw new DatasetException(e);
}
// TODO Does this cause a memory leak?
// If multiple connect/disconnect are called will this break things?
addMetadata(new DynamicConnectionInfo() {
private static final long serialVersionUID = 6220818379127865903L;
public boolean isConnected() {
return connection.isOpen();
}
});
return null;
}
public void disconnect() throws DatasetException {
eventDelegate.clear();
try {
if (connection != null && connection.isOpen()) {
connection.getRemote().sendString("Disconnected from " + urlBuilder.getPath());
connection.close();
}
if (client != null && client.isStarted()) {
client.stop();
}
} catch (Exception e) {
throw new DatasetException(e);
}
}
@Override
public boolean refreshShape() {
try {
createInfo();
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
private void createFileListener() throws Exception {
URI uri = URI.create(urlBuilder.getEventURL());
this.client = new WebSocketClient(exec);
client.start();
final DataEventSocket clientSocket = new DataEventSocket();
// Attempt Connect
Future<Session> fut = client.connect(clientSocket, uri);
// Wait for Connect
connection = fut.get();
// Send a message
connection.getRemote().sendString("Connected to "+urlBuilder.getPath());
}
public class DataEventSocket extends WebSocketAdapter {
@Override
public void onWebSocketText(String data) {
super.onWebSocketText(data);
try {
DataEvent evt = DataEvent.decode(data);
if (evt.getShape()!=null) {
if (dynamicShape) {
resize(evt.getShape());
setMax(evt.getShape());
eventDelegate.fire(evt);
} else {
RemoteDataset.this.transShape = evt.getShape();
}
}
} catch (Exception ne) {
logger.error("Cannot set shape of dataset!", ne);
}
}
}
public void setShapeDynamic(boolean isDyn) {
dynamicShape = isDyn;
if (dynamicShape && transShape!=null) {
this.shape = transShape;
setMax(shape);
transShape = null;
}
}
private void setMax(int[] shape) {
int[] max = new int[shape.length];
for (int i = 0; i < max.length; i++) max[i] = -1;
setMaxShape(max);
}
private void createInfo() throws Exception {
List<String> info = getInfo();
if (this.name == null) this.name = info.get(0);
this.shape = toIntArray(info.get(1));
setMax(shape);
this.oShape = shape;
this.dtype = Integer.parseInt(info.get(2));
this.isize = Integer.parseInt(info.get(3));
try {
size = ShapeUtils.calcLongSize(shape);
} catch (IllegalArgumentException e) {
size = Long.MAX_VALUE; // this indicates that the entire dataset cannot be read in!
}
if (info.size() > 4) {
setMaxShape(toIntArray(info.get(4)));
setChunking(toIntArray(info.get(5)));
}
}
private int[] toIntArray(String array) {
// array is null, or of the form [1,2,3,4]
if (array.equals("null"))
return null;
if (array.length() <= 2) {
return new int[0]; // special case of scalar dataset
}
final String[] split = array.substring(1, array.length()-1).split(",");
final int[] ret = new int[split.length];
for (int i = 0; i < split.length; i++) {
ret[i] = Integer.parseInt(split[i].trim());
}
return ret;
}
private List<String> getInfo() throws Exception {
final List<String> ret = new ArrayList<String>();
final URL url = new URL(urlBuilder.getInfoURL());
URLConnection conn = url.openConnection();
final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
try {
String line = null;
while((line = reader.readLine())!=null) ret.add(line);
} finally {
reader.close();
}
return ret;
}
@Override
public void addDataListener(IDataListener l) {
// If we are not already web socket client and connect has been called, create the listener.
try {
if (this.connection==null && loader!=null) createFileListener();
} catch (Exception ne) {
throw new IllegalArgumentException(ne);
}
eventDelegate.addDataListener(l);
}
public String getPath() {
return urlBuilder.getPath();
}
public void setPath(String path) {
urlBuilder.setPath(path);
}
public String getDatasetName() {
return urlBuilder.getDataset();
}
public void setDatasetName(String dataset) {
urlBuilder.setDataset(dataset);
}
public boolean isWritingExpected() {
return urlBuilder.isWritingExpected();
}
public void setWritingExpected(boolean writingExpected) {
urlBuilder.setWritingExpected(writingExpected);
}
@Override
public IDynamicDataset getDataset() {
return this;
}
@Override
public RemoteDataset clone() {
RemoteDataset ret = new RemoteDataset(urlBuilder.getServerName(),urlBuilder.getPort(),this.exec);
ret.urlBuilder.setDataset(this.urlBuilder.getDataset());
ret.urlBuilder.setPath(this.urlBuilder.getPath());
ret.client = this.client;
ret.connection = this.connection;
ret.loader = this.loader;
ret.shape = shape;
ret.size = size;
ret.maxShape = maxShape;
ret.oShape = oShape;
ret.prepShape = prepShape;
ret.postShape = postShape;
ret.begSlice = begSlice;
ret.delSlice = delSlice;
ret.map = map;
ret.base = base;
ret.metadata = copyMetadata();
ret.oMetadata = oMetadata;
ret.eventDelegate = eventDelegate;
ret.name = this.name;
return ret;
}
}