/*
* This file is part of INDI for Java Client.
*
* INDI for Java Client is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* INDI for Java Client is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with INDI for Java Client. If not, see
* <http://www.gnu.org/licenses/>.
*/
package laazotea.indi.client;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.*;
import laazotea.indi.INDIDateFormat;
import laazotea.indi.INDIProtocolParser;
import laazotea.indi.INDIProtocolReader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* A class representing a INDI Server Connection. Usually this is the entry
* point for any INDI Client to connect to the server, retrieve all devices and
* properties and so on.
*
* @author S. Alonso (Zerjillo) [zerjioi at ugr.es]
* @version 1.32, January 27, 2013
*/
public class INDIServerConnection implements INDIProtocolParser {
/**
* The name of the Connection.
*/
private String name;
/**
* The host of the Connection.
*/
private String host;
/**
* The port of the Connection.
*/
private int port;
/**
* The sockect used in the Connection.
*/
private Socket socket = null;
/**
* The output writer of the Connection.
*/
private PrintWriter out = null;
/**
* A reader to read from the Connection.
*/
INDIProtocolReader reader;
/**
* The set of the devices associated to this Connection.
*/
private LinkedHashMap<String, INDIDevice> devices;
/**
* The list of Listeners of this Connection.
*/
private ArrayList<INDIServerConnectionListener> listeners;
/**
* Constructs an instance of
* <code>INDIServerConnection</code>. The Connection is NOT stablished.
*
* @param name The name of the Connection.
* @param host The host of the Connection.
* @param port The port of the Connection.
*/
public INDIServerConnection(String name, String host, int port) {
init(name, host, port);
}
/**
* Constructs an instance of
* <code>INDIServerConnection</code> with no name. The Connection is NOT
* stablished.
*
* @param host The host of the Connection.
* @param port The port of the Connection.
*/
public INDIServerConnection(String host, int port) {
init("", host, port);
}
/**
* Constructs an instance of
* <code>INDIServerConnection</code> with no name and using the default port
* (7624). The Connection is NOT stablished.
*
* @param host The host of the Connection.
*/
public INDIServerConnection(String host) {
init("", host, 7624);
}
/**
* Initilizes the Connection
*
* @param name The name of the Connection.
* @param host The host of the Connection.
* @param port The port of the Connection.
*/
private void init(String name, String host, int port) {
this.name = name;
this.host = host;
this.port = port;
this.socket = null;
this.out = null;
this.reader = null;
devices = new LinkedHashMap<String, INDIDevice>();
listeners = new ArrayList<INDIServerConnectionListener>();
}
/**
* This function waits until a Device with a
* <code>name</code> exists in this Connection and returns it. The wait is
* dinamic, so it should be called from a different Thread or the app will
* freeze until the Device exists.
*
* @param name The name of the evice to wait for.
* @return The Device once it exists in this Connection.
*/
public INDIDevice waitForDevice(String name) {
return waitForDevice(name, Integer.MAX_VALUE);
}
/**
* This function waits until a Device with a
* <code>name</code> exists in this Connection and returns it. The wait is
* dinamic, so it should be called from a different Thread or the app will
* freeze until the Device exists or the
* <code>maxWait</code> number of seconds have elapsed.
*
* @param name The name of the evice to wait for.
* @param maxWait Maximum number of seconds to wait for the Device
* @return The Device once it exists in this Connection or
* <code>null</code> if the maximum wait is achieved.
*/
public INDIDevice waitForDevice(String name, int maxWait) {
INDIDevice d = null;
long startTime = (new Date()).getTime();
boolean timeElapsed = false;
while ((d == null) && (!timeElapsed)) {
d = this.getDevice(name);
if (d == null) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
long endTime = (new Date()).getTime();
if (((endTime - startTime) / 1000) > maxWait) {
timeElapsed = true;
}
}
return d;
}
/**
* Changes the name, host and port of the Connection if it is not connected.
*
* @param name The new name of the Connection.
* @param host The new host of the Connection.
* @param port The new port of the Connection.
*/
public void setData(String name, String host, int port) {
if (!isConnected()) {
this.name = name;
this.host = host;
this.port = port;
}
}
/**
* Gets the host of the Connection.
*
* @return the host of the Connection.
*/
public String getHost() {
return host;
}
/**
* Gets the name of the Connection.
*
* @return the name of the Connection.
*/
public String getName() {
return name;
}
/**
* Gets the port of the Connection.
*
* @return the port of the Connection.
*/
public int getPort() {
return port;
}
/**
* Connects to the INDI Server.
*
* @throws IOException if there is some problem connecting to the Server.
*/
public void connect() throws IOException {
if (socket == null) {
socket = new Socket();
try {
socket.connect(new InetSocketAddress(host, port), 20000);
} catch (IOException e) {
socket = null;
throw e;
}
out = new PrintWriter(socket.getOutputStream(), true);
reader = new INDIProtocolReader(this);
reader.start();
}
}
/**
* If connected, disconnects from the INDI Server and notifies the listeners.
*
*/
public void disconnect() {
if (socket != null) {
try {
socket.shutdownInput();
out.close();
// socket.close();
} catch (IOException e) {
}
socket = null;
devices.clear();
notifyListenersConnectionLost();
}
}
/**
* Determines if the Connection is stablished of not.
*
* @return
* <code>true</code> if the Connection is stablished.
* <code>false</code> otherwise.
*/
public boolean isConnected() {
if (socket == null) {
return false;
}
return true;
}
/**
* Sends the appropriate message to the INDI Server to be notified about the
* Devices of the Server.
*
* @throws IOException if there is some problem with the Connection.
*/
public void askForDevices() throws IOException {
String message = "<getProperties version=\"1.7\" />";
sendMessageToServer(message);
}
/**
* Sends the appropriate message to the INDI Server to be notified about a
* particular Device of the Server.
*
* @param device the Device name that is asked for.
* @throws IOException if there is some problem with the Connection.
*/
public void askForDevices(String device) throws IOException {
String message = "<getProperties version=\"1.7\" device=\"" + device + "\"/>";
sendMessageToServer(message);
}
/**
* Sends the appropriate message to the INDI Server to be notified about a
* particular Property of a particular Device of the Server.
*
* @param device the Device name of whose property is asked for.
* @param propertyName the Property name that is asked for.
* @throws IOException if there is some problem with the Connection.
*/
public void askForDevices(String device, String propertyName) throws IOException {
String message = "<getProperties version=\"1.7\" device=\"" + device + "\" name=\"" + propertyName + "\"/>";
sendMessageToServer(message);
}
/**
* Sends a XML message to the server.
*
* @param XML the message to be sent.
* @throws IOException if there is some problem with the Connection.
*/
protected void sendMessageToServer(String XML) throws IOException {
out.print(XML);
out.flush();
}
@Override
public void finishReader() {
disconnect(); // If there has been a problem with reading the port really disconnect and notify listeners
}
/**
* Adds a new Device to this Connection and notifies the listeners.
*
* @param device the device to be added.
*/
private void addDevice(INDIDevice device) {
devices.put(device.getName(), device);
notifyListenersNewDevice(device);
}
/**
* Gets a particular Device by its name.
*
* @param name the name of the Device
* @return the Device with the
* <code>name</code> or
* <code>null</code> if there is no Device with that name.
*/
public INDIDevice getDevice(String name) {
return devices.get(name);
}
/**
* A convenience method to get the Property of a Device by specifiying their
* names.
*
* @param deviceName the name of the Device.
* @param propertyName the name of the Property.
* @return the Property with
* <code>propertyName</code> as name of the device with
* <code>deviceName</code> as name.
*/
public INDIProperty getProperty(String deviceName, String propertyName) {
INDIDevice d = getDevice(deviceName);
if (d == null) {
return null;
}
return d.getProperty(propertyName);
}
/**
* A convenience method to get the Element of a Property of a Device by
* specifiying their names.
*
* @param deviceName the name of the Property.
* @param propertyName the name of the Element.
* @param elementName the name of the Element.
* @return the Element with a
* <code>elementName</code> as a name of a Property with
* <code>propertyName</code> as name of the device with
* <code>deviceName</code> as name.
*/
public INDIElement getElement(String deviceName, String propertyName, String elementName) {
INDIDevice d = getDevice(deviceName);
if (d == null) {
return null;
}
return d.getElement(propertyName, elementName);
}
/**
* Parses the XML messages.
*
* @param doc the messages to be parsed.
*/
@Override
public void parseXML(Document doc) {
Element el = doc.getDocumentElement();
if (el.getNodeName().compareTo("INDI") != 0) {
return;
}
NodeList nodes = el.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node n = nodes.item(i);
if (n instanceof Element) {
Element child = (Element) n;
String name = child.getNodeName();
if ((name.equals("defTextVector")) || (name.equals("defNumberVector")) || (name.equals("defSwitchVector")) || (name.equals("defLightVector")) || (name.equals("defBLOBVector"))) {
addProperty(child);
} else if ((name.equals("setTextVector")) || (name.equals("setNumberVector")) || (name.equals("setSwitchVector")) || (name.equals("setLightVector")) || (name.equals("setBLOBVector"))) {
updateProperty(child);
} else if (name.equals("message")) {
messageReceived(child);
} else if (name.equals("delProperty")) {
deleteProperty(child);
}
}
}
}
/**
* Parses a XML <delProperty> element and notifies the listeners.
*
* @param xml The element to be parsed.
*/
private void deleteProperty(Element xml) {
if (xml.hasAttribute("device")) {
String deviceName = xml.getAttribute("device").trim();
INDIDevice d = getDevice(deviceName);
if (d != null) {
String propertyName = xml.getAttribute("name").trim();
if (!(propertyName.length() == 0)) {
d.deleteProperty(xml);
} else {
deleteDevice(d);
}
} else {
deleteAllDevices();
}
}
}
/**
* Deletes all the Devices from the Connection and notifies the listeners.
*/
private void deleteAllDevices() {
Iterator<INDIDevice> devs = devices.values().iterator();
while (!devs.hasNext()) {
deleteDevice(devs.next());
}
}
/**
* Deletes a Device from the Connection and notifies the listeners.
*
* @param device the Device to be removed.
*/
private void deleteDevice(INDIDevice device) {
devices.remove(device.getName());
notifyListenersRemoveDevice(device);
}
/**
* Parses a XML <message> element and notifies the listeners if
* appropriate.
*
* @param xml The XML to be parsed.
*/
private void messageReceived(Element xml) {
if (xml.hasAttribute("device")) {
String deviceName = xml.getAttribute("device").trim();
INDIDevice d = getDevice(deviceName);
if (d != null) {
d.messageReceived(xml);
}
} else { // Global message from server
if (xml.hasAttribute("message")) {
String time = xml.getAttribute("timestamp").trim();
Date timestamp = INDIDateFormat.parseTimestamp(time);
String message = xml.getAttribute("message").trim();
notifyListenersNewMessage(timestamp, message);
}
}
}
/**
* Parses a XML <defXXXVector> element.
*
* @param xml the element to be parsed.
*/
private void addProperty(Element xml) {
String deviceName = xml.getAttribute("device").trim();
INDIDevice d = getDevice(deviceName);
if (d == null) {
d = new INDIDevice(deviceName, this);
addDevice(d);
}
d.addProperty(xml);
}
/**
* Parses a XML <setXXXVector> element.
*
* @param xml the element to be parsed.
*/
private void updateProperty(Element el) {
String deviceName = el.getAttribute("device").trim();
INDIDevice d = getDevice(deviceName);
if (d != null) { // If device does no exist ignore
d.updateProperty(el);
}
}
/**
* Adds a new listener to this Connection.
*
* @param listener the listener to be added.
*/
public void addINDIServerConnectionListener(INDIServerConnectionListener listener) {
listeners.add(listener);
}
/**
* Removes a listener from this Connection.
*
* @param listener the listener to be removed.
*/
public void removeINDIServerConnectionListener(INDIServerConnectionListener listener) {
listeners.remove(listener);
}
/**
* Adds a listener to all the Devices from this Connection
*
* @param listener the listener to add
*/
public void addINDIDeviceListenerToAllDevices(INDIDeviceListener listener) {
List<INDIDevice> l = getDevicesAsList();
for (int i = 0; i < l.size(); i++) {
INDIDevice d = l.get(i);
d.addINDIDeviceListener(listener);
}
}
/**
* Removes a listener from all the Devices from this Connection
*
* @param listener the listener to remove
*/
public void removeINDIDeviceListenerFromAllDevices(INDIDeviceListener listener) {
List<INDIDevice> l = getDevicesAsList();
for (int i = 0; i < l.size(); i++) {
INDIDevice d = l.get(i);
d.removeINDIDeviceListener(listener);
}
}
/**
* Gets the names of the Devices of this Connection.
*
* @return the names of the Devices of this Connection.
*/
public String[] getDeviceNames() {
List<INDIDevice> l = getDevicesAsList();
String[] names = new String[l.size()];
for (int i = 0; i < l.size(); i++) {
names[i] = l.get(i).getName();
}
return names;
}
/**
* Gets a
* <code>List</code> with all the Devices of this Connection.
*
* @return the
* <code>List</code> of Devices belonging to this Connection.
*/
public List<INDIDevice> getDevicesAsList() {
return new ArrayList<INDIDevice>(devices.values());
}
/**
* A convenience method to add a listener to a Device (identified by its name)
* of this Connection.
*
* @param deviceName the Device name to which add the listener
* @param listener the listener to add
*/
public void addINDIDeviceListener(String deviceName, INDIDeviceListener listener) {
INDIDevice d = getDevice(deviceName);
if (d == null) {
return;
}
d.addINDIDeviceListener(listener);
}
/**
* A convenience method to remove a listener from a Device (identified by its
* name) of this Connection.
*
* @param deviceName the Device name to which remove the listener
* @param listener the listener to remove
*/
public void removeINDIDeviceListener(String deviceName, INDIDeviceListener listener) {
INDIDevice d = getDevice(deviceName);
if (d == null) {
return;
}
d.removeINDIDeviceListener(listener);
}
/**
* Notifies the listeners about a new Device.
*
* @param device the new Device.
*/
private void notifyListenersNewDevice(INDIDevice device) {
ArrayList<INDIServerConnectionListener> lCopy = (ArrayList<INDIServerConnectionListener>) listeners.clone();
for (int i = 0; i < lCopy.size(); i++) {
INDIServerConnectionListener l = lCopy.get(i);
l.newDevice(this, device);
}
}
/**
* Notifies the listeners about a Device that is removed.
*
* @param device the removed device.
*/
private void notifyListenersRemoveDevice(INDIDevice device) {
ArrayList<INDIServerConnectionListener> lCopy = (ArrayList<INDIServerConnectionListener>) listeners.clone();
for (int i = 0; i < lCopy.size(); i++) {
INDIServerConnectionListener l = lCopy.get(i);
l.removeDevice(this, device);
}
}
/**
* Notifies the listeners when the Connection is lost.
*/
private void notifyListenersConnectionLost() {
ArrayList<INDIServerConnectionListener> lCopy = (ArrayList<INDIServerConnectionListener>) listeners.clone();
for (int i = 0; i < lCopy.size(); i++) {
INDIServerConnectionListener l = lCopy.get(i);
l.connectionLost(this);
}
}
/**
* Notifies the listeners about a new Server message.
*
* @param timestamp the timestamp of the message.
* @param message the message.
*/
protected void notifyListenersNewMessage(Date timestamp, String message) {
ArrayList<INDIServerConnectionListener> lCopy = (ArrayList<INDIServerConnectionListener>) listeners.clone();
for (int i = 0; i < lCopy.size(); i++) {
INDIServerConnectionListener l = lCopy.get(i);
l.newMessage(this, timestamp, message);
}
}
/**
* Gets the input stream of this Connection.
*
* @return The input stream of this Connection.
*/
@Override
public InputStream getInputStream() {
try {
return socket.getInputStream();
} catch (IOException e) {
return null;
}
}
}