/*
* This software copyright by various authors including the RPTools.net
* development team, and licensed under the LGPL Version 3 or, at your
* option, any later version.
*
* Portions of this software were originally covered under the Apache
* Software License, Version 1.1 or Version 2.0.
*
* See the file LICENSE elsewhere in this distribution for license details.
*/
package net.sbbi.upnp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import net.sbbi.upnp.services.UPNPService;
import org.apache.log4j.Logger;
import org.xml.sax.InputSource;
/**
* This class can be used with the ServiceEventHandler interface to recieve notifications about state variables changes
* on a given UPNP service.
*
* @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
* @version 1.0
*/
public class ServicesEventing implements Runnable {
private final static Logger log = Logger.getLogger(ServicesEventing.class);
private final static ServicesEventing singleton = new ServicesEventing();
private boolean inService = false;
private boolean daemon = true;
private int daemonPort = 9999;
private ServerSocket server = null;
private final List<Subscription> registered = new ArrayList<Subscription>();
private ServicesEventing() {
}
public final static ServicesEventing getInstance() {
return singleton;
}
/**
* Set the listeniner thread as a daemon, default to true. Only works when no more objects are registered.
*
* @param daemon
* the new thread type.
*/
public void setDaemon(boolean daemon) {
this.daemon = daemon;
}
/**
* Sets the listener thread port, default to 9999. Only works when no more objects are registered.
*
* @param daemonPort
* the new listening port
*/
public void setDaemonPort(int daemonPort) {
this.daemonPort = daemonPort;
}
/**
* Register state variable events notification for a device service
*
* @param service
* the service to register with
* @param handler
* the registrant object
* @param subscriptionDuration
* subscription time in seconds, -1 for infinite time
* @return the subscription duration returned by the device, 0 for an infinite duration or -1 if no subscription
* done
* @throws IOException
* if some IOException error happens during coms with the device
*/
public int register(UPNPService service, ServiceEventHandler handler, int subscriptionDuration) throws IOException {
ServiceEventSubscription sub = registerEvent(service, handler, subscriptionDuration);
if (sub != null) {
return sub.getDurationTime();
}
return -1;
}
/**
* Register state variable events notification for a device service
*
* @param service
* the service to register with
* @param handler
* the registrant object
* @param subscriptionDuration
* subscription time in seconds, -1 for infinite time
* @return an ServiceEventSubscription object instance containing all the required info or null if no subscription
* done
* @throws IOException
* if some IOException error happens during coms with the device
*/
public ServiceEventSubscription registerEvent(UPNPService service, ServiceEventHandler handler, int subscriptionDuration) throws IOException {
URL eventingLoc = service.getEventSubURL();
if (eventingLoc != null) {
if (!inService)
startServicesEventingThread();
String duration = Integer.toString(subscriptionDuration);
if (subscriptionDuration == -1) {
duration = "infinite";
}
Subscription sub = lookupSubscriber(service, handler);
if (sub != null) {
// allready registered let's try to unregister it
unRegister(service, handler);
}
StringBuffer packet = new StringBuffer(64);
packet.append("SUBSCRIBE ").append(eventingLoc.getFile()).append(" HTTP/1.1\r\n");
packet.append("HOST: ").append(eventingLoc.getHost()).append(":").append(eventingLoc.getPort()).append("\r\n");
packet.append("CALLBACK: <http://").append(InetAddress.getLocalHost().getHostAddress()).append(":").append(daemonPort).append("").append(eventingLoc.getFile()).append(">\r\n");
packet.append("NT: upnp:event\r\n");
packet.append("Connection: close\r\n");
packet.append("TIMEOUT: Second-").append(duration).append("\r\n\r\n");
Socket skt = new Socket(eventingLoc.getHost(), eventingLoc.getPort());
skt.setSoTimeout(30000); // 30 secs timeout according to the specs
if (log.isDebugEnabled())
log.debug(packet);
OutputStream out = skt.getOutputStream();
out.write(packet.toString().getBytes());
out.flush();
InputStream in = skt.getInputStream();
StringBuffer data = new StringBuffer();
int readen = 0;
byte[] buffer = new byte[256];
while ((readen = in.read(buffer)) != -1) {
data.append(new String(buffer, 0, readen));
}
in.close();
out.close();
skt.close();
if (log.isDebugEnabled())
log.debug(data.toString());
if (data.toString().trim().length() > 0) {
HttpResponse resp = new HttpResponse(data.toString());
if (resp.getHeader().startsWith("HTTP/1.1 200 OK")) {
String sid = resp.getHTTPHeaderField("SID");
String actualTimeout = resp.getHTTPHeaderField("TIMEOUT");
int durationTime = 0;
// actualTimeout = Second-xxx or Second-infinite
if (!actualTimeout.equalsIgnoreCase("Second-infinite")) {
durationTime = Integer.parseInt(actualTimeout.substring(7));
}
sub = new Subscription();
sub.handler = handler;
sub.sub = new ServiceEventSubscription(service.getServiceType(), service.getServiceId(),
service.getEventSubURL(), sid, skt.getInetAddress(),
durationTime);
synchronized (registered) {
registered.add(sub);
}
return sub.sub;
}
}
}
return null;
}
private Subscription lookupSubscriber(UPNPService service, ServiceEventHandler handler) {
synchronized (registered) {
for (Iterator<Subscription> i = registered.iterator(); i.hasNext();) {
Subscription sub = i.next();
if (sub.handler == handler &&
sub.sub.getServiceId().hashCode() == service.getServiceId().hashCode() &&
sub.sub.getServiceType().hashCode() == service.getServiceType().hashCode() &&
sub.sub.getServiceURL().equals(service.getEventSubURL())) {
return sub;
}
}
}
return null;
}
private Subscription lookupSubscriber(String sid, InetAddress deviceIp) {
synchronized (registered) {
for (Iterator<Subscription> i = registered.iterator(); i.hasNext();) {
Subscription sub = i.next();
if (sub.sub.getSID().equals(sid) && sub.sub.getDeviceIp().equals(deviceIp)) {
return sub;
}
}
}
return null;
}
private Subscription lookupSubscriber(String sid) {
synchronized (registered) {
for (Iterator<Subscription> i = registered.iterator(); i.hasNext();) {
Subscription sub = i.next();
if (sub.sub.getSID().equals(sid)) {
return sub;
}
}
}
return null;
}
/**
* Unregisters event notifications from a service
*
* @param service
* the service that need to be unregistered
* @param handler
* the handler that registered for this service
* @return true if unregistered false otherwise ( the given handler never registred for the given service )
* @throws IOException
* if some IOException error happens during coms with the device
*/
public boolean unRegister(UPNPService service, ServiceEventHandler handler) throws IOException {
URL eventingLoc = service.getEventSubURL();
if (eventingLoc != null) {
Subscription sub = lookupSubscriber(service, handler);
if (sub != null) {
synchronized (registered) {
registered.remove(sub);
}
if (registered.isEmpty()) {
stopServicesEventingThread();
}
StringBuffer packet = new StringBuffer(64);
packet.append("UNSUBSCRIBE ").append(eventingLoc.getFile()).append(" HTTP/1.1\r\n");
packet.append("HOST: ").append(eventingLoc.getHost()).append(":").append(eventingLoc.getPort()).append("\r\n");
packet.append("SID: ").append(sub.sub.getSID()).append("\r\n\r\n");
Socket skt = new Socket(eventingLoc.getHost(), eventingLoc.getPort());
skt.setSoTimeout(30000); // 30 secs timeout according to the specs
if (log.isDebugEnabled())
log.debug(packet);
OutputStream out = skt.getOutputStream();
out.write(packet.toString().getBytes());
out.flush();
InputStream in = skt.getInputStream();
StringBuffer data = new StringBuffer();
int readen = 0;
byte[] buffer = new byte[256];
while ((readen = in.read(buffer)) != -1) {
data.append(new String(buffer, 0, readen));
}
in.close();
out.close();
skt.close();
if (log.isDebugEnabled())
log.debug(data.toString());
if (data.toString().trim().length() > 0) {
HttpResponse resp = new HttpResponse(data.toString());
if (resp.getHeader().startsWith("HTTP/1.1 200 OK")) {
return true;
}
}
}
}
return false;
}
private void startServicesEventingThread() {
synchronized (singleton) {
if (!inService) {
Thread deamon = new Thread(singleton, "ServicesEventing daemon");
deamon.setDaemon(daemon);
inService = true;
deamon.start();
}
}
}
private void stopServicesEventingThread() {
synchronized (singleton) {
inService = false;
try {
server.close();
} catch (IOException ex) {
// should not happen
}
}
}
public void run() {
// only the deamon thread is allowed to call such method
if (!Thread.currentThread().getName().equals("ServicesEventing daemon"))
return;
try {
server = new ServerSocket(daemonPort);
} catch (IOException ex) {
log.error("Error during daemon server socket on port " + daemonPort + " creation", ex);
return;
}
while (inService) {
try {
Socket skt = server.accept();
new Thread(new RequestProcessor(skt)).start();
} catch (IOException ioEx) {
if (inService) {
log.error("IO Exception during UPNP messages listening thread", ioEx);
}
}
}
}
private class Subscription {
private ServiceEventSubscription sub = null;
private ServiceEventHandler handler = null;
}
private class RequestProcessor implements Runnable {
private final Socket client;
private RequestProcessor(Socket client) {
this.client = client;
}
public void run() {
try {
client.setSoTimeout(30000);
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
int readen = 0;
StringBuffer data = new StringBuffer();
byte[] buffer = new byte[256];
boolean EOF = false;
while (!EOF && (readen = in.read(buffer)) != -1) {
data.append(new String(buffer, 0, readen));
// avoid a strange behaviour with some impls.. the -1 is never reached and a sockettimeout occurs
// and a 0 byte is sent as the last byte
if (data.charAt(data.length() - 1) == (char) 0) {
EOF = true;
}
}
String packet = data.toString();
if (log.isDebugEnabled())
log.debug("HttpResponse: " + packet);
if (packet.trim().length() > 0) {
if (packet.indexOf((char) 0) != -1)
packet = packet.replace((char) 0, ' ');
HttpResponse resp = new HttpResponse(packet);
if (resp.getHeader().startsWith("NOTIFY")) {
String sid = resp.getHTTPHeaderField("SID");
InetAddress deviceIp = client.getInetAddress();
String postURL = resp.getHTTPHeaderField("SID");
Subscription subscription = null;
if (sid != null && postURL != null) {
subscription = lookupSubscriber(sid, deviceIp);
if (subscription == null) {
// not found maybe that the IP is not the same
subscription = lookupSubscriber(sid);
}
}
String msg;
if (subscription != null) {
// respond ok
msg = "HTTP/1.1 200 OK";
} else {
// unknown sid respond ko
msg = "HTTP/1.1 412 Precondition Failed";
}
out.write((msg + "\r\n").getBytes());
if (log.isDebugEnabled())
log.debug("Subscription Message: " + msg + "\n");
out.flush();
in.close();
out.close();
client.close();
if (subscription != null) {
// let's parse it
SAXParserFactory saxParFact = SAXParserFactory.newInstance();
saxParFact.setValidating(false);
saxParFact.setNamespaceAware(true);
SAXParser parser = saxParFact.newSAXParser();
ServiceEventMessageParser msgParser = new ServiceEventMessageParser();
StringReader stringReader = new StringReader(resp.getBody());
InputSource src = new InputSource(stringReader);
parser.parse(src, msgParser);
Map<String, String> changedStateVars = msgParser.getChangedStateVars();
for (Iterator<String> i = changedStateVars.keySet().iterator(); i.hasNext();) {
String stateVarName = i.next();
String stateVarNewVal = changedStateVars.get(stateVarName);
subscription.handler.handleStateVariableEvent(stateVarName, stateVarNewVal);
}
}
}
}
} catch (IOException ioEx) {
log.error("IO Exception during client processing thread", ioEx);
} catch (Exception ex) {
log.error("Unexpected error during client processing thread", ex);
}
}
}
}