net.sbbi.upnp.ServicesEventing.java Source code

Java tutorial

Introduction

Here is the source code for net.sbbi.upnp.ServicesEventing.java

Source

/*
 * ============================================================================
 *                 The Apache Software License, Version 1.1
 * ============================================================================
 *
 * Copyright (C) 2002 The Apache Software Foundation. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modifica-
 * tion, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of  source code must  retain the above copyright  notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. The end-user documentation included with the redistribution, if any, must
 *    include the following  acknowledgment: "This product includes software
 *    developed by SuperBonBon Industries (http://www.sbbi.net/)."
 *    Alternately, this acknowledgment may appear in the software itself, if
 *    and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "UPNPLib" and "SuperBonBon Industries" must not be
 *    used to endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    info@sbbi.net.
 *
 * 5. Products  derived from this software may not be called 
 *    "SuperBonBon Industries", nor may "SBBI" appear in their name, 
 *    without prior written permission of SuperBonBon Industries.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED. IN NO EVENT SHALL THE
 * APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT,INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU-
 * DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software  consists of voluntary contributions made by many individuals
 * on behalf of SuperBonBon Industries. For more information on 
 * SuperBonBon Industries, please see <http://www.sbbi.net/>.
 */
package net.sbbi.upnp;

import java.util.*;
import java.io.*;
import java.net.*;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import net.sbbi.upnp.services.UPNPService;

import org.apache.commons.logging.*;
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 Log log = LogFactory.getLog(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 List registered = new ArrayList();

    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 i = registered.iterator(); i.hasNext();) {
                Subscription sub = (Subscription) 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 i = registered.iterator(); i.hasNext();) {
                Subscription sub = (Subscription) 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 i = registered.iterator(); i.hasNext();) {
                Subscription sub = (Subscription) i.next();

                if (sub.sub.getSID().equals(sid)) {
                    return sub;
                }
            }
        }
        return null;
    }

    /**
     * Unregisters events 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.size() == 0) {
                    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 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 (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);
                            }
                        }
                        if (subscription != null) {
                            // respond ok
                            out.write("HTTP/1.1 200 OK\r\n".getBytes());
                        } else {
                            // unknown sid respond ko
                            out.write("HTTP/1.1 412 Precondition Failed\r\n".getBytes());
                        }

                        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 changedStateVars = msgParser.getChangedStateVars();
                            for (Iterator i = changedStateVars.keySet().iterator(); i.hasNext();) {
                                String stateVarName = (String) i.next();
                                String stateVarNewVal = (String) 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);
            }
        }
    }
}