diff --git a/.classpath b/.classpath index 1b443b512..10fe6dcd2 100644 --- a/.classpath +++ b/.classpath @@ -17,7 +17,6 @@ - @@ -36,6 +35,6 @@ - + diff --git a/NOTICE b/NOTICE index 7a9657377..fdb65db16 100644 --- a/NOTICE +++ b/NOTICE @@ -9,3 +9,6 @@ Copyright 2001-2008 The Apache Software Foundation This product includes software developed by The Apache Software Foundation (http://www.apache.org/). + +This product includes software developed by +SuperBonBon Industries (http://www.sbbi.net/) \ No newline at end of file diff --git a/build.xml b/build.xml index 438832ab9..4c7352e47 100644 --- a/build.xml +++ b/build.xml @@ -172,7 +172,7 @@ - + @@ -182,7 +182,6 @@ - diff --git a/lib/commons-jxpath-1.1.License b/lib/commons-jxpath-1.1.License deleted file mode 100644 index 603fe6a26..000000000 --- a/lib/commons-jxpath-1.1.License +++ /dev/null @@ -1,55 +0,0 @@ -/* ==================================================================== - * - * The Apache Software License, Version 1.1 - * - * Copyright (c) 1999-2003 The Apache Software Foundation. All rights - * reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, 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 acknowlegement: - * "This product includes software developed by the - * Apache Software Foundation (http://www.apache.org/)." - * Alternately, this acknowlegement may appear in the software itself, - * if and wherever such third-party acknowlegements normally appear. - * - * 4. The names "The Jakarta Project", "Commons", and "Apache Software - * Foundation" must not be used to endorse or promote products derived - * from this software without prior written permission. For written - * permission, please contact apache@apache.org. - * - * 5. Products derived from this software may not be called "Apache" - * nor may "Apache" appear in their names without prior written - * permission of the Apache Group. - * - * 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 (INCLUDING, 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 the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ \ No newline at end of file diff --git a/lib/commons-jxpath-1.1.jar b/lib/commons-jxpath-1.1.jar deleted file mode 100644 index 9516e2ad6..000000000 Binary files a/lib/commons-jxpath-1.1.jar and /dev/null differ diff --git a/lib/commons-jxpath-1.3.License b/lib/commons-jxpath-1.3.License new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/lib/commons-jxpath-1.3.License @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/commons-jxpath-1.3.jar b/lib/commons-jxpath-1.3.jar new file mode 100644 index 000000000..e344a1e66 Binary files /dev/null and b/lib/commons-jxpath-1.3.jar differ diff --git a/lib/sbbi-upnplib-1.0.4.License b/lib/sbbi-upnplib-1.0.4.License deleted file mode 100644 index 5b80110cb..000000000 --- a/lib/sbbi-upnplib-1.0.4.License +++ /dev/null @@ -1,46 +0,0 @@ - ============================================================================ - 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 . - diff --git a/lib/sbbi-upnplib-1.0.4.jar b/lib/sbbi-upnplib-1.0.4.jar deleted file mode 100644 index 271e919c7..000000000 Binary files a/lib/sbbi-upnplib-1.0.4.jar and /dev/null differ diff --git a/source/net/sbbi/upnp/Discovery.java b/source/net/sbbi/upnp/Discovery.java new file mode 100644 index 000000000..ec5cd92ab --- /dev/null +++ b/source/net/sbbi/upnp/Discovery.java @@ -0,0 +1,264 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import net.sbbi.upnp.devices.UPNPRootDevice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Class to discover an UPNP device on the network.
+ * A multicast socket will be created to discover devices, the binding port for this socket is set to 1901, + * if this is causing a problem you can use the net.sbbi.upnp.Discovery.bindPort system property + * to specify another port. + * The discovery methods only accept matching device description and broadcast message response IP + * to avoid a security flaw with the protocol. If you are not happy with such behaviour + * you can set the net.sbbi.upnp.ddos.matchip system property to false to avoid this check. + * @author SuperBonBon + * @version 1.0 + */ + +public class Discovery { + + private final static Log log = LogFactory.getLog( Discovery.class ); + + public final static String ROOT_DEVICES = "upnp:rootdevice"; + public final static String ALL_DEVICES = "ssdp:all"; + + public static final int DEFAULT_MX = 3; + public static final int DEFAULT_TTL = 4; + public static final int DEFAULT_TIMEOUT = 1500; + public static final String DEFAULT_SEARCH = ALL_DEVICES; + public static final int DEFAULT_SSDP_SEARCH_PORT = 1901; + + public final static String SSDP_IP = "239.255.255.250"; + public final static int SSDP_PORT = 1900; + + /** + * Devices discovering on all network interfaces with default values, all root devices will be searched + * @return an array of UPNP Root device or null if nothing found with the default timeout. + * Null does NOT means that no UPNP device is available on the network. It only means + * that for this default timeout no devices responded or that effectively no devices + * are available at all. + * @throws IOException if some IOException occurs during discovering + */ + public static UPNPRootDevice[] discover() throws IOException { + return discover( DEFAULT_TIMEOUT, DEFAULT_TTL, DEFAULT_MX, DEFAULT_SEARCH ); + } + + /** + * Devices discovering on all network interfaces with a given root device to search + * @param searchTarget the device URI to search + * @return an array of UPNP Root device that matches the search or null if nothing found with the default timeout. + * Null does NOT means that no UPNP device is available on the network. It only means + * that for this given timeout no devices responded or that effectively no devices + * are available at all. + * @throws IOException if some IOException occurs during discovering + */ + public static UPNPRootDevice[] discover( String searchTarget ) throws IOException { + return discover( DEFAULT_TIMEOUT, DEFAULT_TTL, DEFAULT_MX, searchTarget ); + } + + /** + * Devices discovering on all network interfaces with a given timeout and a given root device to search + * @param timeOut the time allowed for a device to give a response + * @param searchTarget the device URI to search + * @return an array of UPNP Root device that matches the search or null if nothing found with the given timeout. + * Null does NOT means that no UPNP device is available on the network. It only means + * that for this given timeout no devices responded or that effectively no devices + * are available at all. + * @throws IOException if some IOException occurs during discovering + */ + public static UPNPRootDevice[] discover( int timeOut, String searchTarget ) throws IOException { + return discover( timeOut, DEFAULT_TTL, DEFAULT_MX, searchTarget ); + } + + /** + * Devices discovering on all network interfaces with a given timeout and a given root device to search, as well as a ttl and mx param + * @param timeOut the timeout for the a device to give a reponse + * @param ttl the UDP socket packets time to live + * @param mx discovery message mx http header field value + * @param searchTarget the device URI to search + * @return an array of UPNP Root device that matches the search or null if nothing found within the given timeout. + * Null return does NOT means that no UPNP device is available on the network. It only means + * that for this given timeout no devices responded or that effectively no devices + * are available at all. + * @throws IOException if some IOException occurs during discovering + */ + public static UPNPRootDevice[] discover( int timeOut, int ttl, int mx, String searchTarget ) throws IOException { + return discoverDevices( timeOut, ttl, mx, searchTarget, null ); + } + + /** + * Devices discovering with a given timeout and a given root device to search on an given network interface, as well as a ttl and mx param + * @param timeOut the timeout for the a device to give a reponse + * @param ttl the UDP socket packets time to live + * @param mx discovery message mx http header field value + * @param searchTarget the device URI to search + * @param ni the networkInterface where to search devices, null to lookup all interfaces + * @return an array of UPNP Root device that matches the search or null if nothing found within the given timeout. + * Null return does NOT means that no UPNP device is available on the network. It only means + * that for this given timeout no devices responded or that effectively no devices + * are available at all. + * @throws IOException if some IOException occurs during discovering + */ + public static UPNPRootDevice[] discover( int timeOut, int ttl, int mx, String searchTarget, NetworkInterface ni ) throws IOException { + return discoverDevices( timeOut, ttl, mx, searchTarget, ni ); + } + + private static UPNPRootDevice[] discoverDevices( int timeOut, int ttl, int mx, String searchTarget, NetworkInterface ni ) throws IOException { + if ( searchTarget == null || searchTarget.trim().length() == 0 ) { + throw new IllegalArgumentException( "Illegal searchTarget" ); + } + + final Map devices = new HashMap(); + + DiscoveryResultsHandler handler = new DiscoveryResultsHandler() { + + public void discoveredDevice( String usn, String udn, String nt, String maxAge, URL location, String firmware ) { + synchronized( devices ) { + if ( ! devices.containsKey( usn ) ) { + try { + UPNPRootDevice device = new UPNPRootDevice( location, maxAge, firmware, usn, udn ); + devices.put( usn, device ); + } catch ( Exception ex ) { + log.error( "Error occured during upnp root device object creation from location " + location, ex ); + } + } + } + } + }; + + DiscoveryListener.getInstance().registerResultsHandler( handler, searchTarget ); + if ( ni == null ) { + for ( Enumeration e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) { + NetworkInterface intf = (NetworkInterface)e.nextElement(); + for ( Enumeration adrs = intf.getInetAddresses(); adrs.hasMoreElements(); ) { + InetAddress adr = (InetAddress)adrs.nextElement(); + if ( adr instanceof Inet4Address && !adr.isLoopbackAddress() ) { + sendSearchMessage( adr, ttl, mx, searchTarget ); + } + } + } + } else { + for ( Enumeration adrs = ni.getInetAddresses(); adrs.hasMoreElements(); ) { + InetAddress adr = (InetAddress)adrs.nextElement(); + if ( adr instanceof Inet4Address && !adr.isLoopbackAddress() ) { + sendSearchMessage( adr, ttl, mx, searchTarget ); + } + } + } + + try { + Thread.sleep( timeOut ); + } catch ( InterruptedException ex ) { + // don't care + } + + DiscoveryListener.getInstance().unRegisterResultsHandler( handler, searchTarget ); + + if ( devices.size() == 0 ) { + return null; + } + int j = 0; + UPNPRootDevice[] rootDevices = new UPNPRootDevice[devices.size()]; + for ( Iterator i = devices.values().iterator(); i.hasNext(); ) { + rootDevices[j++] = (UPNPRootDevice)i.next(); + } + return rootDevices; + + } + + /** + * Sends an SSDP search message on the network + * @param src the sender ip + * @param ttl the time to live + * @param mx the mx field + * @param searchTarget the search target + * @throws IOException if some IO errors occurs during search + */ + public static void sendSearchMessage( InetAddress src, int ttl, int mx, String searchTarget ) throws IOException { + + int bindPort = DEFAULT_SSDP_SEARCH_PORT; + String port = System.getProperty( "net.sbbi.upnp.Discovery.bindPort" ); + if ( port != null ) { + bindPort = Integer.parseInt( port ); + } + InetSocketAddress adr = new InetSocketAddress( InetAddress.getByName( Discovery.SSDP_IP ), Discovery.SSDP_PORT ); + + java.net.MulticastSocket skt = new java.net.MulticastSocket( null ); + skt.bind( new InetSocketAddress( src, bindPort ) ); + skt.setTimeToLive( ttl ); + StringBuffer packet = new StringBuffer(); + packet.append( "M-SEARCH * HTTP/1.1\r\n" ); + packet.append( "HOST: 239.255.255.250:1900\r\n" ); + packet.append( "MAN: \"ssdp:discover\"\r\n" ); + packet.append( "MX: ").append( mx ).append( "\r\n" ); + packet.append( "ST: " ).append( searchTarget ).append( "\r\n" ).append( "\r\n" ); + if ( log.isDebugEnabled() ) log.debug( "Sending discovery message on 239.255.255.250:1900 multicast address form ip " + src.getHostAddress() + ":\n" + packet.toString() ); + String toSend = packet.toString(); + byte[] pk = toSend.getBytes(); + skt.send( new DatagramPacket( pk, pk.length, adr ) ); + skt.disconnect(); + skt.close(); + } + +} diff --git a/source/net/sbbi/upnp/DiscoveryAdvertisement.java b/source/net/sbbi/upnp/DiscoveryAdvertisement.java new file mode 100644 index 000000000..a88678c46 --- /dev/null +++ b/source/net/sbbi/upnp/DiscoveryAdvertisement.java @@ -0,0 +1,379 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * SSDP messages listener Thread, notify registered objects implementing the interface DiscoveryEventHandler
+ * when a device joins the networks or leaves it.
+ * The listener thread is set to only accept matching device description and broadcast message sender IP + * to avoid a security flaw with the protocol. If you are not happy with such behaviour + * you can set the net.sbbi.upnp.ddos.matchip system property to false to avoid this check. + * @author SuperBonBon + * @version 1.0 + */ + +public class DiscoveryAdvertisement implements Runnable { + + private final static Log log = LogFactory.getLog( DiscoveryAdvertisement.class ); + + private static boolean MATCH_IP = true; + + static { + String prop = System.getProperty( "net.sbbi.upnp.ddos.matchip" ); + if ( prop != null && prop.equals( "false" ) ) MATCH_IP = false; + } + + private static final int DEFAULT_TIMEOUT = 250; + + public final static int EVENT_SSDP_ALIVE = 0; + public final static int EVENT_SSDP_BYE_BYE = 1; + + private final static String NTS_SSDP_ALIVE = "ssdp:alive"; + private final static String NTS_SSDP_BYE_BYE = "ssdp:byebye"; + private final static String NT_ALL_EVENTS = "DiscoveryAdvertisement:nt:allevents"; + + private Map byeByeRegistered = new HashMap(); + private Map aliveRegistered = new HashMap(); + private Map USNPerIP = new HashMap(); + + private final Object REGISTRATION_PROCESS = new Object(); + + private final static DiscoveryAdvertisement singleton = new DiscoveryAdvertisement(); + private boolean inService = false; + private boolean daemon = true; + + private java.net.MulticastSocket skt; + private DatagramPacket input; + + private DiscoveryAdvertisement() { + } + + public final static DiscoveryAdvertisement getInstance() { + return singleton; + } + + public void setDaemon( boolean daemon ) { + this.daemon = daemon; + } + + /** + * Registers an event category sent by UPNP devices + * @param notificationEvent the event type, either DiscoveryAdvertisement.EVENT_SSDP_ALIVE + * or DiscoveryAdvertisement.EVENT_SSDP_BYE_BYE + * @param nt the type of device advertisement, upnp:rootdevice will return you all advertisement in relation with nt upnp:rootdevice + * a null value specify that all nt type are wanted + * @param eventHandler the events handler, this objet will receive notifications.. + * @throws IOException if an error ocurs when the SSDP events listeners threads starts + */ + public void registerEvent( int notificationEvent, String nt, DiscoveryEventHandler eventHandler ) throws IOException { + synchronized( REGISTRATION_PROCESS ) { + if ( !inService ) startDevicesListenerThread(); + if ( nt == null ) nt = NT_ALL_EVENTS; + if ( notificationEvent == EVENT_SSDP_ALIVE ) { + Set handlers = (Set)aliveRegistered.get( nt ); + if ( handlers == null ) { + handlers = new HashSet(); + aliveRegistered.put( nt, handlers ); + } + handlers.add( eventHandler ); + } else if ( notificationEvent == EVENT_SSDP_BYE_BYE ) { + Set handlers = (Set)byeByeRegistered.get( nt ); + if ( handlers == null ) { + handlers = new HashSet(); + byeByeRegistered.put( nt, handlers ); + } + handlers.add( eventHandler ); + } else { + throw new IllegalArgumentException( "Unknown notificationEvent type" ); + } + } + } + + /** + * Unregisters an event category sent by UPNP devices + * @param notificationEvent the event type, either DiscoveryAdvertisement.EVENT_SSDP_ALIVE + * or DiscoveryAdvertisement.EVENT_SSDP_BYE_BYE + * @param nt the type of device advertisement, upnp:rootdevice will unregister all advertisement in relation with nt upnp:rootdevice + * a null value specify that all nt type are unregistered + * @param eventHandler the events handler that needs to be unregistred. + */ + public void unRegisterEvent( int notificationEvent, String nt, DiscoveryEventHandler eventHandler ) { + synchronized( REGISTRATION_PROCESS ) { + if ( nt == null ) nt = NT_ALL_EVENTS; + if ( notificationEvent == EVENT_SSDP_ALIVE ) { + Set handlers = (Set)aliveRegistered.get( nt ); + if ( handlers != null ) { + handlers.remove( eventHandler ); + if ( handlers.size() == 0 ) { + aliveRegistered.remove( nt ); + } + } + } else if ( notificationEvent == EVENT_SSDP_BYE_BYE ) { + Set handlers = (Set)byeByeRegistered.get( nt ); + if ( handlers != null ) { + handlers.remove( eventHandler ); + if ( handlers.size() == 0 ) { + byeByeRegistered.remove( nt ); + } + } + } else { + throw new IllegalArgumentException( "Unknown notificationEvent type" ); + } + if ( aliveRegistered.size() == 0 && byeByeRegistered.size() == 0 ) { + stopDevicesListenerThread(); + } + } + } + + private void startDevicesListenerThread() throws IOException { + synchronized( singleton ) { + if ( !inService ) { + this.startMultiCastSocket(); + Thread deamon = new Thread( this, "DiscoveryAdvertisement daemon" ); + deamon.setDaemon( daemon ); + deamon.start(); + // wait for the thread to be started + while( !inService ) { + // let's wait a few ms + try { + Thread.sleep( 2 ); + } catch( InterruptedException ex ) { + // don t care + } + } + } + } + } + + private void stopDevicesListenerThread() { + synchronized( singleton ) { + inService = false; + } + } + + private void startMultiCastSocket() throws IOException { + + skt = new java.net.MulticastSocket( null ); + skt.bind( new InetSocketAddress( InetAddress.getByName( "0.0.0.0" ), Discovery.SSDP_PORT ) ); + skt.setTimeToLive( Discovery.DEFAULT_TTL ); + skt.setSoTimeout( DEFAULT_TIMEOUT ); + skt.joinGroup( InetAddress.getByName( Discovery.SSDP_IP ) ); + + byte[] buf = new byte[2048]; + input = new DatagramPacket( buf, buf.length ); + + } + + public void run() { + if ( !Thread.currentThread().getName().equals( "DiscoveryAdvertisement daemon" ) ) { + throw new RuntimeException( "No right to call this method" ); + } + inService = true; + while ( inService ) { + try { + listenBroadCast(); + } catch ( SocketTimeoutException ex ) { + // ignoring + } catch ( IOException ioEx ) { + log.error( "IO Exception during UPNP DiscoveryAdvertisement messages listening thread", ioEx ); + } catch( Exception ex ) { + log.error( "Fatal Error during UPNP DiscoveryAdvertisement messages listening thread, thread will exit", ex ); + inService = false; + aliveRegistered.clear(); + byeByeRegistered.clear(); + USNPerIP.clear(); + } + } + + try { + skt.leaveGroup( InetAddress.getByName( Discovery.SSDP_IP ) ); + skt.close(); + } catch ( Exception ex ) { + // ignoring + } + } + + private void listenBroadCast() throws IOException { + + skt.receive( input ); + InetAddress from = input.getAddress(); + String received = new String( input.getData(), input.getOffset(), input.getLength() ); + HttpResponse msg = null; + try { + msg = new HttpResponse( received ); + } catch (IllegalArgumentException ex ) { + // crappy http sent + if ( log.isDebugEnabled() ) log.debug( "Skipping uncompliant HTTP message " + received ); + return; + } + String header = msg.getHeader(); + if ( header != null && header.startsWith( "NOTIFY" ) ) { + if ( log.isDebugEnabled() ) log.debug( received ); + String ntsField = msg.getHTTPHeaderField( "nts" ); + if( ntsField == null || ntsField.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'ntsField' field" ); + return; + } + if ( ntsField.equals( NTS_SSDP_ALIVE ) ) { + String deviceDescrLoc = msg.getHTTPHeaderField( "location" ); + if( deviceDescrLoc == null || deviceDescrLoc.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'location' field" ); + return; + } + URL loc = new URL( deviceDescrLoc ); + if ( MATCH_IP ) { + InetAddress locHost = InetAddress.getByName( loc.getHost() ); + if ( !from.equals( locHost ) ) { + log.warn( "Discovery message sender IP " + from + + " does not match device description IP " + locHost + + " skipping message, set the net.sbbi.upnp.ddos.matchip system property" + + " to false to avoid this check" ); + return; + } + } + + String nt = msg.getHTTPHeaderField( "nt" ); + if( nt == null || nt.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'nt' field" ); + return; + } + String maxAge = msg.getHTTPFieldElement( "Cache-Control", "max-age" ); + if( maxAge == null || maxAge.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'max-age' field" ); + return; + } + String usn = msg.getHTTPHeaderField( "usn" ); + if( usn == null || usn.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'usn' field" ); + return; + } + + USNPerIP.put( usn, from ); + String udn = usn; + int index = udn.indexOf( "::" ); + if ( index != -1 ) udn = udn.substring( 0, index ); + synchronized( REGISTRATION_PROCESS ) { + Set handlers = (Set)aliveRegistered.get( NT_ALL_EVENTS ); + if ( handlers != null ) { + for ( Iterator i = handlers.iterator(); i.hasNext(); ) { + DiscoveryEventHandler eventHandler = (DiscoveryEventHandler)i.next(); + eventHandler.eventSSDPAlive( usn, udn, nt, maxAge, loc ); + } + } + handlers = (Set)aliveRegistered.get( nt ); + if ( handlers != null ) { + for ( Iterator i = handlers.iterator(); i.hasNext(); ) { + DiscoveryEventHandler eventHandler = (DiscoveryEventHandler)i.next(); + eventHandler.eventSSDPAlive( usn, udn, nt, maxAge, loc ); + } + } + } + } else if ( ntsField.equals( NTS_SSDP_BYE_BYE ) ) { + String usn = msg.getHTTPHeaderField( "usn" ); + if( usn == null || usn.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'usn' field" ); + return; + } + String nt = msg.getHTTPHeaderField( "nt" ); + if( nt == null || nt.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'nt' field" ); + return; + } + + InetAddress originalAliveSenderIp = (InetAddress)USNPerIP.get( usn ); + if ( originalAliveSenderIp != null ) { + // we check that the sender ip of message for the usn + // match the sender ip of the alive message for wich the usn + // has been received + if ( !originalAliveSenderIp.equals( from ) ) { + // someone else is trying to say that the usn is leaving + // since IP do not match we skip the message + return; + } + } + + String udn = usn; + int index = udn.indexOf( "::" ); + if ( index != -1 ) udn = udn.substring( 0, index ); + synchronized( REGISTRATION_PROCESS ) { + Set handlers = (Set)byeByeRegistered.get( NT_ALL_EVENTS ); + if ( handlers != null ) { + for ( Iterator i = handlers.iterator(); i.hasNext(); ) { + DiscoveryEventHandler eventHandler = (DiscoveryEventHandler)i.next(); + eventHandler.eventSSDPByeBye( usn, udn, nt ); + } + } + handlers = (Set)byeByeRegistered.get( nt ); + if ( handlers != null ) { + for ( Iterator i = handlers.iterator(); i.hasNext(); ) { + DiscoveryEventHandler eventHandler = (DiscoveryEventHandler)i.next(); + eventHandler.eventSSDPByeBye( usn, udn, nt ); + } + } + } + } else { + log.warn( "Unvalid NTS field value (" + ntsField + ") received in NOTIFY message :" + received ); + } + } + } +} diff --git a/source/net/sbbi/upnp/DiscoveryEventHandler.java b/source/net/sbbi/upnp/DiscoveryEventHandler.java new file mode 100644 index 000000000..1685a6daa --- /dev/null +++ b/source/net/sbbi/upnp/DiscoveryEventHandler.java @@ -0,0 +1,76 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +/** + * Interface for object that want to receive events from the + * DiscoveryAdvertisement thread + * @author SuperBonBon + * @version 1.0 + */ + +public interface DiscoveryEventHandler { + + /** + * Called when a device joins the network or advertise it is still alive + * @param usn the device USN (udn::nt) + * @param udn the device UDN + * @param nt the device NT + * @param maxAge the device maxAge + * @param location the device location + */ + public void eventSSDPAlive( String usn, String udn, String nt, String maxAge, java.net.URL location ); + + /** + * Called when a device is leaving the network + * @param usn the device USN (udn::nt) + * @param udn the device UDN + * @param nt the device NT + */ + public void eventSSDPByeBye( String usn, String udn, String nt ); +} diff --git a/source/net/sbbi/upnp/DiscoveryListener.java b/source/net/sbbi/upnp/DiscoveryListener.java new file mode 100644 index 000000000..5924070d4 --- /dev/null +++ b/source/net/sbbi/upnp/DiscoveryListener.java @@ -0,0 +1,290 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class can be used to listen for UPNP devices responses when a search message is sent by a control point + * ( using the net.sbbi.upnp.Discovery.sendSearchMessage() method ) + * @author SuperBonBon + * @version 1.0 + */ + +public class DiscoveryListener implements Runnable { + + private final static Log log = LogFactory.getLog( DiscoveryListener.class ); + + private static boolean MATCH_IP = true; + + static { + String prop = System.getProperty( "net.sbbi.upnp.ddos.matchip" ); + if ( prop != null && prop.equals( "false" ) ) MATCH_IP = false; + } + + private static final int DEFAULT_TIMEOUT = 250; + + private Map registeredHandlers = new HashMap(); + + private final Object REGISTRATION_PROCESS = new Object(); + + private final static DiscoveryListener singleton = new DiscoveryListener(); + + private boolean inService = false; + private boolean daemon = true; + + private java.net.MulticastSocket skt; + private DatagramPacket input; + + private DiscoveryListener() { + } + + public final static DiscoveryListener getInstance() { + return singleton; + } + + /** + * Sets the listener as a daemon thread + * @param daemon daemon thread + */ + public void setDaemon( boolean daemon ) { + this.daemon = daemon; + } + + /** + * Registers an SSDP response message handler + * @param resultsHandler the SSDP response message handler + * @param searchTarget the search target + * @throws IOException if some errors occurs during SSDP search response messages listener thread startup + */ + public void registerResultsHandler( DiscoveryResultsHandler resultsHandler, String searchTarget ) throws IOException { + synchronized( REGISTRATION_PROCESS ) { + if ( !inService ) startDevicesListenerThread(); + Set handlers = (Set)registeredHandlers.get( searchTarget ); + if ( handlers == null ) { + handlers = new HashSet(); + registeredHandlers.put( searchTarget, handlers ); + } + handlers.add( resultsHandler ); + } + } + + /** + * Unregisters an SSDP response message handler + * @param resultsHandler the SSDP response message handler + * @param searchTarget the search target + */ + public void unRegisterResultsHandler( DiscoveryResultsHandler resultsHandler, String searchTarget ) { + synchronized( REGISTRATION_PROCESS ) { + Set handlers = (Set)registeredHandlers.get( searchTarget ); + if ( handlers != null ) { + handlers.remove( resultsHandler ); + if ( handlers.size() == 0 ) { + registeredHandlers.remove( searchTarget ); + } + } + if ( registeredHandlers.size() == 0 ) { + stopDevicesListenerThread(); + } + } + } + + private void startDevicesListenerThread() throws IOException { + synchronized( singleton ) { + if ( !inService ) { + + this.startMultiCastSocket(); + Thread deamon = new Thread( this, "DiscoveryListener daemon" ); + deamon.setDaemon( daemon ); + deamon.start(); + while ( !inService ) { + // wait for the thread to be started let's wait a few ms + try { + Thread.sleep( 2 ); + } catch( InterruptedException ex ) { + // don t care + } + } + } + } + } + + private void stopDevicesListenerThread() { + synchronized( singleton ) { + inService = false; + } + } + + private void startMultiCastSocket() throws IOException { + int bindPort = Discovery.DEFAULT_SSDP_SEARCH_PORT; + String port = System.getProperty( "net.sbbi.upnp.Discovery.bindPort" ); + if ( port != null ) { + bindPort = Integer.parseInt( port ); + } + + skt = new java.net.MulticastSocket( null ); + skt.bind( new InetSocketAddress( InetAddress.getByName( "0.0.0.0" ), bindPort ) ); + skt.setTimeToLive( Discovery.DEFAULT_TTL ); + skt.setSoTimeout( DEFAULT_TIMEOUT ); + skt.joinGroup( InetAddress.getByName( Discovery.SSDP_IP ) ); + + byte[] buf = new byte[2048]; + input = new DatagramPacket( buf, buf.length ); + + } + + public void run() { + if ( !Thread.currentThread().getName().equals( "DiscoveryListener daemon" ) ) { + throw new RuntimeException( "No right to call this method" ); + } + inService = true; + while ( inService ) { + try { + listenBroadCast(); + } catch ( SocketTimeoutException ex ) { + // ignoring + } catch ( IOException ioEx ) { + log.error( "IO Exception during UPNP DiscoveryListener messages listening thread", ioEx ); + } catch( Exception ex ) { + log.error( "Fatal Error during UPNP DiscoveryListener messages listening thread, thread will exit", ex ); + inService = false; + } + } + + try { + skt.leaveGroup( InetAddress.getByName( Discovery.SSDP_IP ) ); + skt.close(); + } catch ( Exception ex ) { + // ignoring + } + } + + private void listenBroadCast() throws IOException { + + skt.receive( input ); + InetAddress from = input.getAddress(); + String received = new String( input.getData(), input.getOffset(), input.getLength() ); + HttpResponse msg = null; + try { + msg = new HttpResponse( received ); + } catch (IllegalArgumentException ex ) { + // crappy http sent + if ( log.isDebugEnabled() ) log.debug( "Skipping uncompliant HTTP message " + received ); + return; + } + String header = msg.getHeader(); + if ( header != null && header.startsWith( "HTTP/1.1 200 OK" ) && msg.getHTTPHeaderField( "st" ) != null ) { + // probably a search repsonse ! + String deviceDescrLoc = msg.getHTTPHeaderField( "location" ); + if( deviceDescrLoc == null || deviceDescrLoc.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'location' field" ); + return; + } + URL loc = new URL( deviceDescrLoc ); + if ( MATCH_IP ) { + InetAddress locHost = InetAddress.getByName( loc.getHost() ); + if ( !from.equals( locHost ) ) { + log.warn( "Discovery message sender IP " + from + + " does not match device description IP " + locHost + + " skipping device, set the net.sbbi.upnp.ddos.matchip system property" + + " to false to avoid this check" ); + return; + } + } + if ( log.isDebugEnabled() ) log.debug( "Processing " + deviceDescrLoc + " device description location" ); + String st = msg.getHTTPHeaderField( "st" ); + if( st == null || st.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'st' field" ); + return; + } + String usn = msg.getHTTPHeaderField( "usn" ); + if( usn == null || usn.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'usn' field" ); + return; + } + String maxAge = msg.getHTTPFieldElement( "Cache-Control", "max-age" ); + if( maxAge == null || maxAge.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'max-age' field" ); + return; + } + String server = msg.getHTTPHeaderField( "server" ); + if( server == null || server.trim().length() == 0 ) { + if ( log.isDebugEnabled() ) log.debug( "Skipping SSDP message, missing HTTP header 'server' field" ); + return; + } + + String udn = usn; + int index = udn.indexOf( "::" ); + if ( index != -1 ) udn = udn.substring( 0, index ); + synchronized( REGISTRATION_PROCESS ) { + Set handlers = (Set)registeredHandlers.get( st ); + if ( handlers != null ) { + for ( Iterator i = handlers.iterator(); i.hasNext(); ) { + DiscoveryResultsHandler handler = (DiscoveryResultsHandler)i.next(); + handler.discoveredDevice( usn, udn, st, maxAge, loc, server ); + } + } + } + } else { + if ( log.isDebugEnabled() ) log.debug( "Skipping uncompliant HTTP message " + received ); + } + } +} diff --git a/source/net/sbbi/upnp/DiscoveryResultsHandler.java b/source/net/sbbi/upnp/DiscoveryResultsHandler.java new file mode 100644 index 000000000..1074bcac5 --- /dev/null +++ b/source/net/sbbi/upnp/DiscoveryResultsHandler.java @@ -0,0 +1,71 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +/** + * This interface can be use to register against the DiscoveryListener class + * to receive SSDP search responses. + * @author SuperBonBon + * @version 1.0 + */ + +public interface DiscoveryResultsHandler { + + /** + * Method called by the DiscoveryListener class when a search response message has been received from the + * network + * @param usn the device USN + * @param udn the device UDN + * @param nt the device NT + * @param maxAge the message max age + * @param location the device location + * @param firmware the device firmware + */ + public void discoveredDevice( String usn, String udn, String nt, String maxAge, java.net.URL location, String firmware ); + +} diff --git a/source/net/sbbi/upnp/HttpResponse.java b/source/net/sbbi/upnp/HttpResponse.java new file mode 100644 index 000000000..183d79d03 --- /dev/null +++ b/source/net/sbbi/upnp/HttpResponse.java @@ -0,0 +1,145 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * A class to parse an HTTP response message. + * @author SuperBonBon + * @version 1.0 + */ + +public class HttpResponse { + + private String header; + private Map fields; + private String body; + + + /** + * Constructor of the response, will try to parse the raw response data + * @param rawHttpResponse the raw response data + * @throws IllegalArgumentException if some error occurs during parsing + */ + protected HttpResponse( String rawHttpResponse ) throws IllegalArgumentException { + if ( rawHttpResponse == null || rawHttpResponse.trim().length() == 0 ) { + throw new IllegalArgumentException( "Empty HTTP response message" ); + } + boolean bodyParsing = false; + StringBuffer bodyParsed = new StringBuffer(); + fields = new HashMap(); + String[] lines = rawHttpResponse.split( "\\r\\n" ); + this.header = lines[0].trim(); + + for ( int i = 1; i < lines.length; i++ ) { + + String line = lines[i]; + if ( line.length() == 0 ) { + // line break before body + bodyParsing = true; + } else if ( bodyParsing ) { + // we parse the message body + bodyParsed.append( line ).append( "\r\n" ); + } else { + // we parse the header + if ( line.length() > 0 ) { + int delim = line.indexOf( ':' ); + if ( delim != -1 ) { + String key = line.substring( 0, delim ).toUpperCase(); + String value = line.substring( delim + 1 ).trim(); + fields.put( key, value ); + } else { + throw new IllegalArgumentException( "Invalid HTTP message header :" + line ); + } + } + } + } + if ( bodyParsing ) { + body = bodyParsed.toString(); + } + } + + public String getHeader() { + return header; + } + + public String getBody() { + return body; + } + + public String getHTTPFieldElement( String fieldName, String elementName ) throws IllegalArgumentException { + String fieldNameValue = getHTTPHeaderField( fieldName ); + if ( fieldName!= null ) { + + StringTokenizer tokenizer = new StringTokenizer( fieldNameValue.trim(), "," ); + while (tokenizer.countTokens() > 0) { + String nextToken = tokenizer.nextToken().trim(); + if ( nextToken.startsWith( elementName ) ) { + int index = nextToken.indexOf( "=" ); + if ( index != -1 ) { + return nextToken.substring( index + 1 ).trim(); + } + } + } + } + throw new IllegalArgumentException( "HTTP element field " + elementName + " is not present" ); + } + + public String getHTTPHeaderField( String fieldName ) throws IllegalArgumentException { + String field = (String)fields.get( fieldName.toUpperCase() ); + if ( field == null ) { + throw new IllegalArgumentException( "HTTP field " + fieldName + " is not present"); + } + return field; + } + + +} diff --git a/source/net/sbbi/upnp/JXPathParser.java b/source/net/sbbi/upnp/JXPathParser.java new file mode 100644 index 000000000..d0aa8a37d --- /dev/null +++ b/source/net/sbbi/upnp/JXPathParser.java @@ -0,0 +1,95 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.jxpath.xml.DOMParser; +import org.apache.commons.jxpath.xml.XMLParser; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Parser to use with JXPath, this is used to fix some problems encountered + * with some UPNP devices returning buggy xml docs... + * This parser acts like a wrapper and make some chars search and replace + * such as 0x0 with 0x20 to produce a valid XML doc. + * @author SuperBonBon + * @version 1.0 + */ +public class JXPathParser implements XMLParser { + + private final static Log log = LogFactory.getLog( JXPathParser.class ); + + private char buggyChar = (char)0; + + public Object parseXML( InputStream in ){ + StringBuffer xml = new StringBuffer(); + try { + byte[] buffer = new byte[512]; + int readen = 0; + while ( ( readen = in.read( buffer ) ) != -1 ) { + xml.append( new String( buffer, 0, readen ) ); + } + } catch ( IOException ex ) { + log.error( "IOException occured during XML reception", ex ); + return null; + } + String doc = xml.toString(); + log.debug( "Readen raw xml doc:\n" + doc ); + if ( doc.indexOf( buggyChar ) != -1 ) { + doc = doc.replace( buggyChar, ' ' ); + } + + ByteArrayInputStream in2 = new ByteArrayInputStream( doc.getBytes() ); + DOMParser parser = new DOMParser(); + return parser.parseXML( in2 ) ; + } +} diff --git a/source/net/sbbi/upnp/ServiceEventHandler.java b/source/net/sbbi/upnp/ServiceEventHandler.java new file mode 100644 index 000000000..480626277 --- /dev/null +++ b/source/net/sbbi/upnp/ServiceEventHandler.java @@ -0,0 +1,70 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +/** + * Interface to implement to receive notifications about state + * variables changes on au UPNP service. The object implementing this interface + * can be used with the ServicesEventing class register method to receive the + * desired notifications. + * @author SuperBonBon + * @version 1.0 + */ + +public interface ServiceEventHandler { + + /** + * Handle a var change, called each time a UPNP service fires a + * state variable eventing message.
+ * The code implemented in this method can block the thread. + * @param varName the state variable name + * @param newValue the new state variable value + */ + public void handleStateVariableEvent( String varName, String newValue ); + +} diff --git a/source/net/sbbi/upnp/ServiceEventMessageParser.java b/source/net/sbbi/upnp/ServiceEventMessageParser.java new file mode 100644 index 000000000..36b000cd6 --- /dev/null +++ b/source/net/sbbi/upnp/ServiceEventMessageParser.java @@ -0,0 +1,105 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +import java.util.HashMap; +import java.util.Map; + +import org.xml.sax.Attributes; + +/** + * Simple SAX handler for UPNP service event message parsing, this message is in SOAP format + * @author SuperBonBon + * @version 1.0 + */ + +public class ServiceEventMessageParser extends org.xml.sax.helpers.DefaultHandler { + + private boolean readPropertyName = false; + + private String currentPropName = null; + + private Map changedStateVars = new HashMap(); + + protected ServiceEventMessageParser() { + } + + public Map getChangedStateVars() { + return changedStateVars; + } + + public void characters( char[] ch, int start, int length ) { + if ( currentPropName != null ) { + String origChars = (String)changedStateVars.get( currentPropName ); + String newChars = new String( ch, start, length ); + if ( origChars == null ) { + changedStateVars.put( currentPropName, newChars ); + } else { + changedStateVars.put( currentPropName, origChars + newChars ); + } + } + } + + public void startElement( String uri, String localName, String qName, Attributes attributes ) { + if ( localName.equals( "property" ) ) { + readPropertyName = true; + } else if ( readPropertyName ) { + currentPropName = localName; + } + } + + public void endElement( String uri, String localName, String qName ) { + if ( currentPropName != null && localName.equals( currentPropName ) ) { + readPropertyName = false; + currentPropName = null; + } + } + +} + + diff --git a/source/net/sbbi/upnp/ServiceEventSubscription.java b/source/net/sbbi/upnp/ServiceEventSubscription.java new file mode 100644 index 000000000..8fbfa3b7f --- /dev/null +++ b/source/net/sbbi/upnp/ServiceEventSubscription.java @@ -0,0 +1,111 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp; + +import java.net.InetAddress; +import java.net.URL; + +/** + * This class is used to provide information about a subscription done + * via the ServicesEventing class + * @author SuperBonBon + * @version 1.0 + */ + +public class ServiceEventSubscription { + + private String serviceType = null; + private String serviceId = null; + private URL serviceURL = null; + private String SID = null; + private InetAddress deviceIp = null; + private int durationTime = 0; + + public ServiceEventSubscription( String serviceType, String serviceId, URL serviceURL, + String sid, InetAddress deviceIp, int durationTime ) { + this.serviceType = serviceType; + this.serviceId = serviceId; + this.serviceURL = serviceURL; + SID = sid; + this.deviceIp = deviceIp; + this.durationTime = durationTime; + } + + public InetAddress getDeviceIp() { + return deviceIp; + } + + /** + * Subcription duration in seconds + * @return sub duration time, 0 for an infinite time + */ + public int getDurationTime() { + return durationTime; + } + + public String getServiceId() { + return serviceId; + } + + public String getServiceType() { + return serviceType; + } + + public URL getServiceURL() { + return serviceURL; + } + + /** + * The subscription ID returned by the UPNPDevice + * @return subscription id + */ + public String getSID() { + return SID; + } + +} diff --git a/source/net/sbbi/upnp/ServicesEventing.java b/source/net/sbbi/upnp/ServicesEventing.java new file mode 100644 index 000000000..8778192e0 --- /dev/null +++ b/source/net/sbbi/upnp/ServicesEventing.java @@ -0,0 +1,445 @@ +/* + * ============================================================================ + * 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 . + */ +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.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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 SuperBonBon + * @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: \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 ); + } + } + } +} diff --git a/source/net/sbbi/upnp/devices/DeviceIcon.java b/source/net/sbbi/upnp/devices/DeviceIcon.java new file mode 100644 index 000000000..7037a496b --- /dev/null +++ b/source/net/sbbi/upnp/devices/DeviceIcon.java @@ -0,0 +1,86 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.devices; + +import java.net.*; + +/** + * Java Bean for a device icon properties + * @author SuperBonBon + * @version 1.0 + */ + +public class DeviceIcon { + + protected String mimeType; + protected int width; + protected int height; + protected int depth; + protected URL url; + + public String getMimeType() { + return mimeType; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getDepth() { + return depth; + } + + public URL getUrl() { + return url; + } + +} diff --git a/source/net/sbbi/upnp/devices/UPNPDevice.java b/source/net/sbbi/upnp/devices/UPNPDevice.java new file mode 100644 index 000000000..c9f9e7450 --- /dev/null +++ b/source/net/sbbi/upnp/devices/UPNPDevice.java @@ -0,0 +1,298 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.devices; + +import net.sbbi.upnp.services.*; + +import java.util.*; +import java.net.*; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class represents an UPNP device, this device contains a set of services + * that will be needed to access the device functionalities. + * @author SuperBonBon + * @version 1.0 + */ + +public class UPNPDevice { + + private final static Log log = LogFactory.getLog( UPNPDevice.class ); + + protected String deviceType; + protected String friendlyName; + protected String manufacturer; + protected URL manufacturerURL; + protected URL presentationURL; + protected String modelDescription; + protected String modelName; + protected String modelNumber; + protected String modelURL; + protected String serialNumber; + protected String UDN; + protected String USN; + protected long UPC; + + protected List deviceIcons; + protected List services; + protected List childDevices; + + protected UPNPDevice parent; + + public URL getManufacturerURL() { + return manufacturerURL; + } + + /** + * Presentation URL + * @return URL the presenation URL, or null if the device does not provide + * such information + */ + public URL getPresentationURL() { + return presentationURL; + } + + public String getModelDescription() { + return modelDescription; + } + + public String getModelName() { + return modelName; + } + + public String getModelNumber() { + return modelNumber; + } + + public String getModelURL() { + return modelURL; + } + + public String getSerialNumber() { + return serialNumber; + } + + public String getUDN() { + return UDN; + } + + public String getUSN(){ + return USN; + } + + public long getUPC() { + return UPC; + } + + public String getDeviceType() { + return deviceType; + } + + public String getFriendlyName() { + return friendlyName; + } + + public String getManufacturer() { + return manufacturer; + } + + public boolean isRootDevice() { + return this instanceof UPNPRootDevice; + } + + /** + * Access to the device icons definitions + * @return a list containing DeviceIcon objects or null if no icons defined + */ + public List getDeviceIcons() { + return deviceIcons; + } + + /** + * Generates a list of all the child ( not only top level, full childrens hierarchy included ) + * UPNPDevice objects for this device. + * @return the generated list or null if no child devices bound + */ + public List getChildDevices() { + if ( childDevices == null ) return null; + List rtrVal = new ArrayList(); + for ( Iterator itr = childDevices.iterator(); itr.hasNext(); ) { + UPNPDevice device = (UPNPDevice)itr.next(); + rtrVal.add( device ); + List found = device.getChildDevices(); + if ( found != null ) { + rtrVal.addAll( found ); + } + } + return rtrVal; + + } + + /** + * Generates a list of all the child ( only top level ) + * UPNPDevice objects for this device. + * @return the generated list or null if no child devices bound + */ + public List getTopLevelChildDevices() { + if ( childDevices == null ) return null; + List rtrVal = new ArrayList(); + for ( Iterator itr = childDevices.iterator(); itr.hasNext(); ) { + UPNPDevice device = (UPNPDevice)itr.next(); + rtrVal.add( device ); + } + return rtrVal; + } + + /** + * Return the parent UPNPDevice, null if the device is an UPNPRootDevice + * @return the parent device instance + */ + public UPNPDevice getDirectParent() { + return parent; + } + + /** + * Looks for a child UPNP device definition file, + * the whole devices tree will be searched, starting from the current + * device node. + * @param deviceURI the device URI to search + * @return An UPNPDevice if anything matches or null + */ + public UPNPDevice getChildDevice( String deviceURI ) { + if ( log.isDebugEnabled() ) log.debug( "searching for device URI:" + deviceURI ); + if ( getDeviceType().equals( deviceURI ) ) return this; + if ( childDevices == null ) return null; + for ( Iterator itr = childDevices.iterator(); itr.hasNext(); ) { + UPNPDevice device = (UPNPDevice)itr.next(); + UPNPDevice found = device.getChildDevice( deviceURI ); + if ( found != null ) { + return found; + } + } + return null; + } + + /** + * Looks for all UPNP device service definitions objects + * @return A list of all device services + */ + public List getServices() { + if ( services == null ) return null; + List rtrVal = new ArrayList(); + rtrVal.addAll( services ); + return rtrVal; + } + + /** + * Looks for a UPNP device service definition object for the given service URI (Type) + * @param serviceURI the URI of the service + * @return A matching UPNPService object or null + */ + public UPNPService getService( String serviceURI ) { + if ( services == null ) return null; + if ( log.isDebugEnabled() ) log.debug( "searching for service URI:" + serviceURI ); + for ( Iterator itr = services.iterator(); itr.hasNext(); ) { + UPNPService service = (UPNPService)itr.next(); + if ( service.getServiceType().equals( serviceURI ) ) { + return service; + } + } + return null; + } + + /** + * Looks for a UPNP device service definition object for the given service ID + * @param serviceURI the ID of the service + * @return A matching UPNPService object or null + */ + public UPNPService getServiceByID( String serviceID ) { + if ( services == null ) return null; + if ( log.isDebugEnabled() ) log.debug( "searching for service ID:" + serviceID ); + for ( Iterator itr = services.iterator(); itr.hasNext(); ) { + UPNPService service = (UPNPService)itr.next(); + if ( service.getServiceId().equals( serviceID ) ) { + return service; + } + } + return null; + } + + /** + * Looks for the all the UPNP device service definition object for the current + * UPNP device object. This method can be used to retreive multiple same kind + * ( same service type ) of services with different services id on a device + * @param serviceURI the URI of the service + * @return A matching List of UPNPService objects or null + */ + public List getServices( String serviceURI ) { + if ( services == null ) return null; + List rtrVal = new ArrayList(); + if ( log.isDebugEnabled() ) log.debug( "searching for services URI:" + serviceURI ); + for ( Iterator itr = services.iterator(); itr.hasNext(); ) { + UPNPService service = (UPNPService)itr.next(); + if ( service.getServiceType().equals( serviceURI ) ) { + rtrVal.add( service ); + } + } + if ( rtrVal.size() == 0 ) { + return null; + } + return rtrVal; + } + + /** + * The toString return the device type + * @return the device type + */ + public String toString() { + return getDeviceType(); + } + +} diff --git a/source/net/sbbi/upnp/devices/UPNPRootDevice.java b/source/net/sbbi/upnp/devices/UPNPRootDevice.java new file mode 100644 index 000000000..f11a31e0e --- /dev/null +++ b/source/net/sbbi/upnp/devices/UPNPRootDevice.java @@ -0,0 +1,450 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.devices; + +import java.net.*; +import java.util.*; +import java.io.*; + +import org.apache.commons.jxpath.*; +import org.apache.commons.jxpath.xml.*; +import org.apache.commons.logging.*; + +import net.sbbi.upnp.JXPathParser; +import net.sbbi.upnp.services.*; + +/** + * Root UPNP device that is contained in a device definition file. + * Slightly differs from a simple UPNPDevice object. + * This object will contains all the child devices, this is the top + * objet in the UPNP device devices hierarchy. + * @author SuperBonBon + * @version 1.0 + */ + +public class UPNPRootDevice extends UPNPDevice { + + private final static Log log = LogFactory.getLog( UPNPRootDevice.class ); + + private int specVersionMajor; + private int specVersionMinor; + private URL URLBase; + private long validityTime; + private long creationTime; + private URL deviceDefLoc; + private String deviceDefLocData; + private String vendorFirmware; + private String discoveryUSN; + private String discoveryUDN; + + private DocumentContainer UPNPDevice; + + /** + * Constructor for the root device, constructs itself from + * An xml device definition file provided by the UPNP device via http normally. + * @param deviceDefLoc the location of the XML device definition file + * using "the urn:schemas-upnp-org:device-1-0" namespace + * @param maxAge the maximum age of this UPNP device in secs before considered to be outdated + * @param vendorFirmware the vendor firmware + * @param discoveryUSN the discovery USN used to find and create this device + * @param discoveryUDN the discovery UDN used to find and create this device + * @throws MalformedURLException if the location URL is invalid and cannot be used to populate this root object and its child devices + * IllegalStateException if the device has an unsupported version, currently only version 1.0 is supported + */ + public UPNPRootDevice( URL deviceDefLoc, String maxAge, String vendorFirmware, String discoveryUSN, String discoveryUDN ) throws MalformedURLException, IllegalStateException { + this( deviceDefLoc, maxAge ); + this.vendorFirmware = vendorFirmware; + this.discoveryUSN = discoveryUSN; + this.discoveryUDN = discoveryUDN; + } + + /** + * Constructor for the root device, constructs itself from + * An xml device definition file provided by the UPNP device via http normally. + * @param deviceDefLoc the location of the XML device definition file + * using "the urn:schemas-upnp-org:device-1-0" namespace + * @param maxAge the maximum age of this UPNP device in secs before considered to be outdated + * @param vendorFirmware the vendor firmware + * @throws MalformedURLException if the location URL is invalid and cannot be used to populate this root object and its child devices + * IllegalStateException if the device has an unsupported version, currently only version 1.0 is supported + */ + public UPNPRootDevice( URL deviceDefLoc, String maxAge, String vendorFirmware ) throws MalformedURLException, IllegalStateException { + this( deviceDefLoc, maxAge ); + this.vendorFirmware = vendorFirmware; + } + + /** + * Constructor for the root device, constructs itself from + * An xml device definition file provided by the UPNP device via http normally. + * @param deviceDefLoc the location of the XML device definition file + * using "the urn:schemas-upnp-org:device-1-0" namespace + * @param maxAge the maximum age in secs of this UPNP device before considered to be outdated + * @throws MalformedURLException if the location URL is invalid and cannot be used to populate this root object and its child devices + * IllegalStateException if the device has an unsupported version, currently only version 1.0 is supported + */ + public UPNPRootDevice( URL deviceDefLoc, String maxAge ) throws MalformedURLException, IllegalStateException { + this.deviceDefLoc = deviceDefLoc; + DocumentContainer.registerXMLParser( DocumentContainer.MODEL_DOM, new JXPathParser() ); + UPNPDevice = new DocumentContainer( deviceDefLoc, DocumentContainer.MODEL_DOM ); + validityTime = Integer.parseInt( maxAge ) * 1000; + creationTime = System.currentTimeMillis(); + + JXPathContext context = JXPathContext.newContext( this ); + Pointer rootPtr = context.getPointer( "UPNPDevice/root" ); + JXPathContext rootCtx = context.getRelativeContext( rootPtr ); + + specVersionMajor = Integer.parseInt( (String)rootCtx.getValue( "specVersion/major" ) ); + specVersionMinor = Integer.parseInt( (String)rootCtx.getValue( "specVersion/minor" ) ); + + if ( !( specVersionMajor == 1 && specVersionMinor == 0 ) ) { + throw new IllegalStateException( "Unsupported device version (" + specVersionMajor + "." + specVersionMinor + ")" ); + } + boolean buildURLBase = true; + String base = null; + try { + base = (String)rootCtx.getValue( "URLBase" ); + if ( base != null && base.trim().length() > 0 ) { + URLBase = new URL( base ); + if ( log.isDebugEnabled() ) log.debug( "device URLBase " + URLBase ); + buildURLBase = false; + } + } catch ( JXPathException ex ) { + // URLBase is not mandatory we assume we use the URL of the device + } catch ( MalformedURLException malformedEx ) { + // crappy urlbase provided + log.warn( "Error occured during device baseURL " + base + " parsing, building it from device default location", malformedEx ); + } + if ( buildURLBase ) { + String URL = deviceDefLoc.getProtocol() + "://" + deviceDefLoc.getHost() + ":" + deviceDefLoc.getPort(); + String path = deviceDefLoc.getPath(); + if ( path != null ) { + int lastSlash = path.lastIndexOf( '/' ); + if ( lastSlash != -1 ) { + URL += path.substring( 0, lastSlash ); + } + } + URLBase = new URL( URL ); + } + Pointer devicePtr = rootCtx.getPointer( "device" ); + JXPathContext deviceCtx = rootCtx.getRelativeContext( devicePtr ); + + fillUPNPDevice( this, null, deviceCtx, URLBase ); + } + + /** + * The validity time for this device in milliseconds, + * @return the number of milliseconds remaining before the device object that has been build is considered to + * be outdated, after this delay the UPNP device should resend an advertisement message or a negative value + * if the device is outdated + */ + public long getValidityTime() { + long elapsed = System.currentTimeMillis() - creationTime; + return validityTime - elapsed; + } + + /** + * Resets the device validity time + * @param newMaxAge the maximum age in secs of this UPNP device before considered to be outdated + */ + public void resetValidityTime( String newMaxAge ) { + validityTime = Integer.parseInt( newMaxAge ) * 1000; + creationTime = System.currentTimeMillis(); + } + + /** + * Retreives the device description file location + * @return an URL + */ + public URL getDeviceDefLoc() { + return deviceDefLoc; + } + + public int getSpecVersionMajor() { + return specVersionMajor; + } + + public int getSpecVersionMinor() { + return specVersionMinor; + } + + public String getVendorFirmware() { + return vendorFirmware; + } + + public String getDiscoveryUSN() { + return discoveryUSN; + } + + public String getDiscoveryUDN() { + return discoveryUDN; + } + + /** + * URL base acces + * @return URL the URL base, or null if the device does not provide + * such information + */ + public URL getURLBase() { + return URLBase; + } + + /** + * Parsing an UPNPdevice description element () in the description XML file + * @param device the device object that will be populated + * @param parent the device parent object + * @param deviceCtx an XPath context for object population + * @param baseURL the base URL of the UPNP device + * @throws MalformedURLException if some URL provided in the description file is invalid + */ + private void fillUPNPDevice( UPNPDevice device, UPNPDevice parent, JXPathContext deviceCtx, URL baseURL ) throws MalformedURLException { + + device.deviceType = getMandatoryData( deviceCtx, "deviceType" ); + if ( log.isDebugEnabled() ) log.debug( "parsing device " + device.deviceType ); + device.friendlyName = getMandatoryData( deviceCtx, "friendlyName" ); + device.manufacturer = getNonMandatoryData( deviceCtx, "manufacturer" ); + String base = getNonMandatoryData( deviceCtx, "manufacturerURL" ); + try { + if ( base != null ) device.manufacturerURL = new URL( base ); + } catch ( java.net.MalformedURLException ex ) { + // crappy data provided, keep the field null + } + try { + device.presentationURL = getURL( getNonMandatoryData( deviceCtx, "presentationURL" ), URLBase ); + } catch ( java.net.MalformedURLException ex ) { + // crappy data provided, keep the field null + } + device.modelDescription = getNonMandatoryData( deviceCtx, "modelDescription" ); + device.modelName = getMandatoryData( deviceCtx, "modelName" ); + device.modelNumber = getNonMandatoryData( deviceCtx, "modelNumber" ); + device.modelURL = getNonMandatoryData( deviceCtx, "modelURL" ); + device.serialNumber = getNonMandatoryData( deviceCtx, "serialNumber" ); + device.UDN = getMandatoryData( deviceCtx, "UDN" ); + device.USN = UDN.concat( "::" ).concat( deviceType ); + String tmp = getNonMandatoryData( deviceCtx, "UPC" ); + if ( tmp != null ) { + try { + device.UPC = Long.parseLong( tmp ); + } catch ( Exception ex ) { + // non all numeric field provided, non upnp compliant device + } + } + device.parent = parent; + + fillUPNPServicesList( device, deviceCtx ); + fillUPNPDeviceIconsList( device, deviceCtx, URLBase ); + + Pointer deviceListPtr; + try { + deviceListPtr = deviceCtx.getPointer( "deviceList" ); + } catch ( JXPathException ex ) { + // no pointers for this device list, this can happen + // if the device has no child devices, simply returning + return; + } + JXPathContext deviceListCtx = deviceCtx.getRelativeContext( deviceListPtr ); + Double arraySize = (Double)deviceListCtx.getValue( "count( device )" ); + device.childDevices = new ArrayList(); + if ( log.isDebugEnabled() ) log.debug( "child devices count is " + arraySize ); + for ( int i = 1; i <= arraySize.intValue(); i++ ) { + Pointer devicePtr = deviceListCtx.getPointer( "device[" + i + "]" ); + JXPathContext childDeviceCtx = deviceListCtx.getRelativeContext( devicePtr ); + UPNPDevice childDevice = new UPNPDevice(); + fillUPNPDevice( childDevice, device, childDeviceCtx, baseURL ); + if ( log.isDebugEnabled() ) log.debug( "adding child device " + childDevice.getDeviceType() ); + device.childDevices.add( childDevice ); + } + } + + private String getMandatoryData( JXPathContext ctx, String ctxFieldName ) { + String value = (String)ctx.getValue( ctxFieldName ); + if ( value != null && value.length() == 0 ) { + throw new JXPathException( "Mandatory field " + ctxFieldName + " not provided, uncompliant UPNP device !!" ); + } + return value; + } + + private String getNonMandatoryData( JXPathContext ctx, String ctxFieldName ) { + String value = null; + try { + value = (String)ctx.getValue( ctxFieldName ); + if ( value != null && value.length() == 0 ) { + value = null; + } + } catch ( JXPathException ex ) { + value = null; + } + return value; + } + + /** + * Parsing an UPNPdevice services list element () in the description XML file + * @param device the device object that will store the services list (UPNPService) objects + * @param deviceCtx an XPath context for object population + * @throws MalformedURLException if some URL provided in the description + * file for a service entry is invalid + */ + private void fillUPNPServicesList( UPNPDevice device, JXPathContext deviceCtx ) throws MalformedURLException { + Pointer serviceListPtr = deviceCtx.getPointer( "serviceList" ); + JXPathContext serviceListCtx = deviceCtx.getRelativeContext( serviceListPtr ); + Double arraySize = (Double)serviceListCtx.getValue( "count( service )" ); + if ( log.isDebugEnabled() ) log.debug( "device services count is " + arraySize ); + device.services = new ArrayList(); + for ( int i = 1; i <= arraySize.intValue(); i++ ) { + + Pointer servicePtr = serviceListCtx.getPointer( "service["+i+"]" ); + JXPathContext serviceCtx = serviceListCtx.getRelativeContext( servicePtr ); + // TODO possibility of bugs if deviceDefLoc contains a file name + URL base = URLBase != null ? URLBase : deviceDefLoc; + UPNPService service = new UPNPService( serviceCtx, base, this ); + device.services.add( service ); + } + } + + /** + * Parsing an UPNPdevice icons list element () in the description XML file + * This list can be null + * @param device the device object that will store the icons list (DeviceIcon) objects + * @param deviceCtx an XPath context for object population + * @throws MalformedURLException if some URL provided in the description + * file for an icon URL + */ + private void fillUPNPDeviceIconsList( UPNPDevice device, JXPathContext deviceCtx, URL baseURL ) throws MalformedURLException { + Pointer iconListPtr; + try { + iconListPtr = deviceCtx.getPointer( "iconList" ); + } catch ( JXPathException ex ) { + // no pointers for icons list, this can happen + // simply returning + return; + } + JXPathContext iconListCtx = deviceCtx.getRelativeContext( iconListPtr ); + Double arraySize = (Double)iconListCtx.getValue( "count( icon )" ); + if ( log.isDebugEnabled() ) log.debug( "device icons count is " + arraySize ); + device.deviceIcons = new ArrayList(); + for ( int i = 1; i <= arraySize.intValue(); i++ ) { + + DeviceIcon ico = new DeviceIcon(); + ico.mimeType = (String)iconListCtx.getValue( "icon["+i+"]/mimetype" ); + ico.width = Integer.parseInt( (String)iconListCtx.getValue( "icon["+i+"]/width" ) ); + ico.height = Integer.parseInt( (String)iconListCtx.getValue( "icon["+i+"]/height" ) ); + ico.depth = Integer.parseInt( (String)iconListCtx.getValue( "icon["+i+"]/depth" ) ); + ico.url = getURL( (String)iconListCtx.getValue( "icon["+i+"]/url" ), baseURL ); + if ( log.isDebugEnabled() ) log.debug( "icon URL is " + ico.url ); + device.deviceIcons.add( ico ); + } + } + + /** + * Parsing an URL from the descriptionXML file + * @param url the string representation fo the URL + * @param baseURL the base device URL, needed if the url param is relative + * @return an URL object defining the url param + * @throws MalformedURLException if the url param or baseURL.toExternalForm() + url + * cannot be parsed to create an URL object + */ + public final static URL getURL( String url, URL baseURL ) throws MalformedURLException { + URL rtrVal; + if ( url == null || url.trim().length() == 0 ) return null; + try { + rtrVal = new URL( url ); + } catch ( MalformedURLException malEx ) { + // maybe that the url is relative, we add the baseURL and reparse it + // if relative then we take the device baser url root and add the url + if ( baseURL != null ) { + url = url.replace( '\\', '/' ); + if ( url.charAt( 0 ) != '/' ) { + // the path is relative to the device baseURL + String externalForm = baseURL.toExternalForm(); + if ( !externalForm.endsWith( "/" ) ) { + externalForm += "/"; + } + rtrVal = new URL( externalForm + url ); + } else { + // the path is not relative + String URLRoot = baseURL.getProtocol() + "://" + baseURL.getHost() + ":" + baseURL.getPort(); + rtrVal = new URL( URLRoot + url ); + } + } else { + throw malEx; + } + } + return rtrVal; + } + + /** + * Retrieves the device definition XML data + * @return the device definition XML data as a String + */ + public String getDeviceDefLocData() { + if ( deviceDefLocData == null ) { + try { + java.io.InputStream in = deviceDefLoc.openConnection().getInputStream(); + int readen = 0; + byte[] buff = new byte[512]; + StringBuffer strBuff = new StringBuffer(); + while( ( readen = in.read( buff ) ) != -1 ) { + strBuff.append( new String( buff, 0, readen ) ); + } + in.close(); + deviceDefLocData = strBuff.toString(); + } catch ( IOException ioEx ) { + return null; + } + } + return deviceDefLocData; + } + + /** + * Used for JXPath parsing, do not use this method + * @return a Container object for Xpath parsing capabilities + */ + public Container getUPNPDevice() { + return UPNPDevice; + } + +} diff --git a/source/net/sbbi/upnp/impls/InternetGatewayDevice.java b/source/net/sbbi/upnp/impls/InternetGatewayDevice.java new file mode 100644 index 000000000..a82b2ac3c --- /dev/null +++ b/source/net/sbbi/upnp/impls/InternetGatewayDevice.java @@ -0,0 +1,506 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.impls; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import net.sbbi.upnp.Discovery; +import net.sbbi.upnp.devices.UPNPDevice; +import net.sbbi.upnp.devices.UPNPRootDevice; +import net.sbbi.upnp.messages.ActionMessage; +import net.sbbi.upnp.messages.ActionResponse; +import net.sbbi.upnp.messages.StateVariableMessage; +import net.sbbi.upnp.messages.StateVariableResponse; +import net.sbbi.upnp.messages.UPNPMessageFactory; +import net.sbbi.upnp.messages.UPNPResponseException; +import net.sbbi.upnp.services.UPNPService; + +/** + * This class can be used to access some funtionalities on the + * InternetGatewayDevice on your network without having to know + * anything about the required input/output parameters. + * All device functions are not provided. + * @author SuperBonBon + * @version 1.0 + */ +public class InternetGatewayDevice { + + private final static Log log = LogFactory.getLog( InternetGatewayDevice.class ); + + private UPNPRootDevice igd; + private UPNPMessageFactory msgFactory; + + public InternetGatewayDevice( UPNPRootDevice igd ) throws UnsupportedOperationException { + this( igd, true, true ); + } + + private InternetGatewayDevice( UPNPRootDevice igd, boolean WANIPConnection, boolean WANPPPConnection ) throws UnsupportedOperationException { + this.igd = igd; + UPNPDevice myIGDWANConnDevice = igd.getChildDevice( "urn:schemas-upnp-org:device:WANConnectionDevice:1" ); + if ( myIGDWANConnDevice == null ) { + throw new UnsupportedOperationException( "device urn:schemas-upnp-org:device:WANConnectionDevice:1 not supported by IGD device " + igd.getModelName() ); + } + + UPNPService wanIPSrv = myIGDWANConnDevice.getService( "urn:schemas-upnp-org:service:WANIPConnection:1" ); + UPNPService wanPPPSrv = myIGDWANConnDevice.getService( "urn:schemas-upnp-org:service:WANPPPConnection:1" ); + + if ( ( WANIPConnection && WANPPPConnection ) && ( wanIPSrv == null && wanPPPSrv == null ) ) { + throw new UnsupportedOperationException( "Unable to find any urn:schemas-upnp-org:service:WANIPConnection:1 or urn:schemas-upnp-org:service:WANPPPConnection:1 service" ); + } else if ( ( WANIPConnection && !WANPPPConnection ) && wanIPSrv == null ) { + throw new UnsupportedOperationException( "Unable to find any urn:schemas-upnp-org:service:WANIPConnection:1 service" ); + } else if ( ( !WANIPConnection && WANPPPConnection ) && wanPPPSrv == null ) { + throw new UnsupportedOperationException( "Unable to find any urn:schemas-upnp-org:service:WANPPPConnection:1 service" ); + } + + if ( wanIPSrv != null && wanPPPSrv == null ) { + msgFactory = UPNPMessageFactory.getNewInstance( wanIPSrv ); + } else if ( wanPPPSrv != null && wanIPSrv == null ) { + msgFactory = UPNPMessageFactory.getNewInstance( wanPPPSrv ); + } else { + // Unable to test the following code since no router implementing both IP and PPP connection on hands.. + /*// discover the active WAN interface using the WANCommonInterfaceConfig specs + UPNPDevice wanDevice = igd.getChildDevice( "urn:schemas-upnp-org:device:WANDevice:1" ); + UPNPService configService = wanDevice.getService( "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" ); + if ( configService != null ) { + // retreive the first active connection + ServiceAction act = configService.getUPNPServiceAction( "GetActiveConnection" ); + if ( act != null ) { + UPNPMessageFactory msg = UPNPMessageFactory.getNewInstance( configService ); + String deviceContainer = null; + String serviceID = null; + try { + // always lookup for the first index of active connections. + ActionResponse resp = msg.getMessage( "GetActiveConnection" ).setInputParameter( "NewActiveConnectionIndex", 0 ).service(); + deviceContainer = resp.getOutActionArgumentValue( "NewActiveConnDeviceContainer" ); + serviceID = resp.getOutActionArgumentValue( "NewActiveConnectionServiceID" ); + } catch ( IOException ex ) { + // no response returned + } catch ( UPNPResponseException respEx ) { + // should never happen unless the damn thing is bugged + } + if ( deviceContainer != null && deviceContainer.trim().length() > 0 && + serviceID != null && serviceID.trim().length() > 0 ) { + for ( Iterator i = igd.getChildDevices().iterator(); i.hasNext(); ) { + UPNPDevice dv = (UPNPDevice)i.next(); + + if ( deviceContainer.startsWith( dv.getUDN() ) && + dv.getDeviceType().indexOf( ":WANConnectionDevice:" ) != -1 ) { + myIGDWANConnDevice = dv; + break; + } + } + msgFactory = UPNPMessageFactory.getNewInstance( myIGDWANConnDevice.getServiceByID( serviceID ) ); + } + } + }*/ + // Doing a tricky test with external IP address, the unactive interface should return a null value or none + if ( testWANInterface( wanIPSrv ) ) { + msgFactory = UPNPMessageFactory.getNewInstance( wanIPSrv ); + } else if( testWANInterface( wanPPPSrv ) ) { + msgFactory = UPNPMessageFactory.getNewInstance( wanPPPSrv ); + } + if ( msgFactory == null ) { + // Nothing found using WANCommonInterfaceConfig! IP by default + log.warn( "Unable to detect active WANIPConnection, dfaulting to urn:schemas-upnp-org:service:WANIPConnection:1" ); + msgFactory = UPNPMessageFactory.getNewInstance( wanIPSrv ); + } + } + } + + private boolean testWANInterface( UPNPService srv ) { + UPNPMessageFactory tmp = UPNPMessageFactory.getNewInstance( srv ); + + ActionMessage msg = tmp.getMessage( "GetExternalIPAddress" ); + String ipToParse = null; + try { + ipToParse = msg.service().getOutActionArgumentValue( "NewExternalIPAddress" ); + } catch ( UPNPResponseException ex ) { + // ok probably not the IP interface + } catch ( IOException ex ) { + // not really normal + log.warn( "IOException occured during device detection", ex ); + } + if ( ipToParse != null && ipToParse.length() > 0 && !ipToParse.equals( "0.0.0.0" ) ) { + try { + return InetAddress.getByName( ipToParse ) != null; + } catch ( UnknownHostException ex ) { + // ok a crappy IP provided, definitly the wrong interface.. + } + } + return false; + } + + /** + * Retreives the IDG UNPNRootDevice object + * @return the UNPNRootDevie object bound to this object + */ + public UPNPRootDevice getIGDRootDevice() { + return igd; + } + + /** + * Lookup all the IGD (IP or PPP) devices on the network. If a device implements both + * IP and PPP, the active service will be used for nat mappings. + * @param timeout the timeout in ms to listen for devices response, -1 for default value + * @return an array of devices to play with or null if nothing found. + * @throws IOException if some IO Exception occurs during discovery + */ + public static InternetGatewayDevice[] getDevices( int timeout ) throws IOException { + return lookupDeviceDevices( timeout, Discovery.DEFAULT_TTL, Discovery.DEFAULT_MX, true, true, null ); + } + + /** + * Lookup all the IGD (IP urn:schemas-upnp-org:service:WANIPConnection:1, or PPP urn:schemas-upnp-org:service:WANPPPConnection:1) + * devices for a given network interface. If a device implements both + * IP and PPP, the active service will be used for nat mappings. + * @param timeout the timeout in ms to listen for devices response, -1 for default value + * @param ttl the discovery ttl such as {@link net.sbbi.upnp.Discovery#DEFAULT_TTL} + * @param mx the discovery mx such as {@link net.sbbi.upnp.Discovery#DEFAULT_MX} + * @param ni the network interface where to lookup IGD devices + * @return an array of devices to play with or null if nothing found. + * @throws IOException if some IO Exception occurs during discovery + */ + public static InternetGatewayDevice[] getDevices( int timeout, int ttl, int mx, NetworkInterface ni ) throws IOException { + return lookupDeviceDevices( timeout, ttl, mx, true, true, ni ); + } + + /** + * Lookup all the IGD IP devices on the network (urn:schemas-upnp-org:service:WANIPConnection:1 service) + * @param timeout the timeout in ms to listen for devices response, -1 for default value + * @return an array of devices to play with or null if nothing found or if found devices + * do not have the urn:schemas-upnp-org:service:WANIPConnection:1 service + * @deprecated use generic {@link #getDevices(int)} or {@link #getDevices(int, int, int, NetworkInterface)} methods since this one is not + * usable with all IGD devices ( will only work with devices implementing the urn:schemas-upnp-org:service:WANIPConnection:1 service ) + */ + public static InternetGatewayDevice[] getIPDevices( int timeout ) throws IOException { + return lookupDeviceDevices( timeout, Discovery.DEFAULT_TTL, Discovery.DEFAULT_MX, true, false, null ); + } + + /** + * Lookup all the IGD PPP devices on the network (urn:schemas-upnp-org:service:WANPPPConnection:1 service) + * @param timeout the timeout in ms to listen for devices response, -1 for default value + * @return an array of devices to play with or null if nothing found or if found devices + * do not have the urn:schemas-upnp-org:service:WANPPPConnection:1 service + * @deprecated use generic {@link #getDevices(int)} or {@link #getDevices(int, int, int, NetworkInterface)} methods since this one is not + * usable with all IGD devices ( will only work with devices implementing the urn:schemas-upnp-org:service:WANPPPConnection:1 service ) + */ + public static InternetGatewayDevice[] getPPPDevices( int timeout ) throws IOException { + return lookupDeviceDevices( timeout, Discovery.DEFAULT_TTL, Discovery.DEFAULT_MX, false, true, null ); + } + + private static InternetGatewayDevice[] lookupDeviceDevices( int timeout, int ttl, int mx, boolean WANIPConnection, boolean WANPPPConnection, NetworkInterface ni ) throws IOException { + UPNPRootDevice[] devices = null; + InternetGatewayDevice[] rtrVal = null; + if ( timeout == -1 ) { + devices = Discovery.discover( Discovery.DEFAULT_TIMEOUT, ttl, mx, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ni ); + } else { + devices = Discovery.discover( timeout, ttl, mx, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ni ); + } + + if ( devices != null ) { + Set valid = new HashSet(); + for ( int i = 0; i < devices.length; i++ ) { + try { + valid.add( new InternetGatewayDevice( devices[i], WANIPConnection, WANPPPConnection ) ); + } catch ( UnsupportedOperationException ex ) { + // the device is either not IP or PPP + if ( log.isDebugEnabled() ) log.debug( "UnsupportedOperationException during discovery " + ex.getMessage() ); + } + } + if ( valid.size() == 0 ) { + return null; + } + rtrVal = new InternetGatewayDevice[valid.size()]; + int i = 0; + for ( Iterator itr = valid.iterator(); itr.hasNext(); ) { + rtrVal[i++] = (InternetGatewayDevice)itr.next(); + } + + } + return rtrVal; + } + + /** + * Retreives the external IP address + * @return a String representing the external IP + * @throws UPNPResponseException if the devices returns an error code + * @throws IOException if some error occurs during communication with the device + */ + public String getExternalIPAddress() throws UPNPResponseException, IOException { + ActionMessage msg = msgFactory.getMessage( "GetExternalIPAddress" ); + return msg.service().getOutActionArgumentValue( "NewExternalIPAddress" ); + } + + /** + * Retreives a generic port mapping entry. + * @param newPortMappingIndex the index to lookup in the nat table of the upnp device + * @return an action response Object containing the following fields : + * NewRemoteHost, NewExternalPort, NewProtocol, NewInternalPort, + * NewInternalClient, NewEnabled, NewPortMappingDescription, NewLeaseDuration or null if the index does not exists + * @throws IOException if some error occurs during communication with the device + * @throws UPNPResponseException if some unexpected error occurs on the UPNP device + */ + public ActionResponse getGenericPortMappingEntry( int newPortMappingIndex ) throws IOException, UPNPResponseException { + + ActionMessage msg = msgFactory.getMessage( "GetGenericPortMappingEntry" ); + msg.setInputParameter( "NewPortMappingIndex", newPortMappingIndex ); + + try { + return msg.service(); + } catch ( UPNPResponseException ex ) { + if ( ex.getDetailErrorCode() == 714 ) { + return null; + } + throw ex; + } + + } + + /** + * Retreives information about a specific port mapping + * @param remoteHost the remote host ip to check, null if wildcard + * @param externalPort the port to check + * @param protocol the protocol for the mapping, either TCP or UDP + * @return an action response Object containing the following fields : + * NewInternalPort, NewInternalClient, NewEnabled, NewPortMappingDescription, NewLeaseDuration or + * null if no such entry exists in the device NAT table + * @throws IOException if some error occurs during communication with the device + * @throws UPNPResponseException if some unexpected error occurs on the UPNP device + */ + public ActionResponse getSpecificPortMappingEntry( String remoteHost, int externalPort, String protocol ) throws IOException, UPNPResponseException { + remoteHost = remoteHost == null ? "" : remoteHost; + checkPortMappingProtocol( protocol ); + checkPortRange( externalPort ); + + ActionMessage msg = msgFactory.getMessage( "GetSpecificPortMappingEntry" ); + msg.setInputParameter( "NewRemoteHost", remoteHost ) + .setInputParameter( "NewExternalPort", externalPort ) + .setInputParameter( "NewProtocol", protocol ); + + try { + return msg.service(); + } catch ( UPNPResponseException ex ) { + if ( ex.getDetailErrorCode() == 714 ) { + return null; + } + throw ex; + } + } + + /** + * Configures a nat entry on the UPNP device. + * @param description the mapping description, null for no description + * @param remoteHost the remote host ip for this entry, null for a wildcard value + * @param internalPort the internal client port where data should be redirected + * @param externalPort the external port to open on the UPNP device an map on the internal client, 0 for a wildcard value + * @param internalClient the internal client ip where data should be redirected + * @param leaseDuration the lease duration in seconds 0 for an infinite time + * @param protocol the protocol, either TCP or UDP + * @return true if the port is mapped false if the mapping is allready done for another internal client + * @throws IOException if some error occurs during communication with the device + * @throws UPNPResponseException if the device does not accept some settings :
+ * 402 Invalid Args See UPnP Device Architecture section on Control
+ * 501 Action Failed See UPnP Device Architecture section on Control
+ * 715 WildCardNotPermittedInSrcIP The source IP address cannot be wild-carded
+ * 716 WildCardNotPermittedInExtPort The external port cannot be wild-carded
+ * 724 SamePortValuesRequired Internal and External port values must be the same
+ * 725 OnlyPermanentLeasesSupported The NAT implementation only supports permanent lease times on port mappings
+ * 726 RemoteHostOnlySupportsWildcard RemoteHost must be a wildcard and cannot be a specific IP address or DNS name
+ * 727 ExternalPortOnlySupportsWildcard ExternalPort must be a wildcard and cannot be a specific port value + */ + public boolean addPortMapping( String description, String remoteHost, + int internalPort, int externalPort, + String internalClient, int leaseDuration, + String protocol ) throws IOException, UPNPResponseException { + remoteHost = remoteHost == null ? "" : remoteHost; + checkPortMappingProtocol( protocol ); + if ( externalPort != 0 ) { + checkPortRange( externalPort ); + } + checkPortRange( internalPort ); + description = description == null ? "" : description; + if ( leaseDuration < 0 ) throw new IllegalArgumentException( "Invalid leaseDuration (" + leaseDuration + ") value" ); + + ActionMessage msg = msgFactory.getMessage( "AddPortMapping" ); + msg.setInputParameter( "NewRemoteHost", remoteHost ) + .setInputParameter( "NewExternalPort", externalPort ) + .setInputParameter( "NewProtocol", protocol ) + .setInputParameter( "NewInternalPort", internalPort ) + .setInputParameter( "NewInternalClient", internalClient ) + .setInputParameter( "NewEnabled", true ) + .setInputParameter( "NewPortMappingDescription", description ) + .setInputParameter( "NewLeaseDuration", leaseDuration ); + try { + msg.service(); + return true; + } catch ( UPNPResponseException ex ) { + if ( ex.getDetailErrorCode() == 718 ) { + return false; + } + throw ex; + } + } + + /** + * Deletes a port mapping on the IDG device + * @param remoteHost the host ip for which the mapping was done, null value for a wildcard value + * @param externalPort the port to close + * @param protocol the protocol for the mapping, TCP or UDP + * @return true if the port has been unmapped correctly otherwise false ( entry does not exists ). + * @throws IOException if some error occurs during communication with the device + * @throws UPNPResponseException if the devices returns an error message + */ + public boolean deletePortMapping( String remoteHost, int externalPort, String protocol ) throws IOException, UPNPResponseException { + + remoteHost = remoteHost == null ? "" : remoteHost; + checkPortMappingProtocol( protocol ); + checkPortRange( externalPort ); + ActionMessage msg = msgFactory.getMessage( "DeletePortMapping" ); + msg.setInputParameter( "NewRemoteHost", remoteHost ) + .setInputParameter( "NewExternalPort", externalPort ) + .setInputParameter( "NewProtocol", protocol ); + try { + msg.service(); + return true; + } catch ( UPNPResponseException ex ) { + if ( ex.getDetailErrorCode() == 714 ) { + return false; + } + throw ex; + } + } + + /** + * Retreives the current number of mapping in the NAT table + * @return the nat table current number of mappings or null if the device does not allow to query state variables + * @throws IOException if some error occurs during communication with the device + * @throws UPNPResponseException if the devices returns an error message with error code other than 404 + */ + public Integer getNatMappingsCount() throws IOException, UPNPResponseException { + + Integer rtrval = null; + StateVariableMessage natTableSize = msgFactory.getStateVariableMessage( "PortMappingNumberOfEntries" ); + try { + StateVariableResponse resp = natTableSize.service(); + rtrval = new Integer( resp.getStateVariableValue() ); + } catch ( UPNPResponseException ex ) { + // 404 can happen if device do not implement state variables queries + if ( ex.getDetailErrorCode() != 404 ) { + throw ex; + } + } + return rtrval; + } + + /** + * Computes the total entries in avaliable in the nat table size, not that this method is not guaranteed to work + * with all upnp devices since it is not an generic IGD command + * @return the number of entries or null if the NAT table size cannot be computed for the device + * @throws IOException if some error occurs during communication with the device + * @throws UPNPResponseException if the devices returns an error message with error code other than 713 or 402 + */ + public Integer getNatTableSize() throws IOException, UPNPResponseException { + + // first let's look at the first index.. some crappy devices do not start with index 0 + // we stop at index 50 + int startIndex = -1; + for ( int i = 0; i < 50; i++ ) { + try { + this.getGenericPortMappingEntry( i ); + startIndex = i; + break; + } catch ( UPNPResponseException ex ) { + // some devices return the 402 code + if ( ex.getDetailErrorCode() != 713 && ex.getDetailErrorCode() != 402 ) { + throw ex; + } + } + } + if ( startIndex == -1 ) { + // humm nothing found within the first 200 indexes.. + // returning null + return null; + } + int size = 0; + while ( true ) { + + try { + this.getGenericPortMappingEntry( startIndex++ ); + size++; + } catch ( UPNPResponseException ex ) { + if ( ex.getDetailErrorCode() == 713 || ex.getDetailErrorCode() == 402 ) { + /// ok index unknown + break; + } + throw ex; + } + } + return new Integer( size ); + } + + private void checkPortMappingProtocol( String prot ) throws IllegalArgumentException { + if ( prot == null || ( !prot.equals( "TCP" ) && !prot.equals( "UDP" ) ) ) + throw new IllegalArgumentException( "PortMappingProtocol must be either TCP or UDP" ); + } + + private void checkPortRange( int port ) throws IllegalArgumentException { + if ( port < 1 || port > 65535 ) + throw new IllegalArgumentException( "Port range must be between 1 and 65535" ); + } + + +} diff --git a/source/net/sbbi/upnp/messages/ActionMessage.java b/source/net/sbbi/upnp/messages/ActionMessage.java new file mode 100644 index 000000000..f10eb3fef --- /dev/null +++ b/source/net/sbbi/upnp/messages/ActionMessage.java @@ -0,0 +1,437 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +import java.util.*; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.*; +import net.sbbi.upnp.services.*; + +import javax.xml.parsers.*; + +/** + * Message object for an UPNP action, simply call setInputParameter() to add + * the required action message params and then service() to receive the ActionResponse + * built with the parsed UPNP device SOAP xml response. + * @author SuperBonBon + * @version 1.0 + */ +public class ActionMessage { + + private final static Log log = LogFactory.getLog( ActionMessage.class ); + + private UPNPService service; + private ServiceAction serviceAction; + private List inputParameters; + + /** + * Protected constuctor so that only messages factories can build it + * @param service the service for which the + * @param serviceAction + */ + protected ActionMessage( UPNPService service, ServiceAction serviceAction ) { + this.service = service; + this.serviceAction = serviceAction; + if ( serviceAction.getInputActionArguments() != null ) { + inputParameters = new ArrayList(); + } + } + + /** + * Method to clear all set input parameters so that + * this object can be reused + */ + public void clearInputParameters() { + inputParameters.clear(); + } + + /** + * Executes the message and retuns the UPNP device response, according to the UPNP specs, + * this method could take up to 30 secs to process ( time allowed for a device to respond to a request ) + * @return a response object containing the UPNP parsed response + * @throws IOException if some IOException occurs during message send and reception process + * @throws UPNPResponseException if an UPNP error message is returned from the server + * or if some parsing exception occurs ( detailErrorCode = 899, detailErrorDescription = SAXException message ) + */ + public ActionResponse service() throws IOException, UPNPResponseException { + ActionResponse rtrVal = null; + UPNPResponseException upnpEx = null; + IOException ioEx = null; + StringBuffer body = new StringBuffer( 256 ); + + body.append( "\r\n" ); + body.append( "" ); + body.append( "" ); + body.append( "" ); + + if ( serviceAction.getInputActionArguments() != null ) { + // this action requires params so we just set them... + for ( Iterator itr = inputParameters.iterator(); itr.hasNext(); ) { + InputParamContainer container = (InputParamContainer)itr.next(); + body.append( "<" ).append( container.name ).append( ">" ).append( container.value ); + body.append( "" ); + } + } + body.append( "" ); + body.append( "" ); + body.append( "" ); + + if ( log.isDebugEnabled() ) log.debug( "POST prepared for URL " + service.getControlURL() ); + URL url = new URL( service.getControlURL().toString() ); + HttpURLConnection conn = (HttpURLConnection)url.openConnection(); + conn.setDoInput( true ); + conn.setDoOutput( true ); + conn.setUseCaches( false ); + conn.setRequestMethod( "POST" ); + HttpURLConnection.setFollowRedirects( false ); + //conn.setConnectTimeout( 30000 ); + conn.setRequestProperty( "HOST", url.getHost() + ":" + url.getPort() ); + conn.setRequestProperty( "CONTENT-TYPE", "text/xml; charset=\"utf-8\"" ); + conn.setRequestProperty( "CONTENT-LENGTH", Integer.toString( body.length() ) ); + conn.setRequestProperty( "SOAPACTION", "\"" + service.getServiceType() + "#" + serviceAction.getName() + "\"" ); + OutputStream out = conn.getOutputStream(); + out.write( body.toString().getBytes() ); + out.flush(); + out.close(); + conn.connect(); + InputStream input = null; + + if ( log.isDebugEnabled() ) log.debug( "executing query :\n" + body ); + try { + input = conn.getInputStream(); + } catch ( IOException ex ) { + // java can throw an exception if he error code is 500 or 404 or something else than 200 + // but the device sends 500 error message with content that is required + // this content is accessible with the getErrorStream + input = conn.getErrorStream(); + } + + if ( input != null ) { + int response = conn.getResponseCode(); + String responseBody = getResponseBody( input ); + if ( log.isDebugEnabled() ) log.debug( "received response :\n" + responseBody ); + SAXParserFactory saxParFact = SAXParserFactory.newInstance(); + saxParFact.setValidating( false ); + saxParFact.setNamespaceAware( true ); + ActionMessageResponseParser msgParser = new ActionMessageResponseParser( serviceAction ); + StringReader stringReader = new StringReader( responseBody ); + InputSource src = new InputSource( stringReader ); + try { + SAXParser parser = saxParFact.newSAXParser(); + parser.parse( src, msgParser ); + } catch ( ParserConfigurationException confEx ) { + // should never happen + // we throw a runtimeException to notify the env problem + throw new RuntimeException( "ParserConfigurationException during SAX parser creation, please check your env settings:" + confEx.getMessage() ); + } catch ( SAXException saxEx ) { + // kind of tricky but better than nothing.. + upnpEx = new UPNPResponseException( 899, saxEx.getMessage() ); + } finally { + try { + input.close(); + } catch ( IOException ex ) { + // ignore + } + } + if ( upnpEx == null ) { + if ( response == HttpURLConnection.HTTP_OK ) { + rtrVal = msgParser.getActionResponse(); + } else if ( response == HttpURLConnection.HTTP_INTERNAL_ERROR ) { + upnpEx = msgParser.getUPNPResponseException(); + } else { + ioEx = new IOException( "Unexpected server HTTP response:" + response ); + } + } + } + try { + out.close(); + } catch ( IOException ex ) { + // ignore + } + conn.disconnect(); + if ( upnpEx != null ) { + throw upnpEx; + } + if ( rtrVal == null && ioEx == null ) { + ioEx = new IOException( "Unable to receive a response from the UPNP device" ); + } + if ( ioEx != null ) { + throw ioEx; + } + return rtrVal; + } + + private String getResponseBody( InputStream in ) throws IOException { + byte[] buffer = new byte[256]; + int readen = 0; + StringBuffer content = new StringBuffer( 256 ); + while ( ( readen = in.read( buffer ) ) != -1 ) { + content.append( new String( buffer, 0 , readen ) ); + } + // some devices add \0 chars at XML message end + // which causes XML parsing errors... + int len = content.length(); + while ( content.charAt( len-1 ) == '\0' ) { + len--; + content.setLength( len ); + } + return content.toString().trim(); + } + + /** + * The list of input parameters that should be accepted by the device service for this message + * @return a list of required input parameters ServiceActionArgument objects for this message + * or null if the message does not require any input params + */ + public List getInputParameterNames() { + return serviceAction.getInputActionArgumentsNames(); + } + + /** + * The list of output parameters that should be returned by the device service + * @return a list of output parameters ServiceActionArgument objects for this message + * or null if the message does not contains any output params. + */ + public List getOutputParameterNames() { + return serviceAction.getOutputActionArgumentsNames(); + } + + /** + * Set the value of an input parameter before a message service call. If the param name already + * exists, the param value will be overwritten with the new value provided. + * @param parameterName the parameter name + * @param parameterValue the parameter value as an object, primitive object are handled, all other object + * will be assigned with a call to their toString() method call + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, Object parameterValue ) throws IllegalArgumentException { + if ( parameterValue == null ) { + return setInputParameter( parameterName, "" ); + } else if ( parameterValue instanceof Date ) { + return setInputParameter( parameterName, (Date)parameterValue ); + } else if ( parameterValue instanceof Boolean ) { + return setInputParameter( parameterName, ((Boolean)parameterValue).booleanValue() ); + } else if ( parameterValue instanceof Integer ) { + return setInputParameter( parameterName, ((Integer)parameterValue).intValue() ); + } else if ( parameterValue instanceof Byte ) { + return setInputParameter( parameterName, ((Byte)parameterValue).byteValue() ); + } else if ( parameterValue instanceof Short ) { + return setInputParameter( parameterName, ((Short)parameterValue).shortValue() ); + } else if ( parameterValue instanceof Float ) { + return setInputParameter( parameterName, ((Float)parameterValue).floatValue() ); + } else if ( parameterValue instanceof Double ) { + return setInputParameter( parameterName, ((Double)parameterValue).doubleValue() ); + } else if ( parameterValue instanceof Long ) { + return setInputParameter( parameterName, ((Long)parameterValue).longValue() ); + } + return setInputParameter( parameterName, parameterValue.toString() ); + } + + + + /** + * Set the value of an input parameter before a message service call. If the param name already + * exists, the param value will be overwritten with the new value provided + * @param parameterName the parameter name + * @param parameterValue the string parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, String parameterValue ) throws IllegalArgumentException { + if ( serviceAction.getInputActionArguments() == null ) throw new IllegalArgumentException( "No input parameters required for this message" ); + ServiceActionArgument arg = serviceAction.getInputActionArgument( parameterName ); + if ( arg == null ) throw new IllegalArgumentException( "Wrong input argument name for this action:" + parameterName + " available parameters are : " + getInputParameterNames() ); + for ( Iterator i = inputParameters.iterator(); i.hasNext(); ) { + InputParamContainer container = (InputParamContainer)i.next(); + if ( container.name.equals( parameterName ) ) { + container.value = parameterValue; + return this; + } + } + // nothing found add the new value + InputParamContainer container = new InputParamContainer(); + container.name = parameterName; + container.value = parameterValue; + inputParameters.add( container ); + return this; + } + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the date parameter value, will be automatically translated to the correct + * ISO 8601 date format for the given action input param related state variable + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, Date parameterValue ) throws IllegalArgumentException { + if ( serviceAction.getInputActionArguments() == null ) throw new IllegalArgumentException( "No input parameters required for this message" ); + ServiceActionArgument arg = serviceAction.getInputActionArgument( parameterName ); + if ( arg == null ) throw new IllegalArgumentException( "Wrong input argument name for this action:" + parameterName + " available parameters are : " + getInputParameterNames() ); + ServiceStateVariable linkedVar = arg.getRelatedStateVariable(); + if ( linkedVar.getDataType().equals( ServiceStateVariable.TIME ) ) { + return setInputParameter( parameterName, ISO8601Date.getIsoTime( parameterValue ) ); + } else if ( linkedVar.getDataType().equals( ServiceStateVariable.TIME_TZ ) ) { + return setInputParameter( parameterName, ISO8601Date.getIsoTimeZone( parameterValue ) ); + } else if ( linkedVar.getDataType().equals( ServiceStateVariable.DATE ) ) { + return setInputParameter( parameterName, ISO8601Date.getIsoDate( parameterValue ) ); + } else if ( linkedVar.getDataType().equals( ServiceStateVariable.DATETIME ) ) { + return setInputParameter( parameterName, ISO8601Date.getIsoDateTime( parameterValue ) ); + } else if ( linkedVar.getDataType().equals( ServiceStateVariable.DATETIME_TZ ) ) { + return setInputParameter( parameterName, ISO8601Date.getIsoDateTimeZone( parameterValue ) ); + } else { + throw new IllegalArgumentException( "Related input state variable " + linkedVar.getName() + " is not of an date type" ); + } + } + + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the boolean parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, boolean parameterValue ) throws IllegalArgumentException { + return setInputParameter( parameterName, parameterValue ? "1" : "0" ); + } + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the byte parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, byte parameterValue ) throws IllegalArgumentException { + return setInputParameter( parameterName, Byte.toString( parameterValue ) ); + } + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the short parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, short parameterValue ) throws IllegalArgumentException { + return setInputParameter( parameterName, Short.toString( parameterValue ) ); + } + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the integer parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, int parameterValue ) throws IllegalArgumentException { + return setInputParameter( parameterName, Integer.toString( parameterValue ) ); + } + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the long parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, long parameterValue ) throws IllegalArgumentException { + return setInputParameter( parameterName, Long.toString( parameterValue ) ); + } + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the float parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, float parameterValue ) throws IllegalArgumentException { + return setInputParameter( parameterName, Float.toString( parameterValue ) ); + } + + /** + * Set the value of an input parameter before a message service call + * @param parameterName the parameter name + * @param parameterValue the double parameter value + * @return the current ActionMessage object instance + * @throws IllegalArgumentException if the provided parameterName is not valid for this message + * or if no input parameters are required for this message + */ + public ActionMessage setInputParameter( String parameterName, double parameterValue ) throws IllegalArgumentException { + return setInputParameter( parameterName, Double.toString( parameterValue ) ); + } + + /** + * Input params class container + */ + private class InputParamContainer { + + private String name; + private String value; + + } + +} diff --git a/source/net/sbbi/upnp/messages/ActionMessageResponseParser.java b/source/net/sbbi/upnp/messages/ActionMessageResponseParser.java new file mode 100644 index 000000000..ce9e29392 --- /dev/null +++ b/source/net/sbbi/upnp/messages/ActionMessageResponseParser.java @@ -0,0 +1,160 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.Attributes; + +import net.sbbi.upnp.services.*; + +/** + * Simple SAX handler for UPNP response message parsing, this message is in SOAP format + * @author SuperBonBon + * @version 1.0 + */ + +public class ActionMessageResponseParser extends org.xml.sax.helpers.DefaultHandler { + + private final static Log log = LogFactory.getLog( ActionMessageResponseParser.class ); + + private final static String SOAP_FAULT_EL = "Fault"; + + private ServiceAction serviceAction; + private String bodyElementName; + private boolean faultResponse = false; + private UPNPResponseException msgEx; + + private boolean readFaultCode = false; + private boolean readFaultString = false; + private boolean readErrorCode = false; + private boolean readErrorDescription = false; + private boolean parseOutputParams = false; + private ActionResponse result; + private ServiceActionArgument parsedResultOutArg; + + protected ActionMessageResponseParser( ServiceAction serviceAction ) { + this.serviceAction = serviceAction; + bodyElementName = serviceAction.getName() + "Response"; + } + + protected UPNPResponseException getUPNPResponseException() { + return msgEx; + } + + protected ActionResponse getActionResponse() { + return result; + } + + public void characters( char[] ch, int start, int length ) { + if ( parseOutputParams ) { + if ( parsedResultOutArg != null ) { + String origChars = result.getOutActionArgumentValue( parsedResultOutArg.getName() ); + String newChars = new String( ch, start, length ); + if ( origChars == null ) { + result.addResult( parsedResultOutArg, newChars ); + } else { + result.addResult( parsedResultOutArg, origChars + newChars ); + } + } + } else if ( readFaultCode ) { + msgEx.faultCode = new String( ch, start, length ); + readFaultCode = false; + } else if ( readFaultString ) { + msgEx.faultString = new String( ch, start, length ); + readFaultString = false; + } else if ( readErrorCode ) { + String code = new String( ch, start, length ); + try { + msgEx.detailErrorCode = Integer.parseInt( code ); + } catch ( Throwable ex ) { + log.debug( "Error during returned error code " + code + " parsing" ); + } + readErrorCode = false; + } else if ( readErrorDescription ) { + msgEx.detailErrorDescription = new String( ch, start, length ); + readErrorDescription = false; + } + } + + public void startElement( String uri, String localName, String qName, Attributes attributes ) { + if ( parseOutputParams ) { + ServiceActionArgument arg = serviceAction.getActionArgument( localName ); + if ( arg != null && arg.getDirection() == ServiceActionArgument.DIRECTION_OUT ) { + parsedResultOutArg = arg; + result.addResult( parsedResultOutArg, null ); + } else { + parsedResultOutArg = null; + } + } else if ( faultResponse ) { + if ( localName.equals( "faultcode") ) { + readFaultCode = true; + } else if ( localName.equals( "faultstring" ) ) { + readFaultString = true; + } else if ( localName.equals( "errorCode" ) ) { + readErrorCode = true; + } else if ( localName.equals( "errorDescription" ) ) { + readErrorDescription = true; + } + } else if ( localName.equals( SOAP_FAULT_EL ) ) { + msgEx = new UPNPResponseException(); + faultResponse = true; + } else if ( localName.equals( bodyElementName ) ) { + parseOutputParams = true; + result = new ActionResponse(); + } + } + + public void endElement( String uri, String localName, String qName ) { + if ( parsedResultOutArg != null && parsedResultOutArg.getName().equals( localName ) ) { + parsedResultOutArg = null; + } else if ( localName.equals( bodyElementName ) ) { + parseOutputParams = false; + } + } +} diff --git a/source/net/sbbi/upnp/messages/ActionResponse.java b/source/net/sbbi/upnp/messages/ActionResponse.java new file mode 100644 index 000000000..bc67aa9e4 --- /dev/null +++ b/source/net/sbbi/upnp/messages/ActionResponse.java @@ -0,0 +1,101 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +import net.sbbi.upnp.services.ServiceActionArgument; + +import java.util.*; + +/** + * An action respons container Object + * @author SuperBonBon + * @version 1.0 + */ +public class ActionResponse { + + private Map outArguments = new HashMap(); + private Map outArgumentsVals = new HashMap(); + + protected ActionResponse() { + } + + public ServiceActionArgument getOutActionArgument( String actionArgumentName ) { + return (ServiceActionArgument)outArguments.get( actionArgumentName ); + } + + public String getOutActionArgumentValue( String actionArgumentName ) { + return (String)outArgumentsVals.get( actionArgumentName ); + } + + public Set getOutActionArgumentNames() { + return outArguments.keySet(); + } + + /** + * Adds a result to the response, adding an existing result ServiceActionArgument + * will override the ServiceActionArgument value + * @param arg the service action argument + * @param value the arg value + */ + protected void addResult( ServiceActionArgument arg, String value ) { + outArguments.put( arg.getName(), arg ); + outArgumentsVals.put( arg.getName(), value ); + } + + public String toString() { + StringBuffer rtrVal = new StringBuffer(); + for ( Iterator i = outArguments.keySet().iterator(); i.hasNext(); ) { + String name = (String)i.next(); + String value = (String)outArgumentsVals.get( name ); + rtrVal.append( name ).append( "=" ).append( value ); + if ( i.hasNext() ) rtrVal.append( "\n" ); + } + return rtrVal.toString(); + } + +} diff --git a/source/net/sbbi/upnp/messages/StateVariableMessage.java b/source/net/sbbi/upnp/messages/StateVariableMessage.java new file mode 100644 index 000000000..89fdf4b5d --- /dev/null +++ b/source/net/sbbi/upnp/messages/StateVariableMessage.java @@ -0,0 +1,204 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +import org.xml.sax.*; +import javax.xml.parsers.*; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import net.sbbi.upnp.services.*; + +/** + * This class is used to create state variable messages to + * comminicate with the device + * @author SuperBonBon + * @version 1.0 + */ + +public class StateVariableMessage { + + private final static Log log = LogFactory.getLog( StateVariableMessage.class ); + + private UPNPService service; + private ServiceStateVariable serviceStateVar; + + protected StateVariableMessage( UPNPService service, ServiceStateVariable serviceStateVar ) { + this.service = service; + this.serviceStateVar = serviceStateVar; + } + + /** + * Executes the state variable query and retuns the UPNP device response, according to the UPNP specs, + * this method could take up to 30 secs to process ( time allowed for a device to respond to a request ) + * @return a state variable response object containing the variable value + * @throws IOException if some IOException occurs during message send and reception process + * @throws UPNPResponseException if an UPNP error message is returned from the server + * or if some parsing exception occurs ( detailErrorCode = 899, detailErrorDescription = SAXException message ) + */ + public StateVariableResponse service() throws IOException, UPNPResponseException { + StateVariableResponse rtrVal = null; + UPNPResponseException upnpEx = null; + IOException ioEx = null; + StringBuffer body = new StringBuffer( 256 ); + + body.append( "\r\n" ); + body.append( "" ); + body.append( "" ); + body.append( "" ); + body.append( "" ).append( serviceStateVar.getName() ).append( "" ); + body.append( "" ); + body.append( "" ); + body.append( "" ); + + if ( log.isDebugEnabled() ) log.debug( "POST prepared for URL " + service.getControlURL() ); + URL url = new URL( service.getControlURL().toString() ); + HttpURLConnection conn = (HttpURLConnection)url.openConnection(); + conn.setDoInput( true ); + conn.setDoOutput( true ); + conn.setUseCaches( false ); + conn.setRequestMethod( "POST" ); + HttpURLConnection.setFollowRedirects( false ); + //conn.setConnectTimeout( 30000 ); + conn.setRequestProperty( "HOST", url.getHost() + ":" + url.getPort() ); + conn.setRequestProperty( "SOAPACTION", "\"urn:schemas-upnp-org:control-1-0#QueryStateVariable\"" ); + conn.setRequestProperty( "CONTENT-TYPE", "text/xml; charset=\"utf-8\"" ); + conn.setRequestProperty( "CONTENT-LENGTH", Integer.toString( body.length() ) ); + OutputStream out = conn.getOutputStream(); + out.write( body.toString().getBytes() ); + out.flush(); + conn.connect(); + InputStream input = null; + + if ( log.isDebugEnabled() ) log.debug( "executing query :\n" + body ); + try { + input = conn.getInputStream(); + } catch ( IOException ex ) { + // java can throw an exception if he error code is 500 or 404 or something else than 200 + // but the device sends 500 error message with content that is required + // this content is accessible with the getErrorStream + input = conn.getErrorStream(); + } + + if ( input != null ) { + int response = conn.getResponseCode(); + String responseBody = getResponseBody( input ); + if ( log.isDebugEnabled() ) log.debug( "received response :\n" + responseBody ); + SAXParserFactory saxParFact = SAXParserFactory.newInstance(); + saxParFact.setValidating( false ); + saxParFact.setNamespaceAware( true ); + StateVariableResponseParser msgParser = new StateVariableResponseParser( serviceStateVar ); + StringReader stringReader = new StringReader( responseBody ); + InputSource src = new InputSource( stringReader ); + try { + SAXParser parser = saxParFact.newSAXParser(); + parser.parse( src, msgParser ); + } catch ( ParserConfigurationException confEx ) { + // should never happen + // we throw a runtimeException to notify the env problem + throw new RuntimeException( "ParserConfigurationException during SAX parser creation, please check your env settings:" + confEx.getMessage() ); + } catch ( SAXException saxEx ) { + // kind of tricky but better than nothing.. + upnpEx = new UPNPResponseException( 899, saxEx.getMessage() ); + } finally { + try { + input.close(); + } catch ( IOException ex ) { + // ignoring + } + } + if ( upnpEx == null ) { + if ( response == HttpURLConnection.HTTP_OK ) { + rtrVal = msgParser.getStateVariableResponse(); + } else if ( response == HttpURLConnection.HTTP_INTERNAL_ERROR ) { + upnpEx = msgParser.getUPNPResponseException(); + } else { + ioEx = new IOException( "Unexpected server HTTP response:" + response ); + } + } + } + try { + out.close(); + } catch ( IOException ex ) { + // ignore + } + conn.disconnect(); + if ( upnpEx != null ) { + throw upnpEx; + } + if ( rtrVal == null && ioEx == null ) { + ioEx = new IOException( "Unable to receive a response from the UPNP device" ); + } + if ( ioEx != null ) { + throw ioEx; + } + return rtrVal; + } + + private String getResponseBody( InputStream in ) throws IOException { + byte[] buffer = new byte[256]; + int readen = 0; + StringBuffer content = new StringBuffer( 256 ); + while ( ( readen = in.read( buffer ) ) != -1 ) { + content.append( new String( buffer, 0 , readen ) ); + } + // some devices add \0 chars at XML message end + // which causes XML parsing errors... + int len = content.length(); + while ( content.charAt( len-1 ) == '\0' ) { + len--; + content.setLength( len ); + } + return content.toString().trim(); + } +} diff --git a/source/net/sbbi/upnp/messages/StateVariableResponse.java b/source/net/sbbi/upnp/messages/StateVariableResponse.java new file mode 100644 index 000000000..196f7c036 --- /dev/null +++ b/source/net/sbbi/upnp/messages/StateVariableResponse.java @@ -0,0 +1,74 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +import net.sbbi.upnp.services.ServiceStateVariable; + +/** + * This class contains data returned by a state variable query response + * @author SuperBonBon + * @version 1.0 + */ +public class StateVariableResponse { + + protected ServiceStateVariable stateVar; + protected String stateVariableValue; + + protected StateVariableResponse() { + } + + public ServiceStateVariable getStateVar() { + return stateVar; + } + + public String getStateVariableValue() { + return stateVariableValue; + } + + +} diff --git a/source/net/sbbi/upnp/messages/StateVariableResponseParser.java b/source/net/sbbi/upnp/messages/StateVariableResponseParser.java new file mode 100644 index 000000000..000164ce0 --- /dev/null +++ b/source/net/sbbi/upnp/messages/StateVariableResponseParser.java @@ -0,0 +1,154 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import net.sbbi.upnp.services.*; + +/** + * Simple SAX handler for UPNP state variable query response message parsing, this message is in SOAP format + * @author SuperBonBon + * @version 1.0 + */ + +public class StateVariableResponseParser extends org.xml.sax.helpers.DefaultHandler { + + private final static Log log = LogFactory.getLog( StateVariableResponseParser.class ); + + private final static String SOAP_FAULT_EL = "Fault"; + + private ServiceStateVariable stateVar; + private boolean faultResponse = false; + private UPNPResponseException msgEx; + + private boolean readFaultCode = false; + private boolean readFaultString = false; + private boolean readErrorCode = false; + private boolean readErrorDescription = false; + private boolean parseStateVar = false; + private StateVariableResponse result; + + protected StateVariableResponseParser( ServiceStateVariable stateVar ) { + this.stateVar = stateVar; + } + + protected UPNPResponseException getUPNPResponseException() { + return msgEx; + } + + protected StateVariableResponse getStateVariableResponse() { + return result; + } + + public void characters( char[] ch, int start, int length ) { + if ( parseStateVar ) { + String origChars = result.stateVariableValue; + String newChars = new String( ch, start, length ); + if ( origChars == null ) { + result.stateVariableValue = newChars; + } else { + result.stateVariableValue = origChars + newChars; + } + } else if ( readFaultCode ) { + msgEx.faultCode = new String( ch, start, length ); + readFaultCode = false; + } else if ( readFaultString ) { + msgEx.faultString = new String( ch, start, length ); + readFaultString = false; + } else if ( readErrorCode ) { + String code = new String( ch, start, length ); + try { + msgEx.detailErrorCode = Integer.parseInt( code ); + } catch ( Throwable ex ) { + log.debug( "Error during returned error code " + code + " parsing" ); + } + readErrorCode = false; + } else if ( readErrorDescription ) { + msgEx.detailErrorDescription = new String( ch, start, length ); + readErrorDescription = false; + } + } + + public void startElement( String uri, String localName, String qName, Attributes attributes ) { + + if ( faultResponse ) { + if ( localName.equals( "faultcode") ) { + readFaultCode = true; + } else if ( localName.equals( "faultstring" ) ) { + readFaultString = true; + } else if ( localName.equals( "errorCode" ) ) { + readErrorCode = true; + } else if ( localName.equals( "errorDescription" ) ) { + readErrorDescription = true; + } + } else if ( localName.equals( SOAP_FAULT_EL ) ) { + msgEx = new UPNPResponseException(); + faultResponse = true; + } else if ( localName.equals( "return" ) || localName.equals( "varName" ) ) { + // some buggy implementations ( intel sample media server ) + // do not use the specs compliant return element name but varName ... + parseStateVar = true; + result = new StateVariableResponse(); + result.stateVar = stateVar; + } + } + + public void endElement( String uri, String localName, String qName ) throws SAXException { + // some buggy implementations ( intel sample media server ) + // do not use the specs compliant return element name but varName ... + if ( localName.equals( "return" ) || localName.equals( "varName" ) ) { + parseStateVar = false; + } + } + +} + diff --git a/source/net/sbbi/upnp/messages/UPNPMessageFactory.java b/source/net/sbbi/upnp/messages/UPNPMessageFactory.java new file mode 100644 index 000000000..9880582dc --- /dev/null +++ b/source/net/sbbi/upnp/messages/UPNPMessageFactory.java @@ -0,0 +1,108 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +import net.sbbi.upnp.services.*; + +/** + * Factory to create UPNP messages to access and communicate with + * a given UPNPDevice service capabilities + * @author SuperBonBon + * @version 1.0 + */ + +public class UPNPMessageFactory { + + private UPNPService service; + + /** + * Private constructor since this is a factory. + * @param service the UPNPService that will be used to generate messages by thid factory + */ + private UPNPMessageFactory( UPNPService service ) { + this.service = service; + } + + /** + * Generate a new factory instance for a given device service definition object + * @param service the UPNP service definition object for messages creation + * @return a new message factory + */ + public static UPNPMessageFactory getNewInstance( UPNPService service ) { + return new UPNPMessageFactory( service ); + } + + /** + * Creation of a new ActionMessage to communicate with the UPNP device + * @param serviceActionName the name of a service action, this name is case sensitive + * and matches exactly the name provided by the UPNP device in the XML definition file + * @return a ActionMessage object or null if the action is unknown for this service messages factory + */ + public ActionMessage getMessage( String serviceActionName ) { + ServiceAction serviceAction = service.getUPNPServiceAction( serviceActionName ); + if ( serviceAction != null ) { + return new ActionMessage( service, serviceAction ); + } + return null; + } + + /** + * Creation of a new StateVariableMessage to communicate with the UPNP device, for a service state variable query + * @param serviceStateVariable the name of a service state variable, this name is case sensitive + * and matches exactly the name provided by the UPNP device in the XML definition file + * @return a StateVariableMessage object or null if the state variable is unknown for this service mesages factory + */ + public StateVariableMessage getStateVariableMessage( String serviceStateVariable ) { + ServiceStateVariable stateVar = service.getUPNPServiceStateVariable( serviceStateVariable ); + if ( stateVar != null ) { + return new StateVariableMessage( service, stateVar ); + } + return null; + } + +} diff --git a/source/net/sbbi/upnp/messages/UPNPResponseException.java b/source/net/sbbi/upnp/messages/UPNPResponseException.java new file mode 100644 index 000000000..d3a5e1f48 --- /dev/null +++ b/source/net/sbbi/upnp/messages/UPNPResponseException.java @@ -0,0 +1,97 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.messages; + +/** + * An exception throws when parsing a message if a SOAP fault + * exception is returned. + * @author SuperBonBon + * @version 1.0 + */ +public class UPNPResponseException extends Exception { + + private static final long serialVersionUID = 8313107558129180594L; + + protected String faultCode; + protected String faultString; + protected int detailErrorCode; + protected String detailErrorDescription; + + public UPNPResponseException() { + } + + public UPNPResponseException( int detailErrorCode, String detailErrorDescription ) { + this.detailErrorCode = detailErrorCode; + this.detailErrorDescription = detailErrorDescription; + } + + public String getFaultCode() { + return faultCode == null ? "Client" : faultCode ; + } + + public String getFaultString() { + return faultString == null ? "UPnPError" : faultString; + } + + public int getDetailErrorCode() { + return detailErrorCode; + } + + public String getDetailErrorDescription() { + return detailErrorDescription; + } + + public String getMessage() { + return "Detailed error code :" + detailErrorCode + ", Detailed error description :" + detailErrorDescription; + } + + public String getLocalizedMessage() { + return getMessage(); + } + +} diff --git a/source/net/sbbi/upnp/services/ISO8601Date.java b/source/net/sbbi/upnp/services/ISO8601Date.java new file mode 100644 index 000000000..6194a7109 --- /dev/null +++ b/source/net/sbbi/upnp/services/ISO8601Date.java @@ -0,0 +1,331 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.services; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; +import java.util.TimeZone; + +/** + * ISO8601 Date implementation taken from org.w3c package and modified + * to work with UPNP date types + * @author SuperBonBon + * @version 1.0 + */ + +public class ISO8601Date { + + private static boolean check(StringTokenizer st, String token) throws NumberFormatException { + try { + if (st.nextToken().equals(token)) { + return true; + } else { + throw new NumberFormatException("Missing [" + token + "]"); + } + } catch (NoSuchElementException ex) { + return false; + } + } + + private static Calendar getCalendar( String isodate ) throws NumberFormatException { + // YYYY-MM-DDThh:mm:ss.sTZD or hh:mm:ss.sTZD + // does it contains a date ? + boolean isATime = isodate.indexOf( ':' ) != -1; + boolean isADate = isodate.indexOf( '-' ) != -1 || ( isodate.length() == 4 && !isATime ); + if ( isATime && ! isADate ) { + if ( !isodate.toUpperCase().startsWith( "T" ) ) { + isodate = "T" + isodate; + } + } + StringTokenizer st = new StringTokenizer(isodate, "-T:.+Z", true); + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.clear(); + if ( isADate ) { + // Year + if (st.hasMoreTokens()) { + int year = Integer.parseInt(st.nextToken()); + calendar.set(Calendar.YEAR, year); + } else { + return calendar; + } + // Month + if (check(st, "-") && (st.hasMoreTokens())) { + int month = Integer.parseInt(st.nextToken()) - 1; + calendar.set(Calendar.MONTH, month); + } else { + return calendar; + } + // Day + if (check(st, "-") && (st.hasMoreTokens())) { + int day = Integer.parseInt(st.nextToken()); + calendar.set(Calendar.DAY_OF_MONTH, day); + } else { + return calendar; + } + } + // Hour + if ( ( check(st, "T") ) && ( st.hasMoreTokens() ) ) { + int hour = Integer.parseInt(st.nextToken()); + calendar.set(Calendar.HOUR_OF_DAY, hour); + } else { + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar; + } + // Minutes + if (check(st, ":") && (st.hasMoreTokens())) { + int minutes = Integer.parseInt(st.nextToken()); + calendar.set(Calendar.MINUTE, minutes); + } else { + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar; + } + + // + // Not mandatory now + // + + // Secondes + if (!st.hasMoreTokens()) { + return calendar; + } + String tok = st.nextToken(); + if (tok.equals(":")) { // secondes + if (st.hasMoreTokens()) { + int secondes = Integer.parseInt(st.nextToken()); + calendar.set(Calendar.SECOND, secondes); + if (!st.hasMoreTokens()) { + return calendar; + } + // frac sec + tok = st.nextToken(); + if (tok.equals(".")) { + // bug fixed, thx to Martin Bottcher + String nt = st.nextToken(); + while (nt.length() < 3) { + nt += "0"; + } + nt = nt.substring(0, 3); // Cut trailing chars.. + int millisec = Integer.parseInt(nt); + // int millisec = Integer.parseInt(st.nextToken()) * 10; + calendar.set(Calendar.MILLISECOND, millisec); + if (!st.hasMoreTokens()) { + return calendar; + } + tok = st.nextToken(); + } else { + calendar.set(Calendar.MILLISECOND, 0); + } + } else { + throw new NumberFormatException("No secondes specified"); + } + } else { + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + } + // Timezone + if (!tok.equals("Z")) { // UTC + if (!(tok.equals("+") || tok.equals("-"))) { + throw new NumberFormatException("only Z, + or - allowed"); + } + boolean plus = tok.equals("+"); + if (!st.hasMoreTokens()) { + throw new NumberFormatException("Missing hour field"); + } + int tzhour = Integer.parseInt(st.nextToken()); + int tzmin = 0; + if (check(st, ":") && (st.hasMoreTokens())) { + tzmin = Integer.parseInt(st.nextToken()); + } else { + throw new NumberFormatException("Missing minute field"); + } + if (plus) { + calendar.add(Calendar.HOUR, -tzhour); + calendar.add(Calendar.MINUTE, -tzmin); + } else { + calendar.add(Calendar.HOUR, tzhour); + calendar.add(Calendar.MINUTE, tzmin); + } + } + return calendar; + } + + /** + * Parse the given string in ISO 8601 format and build a Date object. + * @param isodate the date in ISO 8601 format + * @return a Date instance + * @exception InvalidDateException + * if the date is not valid + */ + public static Date parse(String isodate) throws NumberFormatException { + Calendar calendar = getCalendar(isodate); + return calendar.getTime(); + } + + private static String twoDigit(int i) { + if (i >= 0 && i < 10) { + return "0" + String.valueOf(i); + } + return String.valueOf(i); + } + + /** + * Generate a ISO 8601 date + * @param date a Date instance + * @return a string representing the date in the ISO 8601 format + */ + public static String getIsoDate(Date date) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + StringBuffer buffer = new StringBuffer(); + buffer.append(calendar.get(Calendar.YEAR)); + buffer.append("-"); + buffer.append(twoDigit(calendar.get(Calendar.MONTH) + 1)); + buffer.append("-"); + buffer.append(twoDigit(calendar.get(Calendar.DAY_OF_MONTH))); + return buffer.toString(); + } + + /** + * Generate a ISO 8601 date time without timezone + * @param date a Date instance + * @return a string representing the date in the ISO 8601 format + */ + public static String getIsoDateTime(Date date) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + StringBuffer buffer = new StringBuffer(); + buffer.append(calendar.get(Calendar.YEAR)); + buffer.append("-"); + buffer.append(twoDigit(calendar.get(Calendar.MONTH) + 1)); + buffer.append("-"); + buffer.append(twoDigit(calendar.get(Calendar.DAY_OF_MONTH))); + buffer.append("T"); + buffer.append(twoDigit(calendar.get(Calendar.HOUR_OF_DAY))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.MINUTE))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.SECOND))); + buffer.append("."); + buffer.append(twoDigit(calendar.get(Calendar.MILLISECOND) / 10)); + return buffer.toString(); + } + + /** + * Generate a ISO 8601 date time with timezone + * @param date a Date instance + * @return a string representing the date in the ISO 8601 format + */ + public static String getIsoDateTimeZone(Date date) { + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.setTime(date); + StringBuffer buffer = new StringBuffer(); + buffer.append(calendar.get(Calendar.YEAR)); + buffer.append("-"); + buffer.append(twoDigit(calendar.get(Calendar.MONTH) + 1)); + buffer.append("-"); + buffer.append(twoDigit(calendar.get(Calendar.DAY_OF_MONTH))); + buffer.append("T"); + buffer.append(twoDigit(calendar.get(Calendar.HOUR_OF_DAY))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.MINUTE))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.SECOND))); + buffer.append("."); + buffer.append(twoDigit(calendar.get(Calendar.MILLISECOND) / 10)); + buffer.append("Z"); + return buffer.toString(); + } + + /** + * Generate a ISO 8601 time + * @param date a Date instance + * @return a string representing the date in the ISO 8601 format + */ + public static String getIsoTime(Date date) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + StringBuffer buffer = new StringBuffer(); + buffer.append(twoDigit(calendar.get(Calendar.HOUR_OF_DAY))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.MINUTE))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.SECOND))); + buffer.append("."); + buffer.append(twoDigit(calendar.get(Calendar.MILLISECOND) / 10)); + return buffer.toString(); + } + + /** + * Generate a ISO 8601 time + * @param date a Date instance + * @return a string representing the date in the ISO 8601 format + */ + public static String getIsoTimeZone(Date date) { + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.setTime(date); + StringBuffer buffer = new StringBuffer(); + buffer.append(twoDigit(calendar.get(Calendar.HOUR_OF_DAY))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.MINUTE))); + buffer.append(":"); + buffer.append(twoDigit(calendar.get(Calendar.SECOND))); + buffer.append("."); + buffer.append(twoDigit(calendar.get(Calendar.MILLISECOND) / 10)); + buffer.append("Z"); + return buffer.toString(); + } + +} diff --git a/source/net/sbbi/upnp/services/ServiceAction.java b/source/net/sbbi/upnp/services/ServiceAction.java new file mode 100644 index 000000000..b994ffc16 --- /dev/null +++ b/source/net/sbbi/upnp/services/ServiceAction.java @@ -0,0 +1,202 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.services; + +import java.util.*; + +/** + * An object to represent a service action proposed by an UPNP service + * @author SuperBonBon + * @version 1.0 + */ +public class ServiceAction { + + protected String name; + protected UPNPService parent; + private List orderedActionArguments; + private List orderedInputActionArguments; + private List orderedOutputActionArguments; + private List orderedInputActionArgumentsNames; + private List orderedOutputActionArgumentsNames; + + protected ServiceAction() { + } + + public UPNPService getParent() { + return parent; + } + + /** + * The action in and out arguments ServiceActionArgument objects list + * @return the list with ServiceActionArgument objects or null if the action has no params + */ + public List getActionArguments() { + return orderedActionArguments; + } + + /** + * Look for an ServiceActionArgument for a given name + * @param argumentName the argument name + * @return the argument or null if not found or not available + */ + public ServiceActionArgument getActionArgument( String argumentName ) { + if ( orderedActionArguments == null ) return null; + for ( Iterator i = orderedActionArguments.iterator(); i.hasNext(); ) { + ServiceActionArgument arg = (ServiceActionArgument)i.next(); + if ( arg.getName().equals( argumentName ) ) return arg; + } + return null; + } + + protected void setActionArguments( List orderedActionArguments ) { + this.orderedActionArguments = orderedActionArguments; + orderedInputActionArguments = getListForActionArgument( orderedActionArguments, ServiceActionArgument.DIRECTION_IN ); + orderedOutputActionArguments = getListForActionArgument( orderedActionArguments, ServiceActionArgument.DIRECTION_OUT ); + orderedInputActionArgumentsNames = getListForActionArgumentNames( orderedActionArguments, ServiceActionArgument.DIRECTION_IN ); + orderedOutputActionArgumentsNames = getListForActionArgumentNames( orderedActionArguments, ServiceActionArgument.DIRECTION_OUT ); + } + + /** + * Return a list containing input ( when a response is sent ) arguments objects + * @return a list containing input arguments ServiceActionArgument objects or null when nothing + * is needed for such operation + */ + public List getInputActionArguments() { + return orderedInputActionArguments; + } + + /** + * Look for an input ServiceActionArgument for a given name + * @param argumentName the input argument name + * @return the argument or null if not found or not available + */ + public ServiceActionArgument getInputActionArgument( String argumentName ) { + if ( orderedInputActionArguments == null ) return null; + for ( Iterator i = orderedInputActionArguments.iterator(); i.hasNext(); ) { + ServiceActionArgument arg = (ServiceActionArgument)i.next(); + if ( arg.getName().equals( argumentName ) ) return arg; + } + return null; + } + + + /** + * Return a list containing output ( when a response is received ) arguments objects + * @return a list containing output arguments ServiceActionArgument objects or null when nothing + * returned for such operation + */ + public List getOutputActionArguments() { + return orderedOutputActionArguments; + } + + /** + * Look for an output ServiceActionArgument for a given name + * @param argumentName the input argument name + * @return the argument or null if not found or not available + */ + public ServiceActionArgument getOutputActionArgument( String argumentName ) { + if ( orderedOutputActionArguments == null ) return null; + for ( Iterator i = orderedOutputActionArguments.iterator(); i.hasNext(); ) { + ServiceActionArgument arg = (ServiceActionArgument)i.next(); + if ( arg.getName().equals( argumentName ) ) return arg; + } + return null; + } + + /** + * Return a list containing input ( when a response is sent ) arguments names + * @return a list containing input arguments names as Strings or null when nothing + * is needed for such operation + */ + public List getInputActionArgumentsNames() { + return orderedInputActionArgumentsNames; + } + + /** + * Return a list containing output ( when a response is received ) arguments names + * @return a list containing output arguments names as Strings or null when nothing + * returned for such operation + */ + public List getOutputActionArgumentsNames() { + return orderedOutputActionArgumentsNames; + } + + /** + * The action name + * @return The action name + */ + public String getName() { + return name; + } + + private List getListForActionArgument( List args, String direction ) { + if ( args == null ) return null; + List rtrVal = new ArrayList(); + for ( Iterator itr = args.iterator(); itr.hasNext(); ) { + ServiceActionArgument actArg = (ServiceActionArgument)itr.next(); + if ( actArg.getDirection() == direction ) { + rtrVal.add( actArg ); + } + } + if ( rtrVal.size() == 0 ) rtrVal = null; + return rtrVal; + } + + private List getListForActionArgumentNames( List args, String direction ) { + if ( args == null ) return null; + List rtrVal = new ArrayList(); + for ( Iterator itr = args.iterator(); itr.hasNext(); ) { + ServiceActionArgument actArg = (ServiceActionArgument)itr.next(); + if ( actArg.getDirection() == direction ) { + rtrVal.add( actArg.getName() ); + } + } + if ( rtrVal.size() == 0 ) rtrVal = null; + return rtrVal; + } +} diff --git a/source/net/sbbi/upnp/services/ServiceActionArgument.java b/source/net/sbbi/upnp/services/ServiceActionArgument.java new file mode 100644 index 000000000..4bd1a613e --- /dev/null +++ b/source/net/sbbi/upnp/services/ServiceActionArgument.java @@ -0,0 +1,87 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.services; + +/** + * An argument for a service action + * @author SuperBonBon + * @version 1.0 + */ +public class ServiceActionArgument { + + public final static String DIRECTION_IN = "in"; + public final static String DIRECTION_OUT = "out"; + + protected ServiceStateVariable relatedStateVariable; + protected String name; + protected String direction; + + /** + * The related service state variable for this ServiceActionArgument + * @return The related service state variable for this ServiceActionArgument + */ + public ServiceStateVariable getRelatedStateVariable() { + return relatedStateVariable; + } + + /** + * The argument name + * @return the argument name + */ + public String getName() { + return name; + } + + /** + * The argument direction + * @return the argument direction (in|out) + */ + public String getDirection() { + return direction; + } +} diff --git a/source/net/sbbi/upnp/services/ServiceStateVariable.java b/source/net/sbbi/upnp/services/ServiceStateVariable.java new file mode 100644 index 000000000..0ae5e3437 --- /dev/null +++ b/source/net/sbbi/upnp/services/ServiceStateVariable.java @@ -0,0 +1,263 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.services; + +import java.io.IOException; +import java.net.URI; +import java.util.Date; +import java.util.Set; + +import net.sbbi.upnp.messages.StateVariableMessage; +import net.sbbi.upnp.messages.UPNPMessageFactory; +import net.sbbi.upnp.messages.UPNPResponseException; + +/** + * Class to contain a service state variable definition + * @author SuperBonBon + * @version 1.0 + */ + +public class ServiceStateVariable implements ServiceStateVariableTypes { + + private StateVariableMessage stateVarMsg = null; + + protected String name; + protected boolean sendEvents; + protected String dataType; + protected String defaultValue; + + protected String minimumRangeValue; + protected String maximumRangeValue; + protected String stepRangeValue; + protected Set allowedvalues; + protected UPNPService parent; + + protected ServiceStateVariable() { + } + + /** + * Call to the UPNP device to retreive the state variable actual value + * @return the state variable actual value on the device, should be never null, an empty string could be returned by the device + * @throws UPNPResponseException if the device throws an exception during query + * @throws IOException if some IO error with device occurs during query + */ + public String getValue() throws UPNPResponseException, IOException { + if ( stateVarMsg == null ) { + synchronized( this ) { + if ( stateVarMsg == null ) { + UPNPMessageFactory factory = UPNPMessageFactory.getNewInstance( parent ); + stateVarMsg = factory.getStateVariableMessage( name ); + } + } + } + return stateVarMsg.service().getStateVariableValue(); + } + + /** + * State variable name + * @return the state variable name + */ + public String getName() { + return name; + } + + /** + * The parent UPNPService Object + * @return the parent object instance + */ + public UPNPService getParent() { + return parent; + } + + /** + * Boolean to indicate if the variable is sending events when value + * of the var is changing. The events can be subscribed via the {@link net.sbbi.upnp.ServicesEventing} + * class + * @return true if sending events + */ + public boolean isSendEvents() { + return sendEvents; + } + + /** + * The default value of the state variable + * @return the default value representation as an string + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * The variable UPNP data type + * @return the data type + */ + public String getDataType() { + return dataType; + } + + /** + * The varialbe JAVA data type (using an UPNP->Java mapping) + * @return the class mapped + */ + public Class getDataTypeAsClass() { + return getDataTypeClassMapping( dataType ); + } + + /** + * A set of allowed values (String objects) for the variable + * @return the allowed values or null if none + */ + public Set getAllowedvalues() { + return allowedvalues; + } + + /** + * The minimum value as a string + * @return the minimum value or null if no restriction + */ + public String getMinimumRangeValue() { + return minimumRangeValue; + } + + /** + * The maximum value as a string + * @return the maximum value or null if no restriction + */ + public String getMaximumRangeValue() { + return maximumRangeValue; + } + + /** + * The value step range as a string + * @return the value step raqnge or null if no restriction + */ + public String getStepRangeValue() { + return stepRangeValue; + } + + public static Class getDataTypeClassMapping( String dataType ) { + int hash = dataType.hashCode(); + if ( hash == UI1_INT ) return Short.class; + else if ( hash == UI2_INT ) return Integer.class; + else if ( hash == UI4_INT ) return Long.class; + else if ( hash == I1_INT ) return Byte.class; + else if ( hash == I2_INT ) return Short.class; + else if ( hash == I4_INT ) return Integer.class; + else if ( hash == INT_INT ) return Integer.class; + else if ( hash == R4_INT ) return Float.class; + else if ( hash == R8_INT ) return Double.class; + else if ( hash == NUMBER_INT ) return Double.class; + else if ( hash == FIXED_14_4_INT ) return Double.class; + else if ( hash == FLOAT_INT ) return Float.class; + else if ( hash == CHAR_INT) return Character.class; + else if ( hash == STRING_INT ) return String.class; + else if ( hash == DATE_INT ) return Date.class; + else if ( hash == DATETIME_INT ) return Date.class; + else if ( hash == DATETIME_TZ_INT ) return Date.class; + else if ( hash == TIME_INT ) return Date.class; + else if ( hash == TIME_TZ_INT ) return Date.class; + else if ( hash == BOOLEAN_INT ) return Boolean.class; + else if ( hash == BIN_BASE64_INT ) return String.class; + else if ( hash == BIN_HEX_INT ) return String.class; + else if ( hash == URI_INT ) return URI.class; + else if ( hash == UUID_INT ) return String.class; + return null; + } + + public static String getUPNPDataTypeMapping( String className ) { + if ( className.equals( Short.class.getName() ) || className.equals( "short" )) return I2; + else if ( className.equals( Byte.class.getName() ) || className.equals( "byte" )) return I1; + else if ( className.equals( Integer.class.getName() ) || className.equals( "int" ) ) return INT; + else if ( className.equals( Long.class.getName() ) || className.equals( "long" ) ) return UI4; + else if ( className.equals( Float.class.getName() ) || className.equals( "float" )) return FLOAT; + else if ( className.equals( Double.class.getName() ) || className.equals( "double" ) ) return NUMBER; + else if ( className.equals( Character.class.getName() ) || className.equals( "char" ) ) return CHAR; + else if ( className.equals( String.class.getName() ) || className.equals( "string" ) ) return STRING; + else if ( className.equals( Date.class.getName() ) ) return DATETIME; + else if ( className.equals( Boolean.class.getName() ) || className.equals( "boolean" ) ) return BOOLEAN; + else if ( className.equals( URI.class.getName() ) ) return URI; + return null; + } + + public static Object UPNPToJavaObject( String dataType, String value ) throws Throwable { + if ( value == null ) throw new Exception( "null value" ); + if ( dataType == null ) throw new Exception( "null dataType" ); + int hash = dataType.hashCode(); + if ( hash == UI1_INT ) return new Short( value ); + else if ( hash == UI2_INT ) return new Integer( value ); + else if ( hash == UI4_INT ) return new Long( value ); + else if ( hash == I1_INT ) return new Byte( value ); + else if ( hash == I2_INT ) return new Short( value ); + else if ( hash == I4_INT ) return new Integer( value ); + else if ( hash == INT_INT ) return new Integer( value ); + else if ( hash == R4_INT ) return new Float( value ); + else if ( hash == R8_INT ) return new Double( value ); + else if ( hash == NUMBER_INT ) return new Double( value ); + else if ( hash == FIXED_14_4_INT ) return new Double( value ); + else if ( hash == FLOAT_INT ) return new Float( value ); + else if ( hash == CHAR_INT) return new Character( value.charAt( 0 ) ); + else if ( hash == STRING_INT ) return value; + else if ( hash == DATE_INT ) return ISO8601Date.parse( value ); + else if ( hash == DATETIME_INT ) return ISO8601Date.parse( value ); + else if ( hash == DATETIME_TZ_INT ) return ISO8601Date.parse( value ); + else if ( hash == TIME_INT ) return ISO8601Date.parse( value ); + else if ( hash == TIME_TZ_INT ) return ISO8601Date.parse( value ); + else if ( hash == BOOLEAN_INT ) { + if ( value.equals( "1" ) || value.equalsIgnoreCase( "yes" ) || value.equals( "true" ) ) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } + else if ( hash == BIN_BASE64_INT ) return value; + else if ( hash == BIN_HEX_INT ) return value; + else if ( hash == URI_INT ) return new URI( value ); + else if ( hash == UUID_INT ) return value; + throw new Exception( "Unhandled data type " + dataType ); + } + +} diff --git a/source/net/sbbi/upnp/services/ServiceStateVariableTypes.java b/source/net/sbbi/upnp/services/ServiceStateVariableTypes.java new file mode 100644 index 000000000..558d559c0 --- /dev/null +++ b/source/net/sbbi/upnp/services/ServiceStateVariableTypes.java @@ -0,0 +1,188 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.services; + +/** + * Interface to defined allowed values for service state variables data types + * @author SuperBonBon + * @version 1.0 + */ +public interface ServiceStateVariableTypes { + + /** + * Unsigned 1 Byte int. Same format as int without leading sign. + */ + public final static String UI1 = "ui1"; + /** + * Unsigned 2 Byte int. Same format as int without leading sign. + */ + public final static String UI2 = "ui2"; + /** + * Unsigned 4 Byte int. Same format as int without leading sign. + */ + public final static String UI4 = "ui4"; + /** + * 1 Byte int. Same format as int. + */ + public final static String I1 = "i1"; + /** + * 2 Byte int. Same format as int. + */ + public final static String I2 = "i2"; + /** + * 4 Byte int. Same format as int. + */ + public final static String I4 = "i4"; + /** + * Fixed point, integer number. May have leading sign. May have leading zeros. + * (No currency symbol.) (No grouping of digits to the left of the decimal, e.g., no commas.) + */ + public final static String INT = "int"; + /** + * 4 Byte float. Same format as float. Must be between 3.40282347E+38 to 1.17549435E-38. + */ + public final static String R4 = "r4"; + /** + * 8 Byte float. Same format as float. Must be between -1.79769313486232E308 and -4.94065645841247E-324 + * for negative values, and between 4.94065645841247E-324 and 1.79769313486232E308 for positive values, + * i.e., IEEE 64-bit (8-Byte) double. + */ + public final static String R8 = "r8"; + /** + * Same as r8. + */ + public final static String NUMBER = "number"; + /** + * Same as r8 but no more than 14 digits to the left of the decimal point and no more than 4 to the right. + */ + public final static String FIXED_14_4 = "fixed.14.4"; + /** + * Floating point number. Mantissa (left of the decimal) and/or exponent may have a leading sign. + * Mantissa and/or exponent may have leading zeros. Decimal character in mantissa is a period, i.e., + * whole digits in mantissa separated from fractional digits by period. Mantissa separated from exponent + * by E. (No currency symbol.) (No grouping of digits in the mantissa, e.g., no commas.) + */ + public final static String FLOAT = "float"; + /** + * Unicode string. One character long. + */ + public final static String CHAR = "char"; + /** + * Unicode string. No limit on length. + */ + public final static String STRING = "string"; + /** + * Date in a subset of ISO 8601 format without time data. + */ + public final static String DATE = "date"; + /** + * Date in ISO 8601 format with optional time but no time zone. + */ + public final static String DATETIME = "dateTime"; + /** + * Date in ISO 8601 format with optional time and optional time zone. + */ + public final static String DATETIME_TZ = "dateTime.tz"; + /** + * Time in a subset of ISO 8601 format with no date and no time zone. + */ + public final static String TIME = "time"; + /** + * Time in a subset of ISO 8601 format with optional time zone but no date. + */ + public final static String TIME_TZ = "time.tz"; + /** + * 0, false, or no for false; 1, true, or yes for true. + */ + public final static String BOOLEAN = "boolean"; + /** + * MIME-style Base64 encoded binary BLOB. Takes 3 Bytes, splits them into 4 parts, + * and maps each 6 bit piece to an octet. (3 octets are encoded as 4.) No limit on size. + */ + public final static String BIN_BASE64 = "bin.base64"; + /** + * Hexadecimal digits representing octets. Treats each nibble as a hex digit and encodes + * as a separate Byte. (1 octet is encoded as 2.) No limit on size. + */ + public final static String BIN_HEX = "bin.hex"; + /** + * Universal Resource Identifier. + */ + public final static String URI = "uri"; + /** + * Universally Unique ID. Hexadecimal digits representing octets. + * Optional embedded hyphens are ignored. + */ + public final static String UUID = "uuid"; + + public final static int UI1_INT = "ui1".hashCode(); + public final static int UI2_INT = "ui2".hashCode(); + public final static int UI4_INT = "ui4".hashCode(); + public final static int I1_INT = "i1".hashCode(); + public final static int I2_INT = "i2".hashCode(); + public final static int I4_INT = "i4".hashCode(); + public final static int INT_INT = "int".hashCode(); + public final static int R4_INT = "r4".hashCode(); + public final static int R8_INT = "r8".hashCode(); + public final static int NUMBER_INT = "number".hashCode(); + public final static int FIXED_14_4_INT = "fixed.14.4".hashCode(); + public final static int FLOAT_INT = "float".hashCode(); + public final static int CHAR_INT = "char".hashCode(); + public final static int STRING_INT = "string".hashCode(); + public final static int DATE_INT = "date".hashCode(); + public final static int DATETIME_INT = "dateTime".hashCode(); + public final static int DATETIME_TZ_INT = "dateTime.tz".hashCode(); + public final static int TIME_INT = "time".hashCode(); + public final static int TIME_TZ_INT = "time.tz".hashCode(); + public final static int BOOLEAN_INT = "boolean".hashCode(); + public final static int BIN_BASE64_INT = "bin.base64".hashCode(); + public final static int BIN_HEX_INT = "bin.hex".hashCode(); + public final static int URI_INT = "uri".hashCode(); + public final static int UUID_INT = "uuid".hashCode(); + +} diff --git a/source/net/sbbi/upnp/services/UPNPService.java b/source/net/sbbi/upnp/services/UPNPService.java new file mode 100644 index 000000000..4c3bf2d90 --- /dev/null +++ b/source/net/sbbi/upnp/services/UPNPService.java @@ -0,0 +1,326 @@ +/* + * ============================================================================ + * 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 . + */ +package net.sbbi.upnp.services; + +import org.apache.commons.jxpath.*; +import org.apache.commons.jxpath.xml.*; + +import java.util.*; +import java.io.IOException; +import java.net.*; + +import net.sbbi.upnp.JXPathParser; +import net.sbbi.upnp.devices.*; + +/** + * Representation of an UPNP service + * @author SuperBonBon + * @version 1.0 + */ +public class UPNPService { + + protected String serviceType; + protected String serviceId; + private int specVersionMajor; + private int specVersionMinor; + protected URL SCPDURL; + protected String SCPDURLData; + protected URL controlURL; + protected URL eventSubURL; + protected UPNPDevice serviceOwnerDevice; + + protected Map UPNPServiceActions; + protected Map UPNPServiceStateVariables; + private String USN; + + private boolean parsedSCPD = false; + private DocumentContainer UPNPService; + + public UPNPService( JXPathContext serviceCtx, URL baseDeviceURL, UPNPDevice serviceOwnerDevice ) throws MalformedURLException { + this.serviceOwnerDevice = serviceOwnerDevice; + serviceType = (String)serviceCtx.getValue( "serviceType" ); + serviceId = (String)serviceCtx.getValue( "serviceId" ); + SCPDURL = UPNPRootDevice.getURL( (String)serviceCtx.getValue( "SCPDURL" ), baseDeviceURL ); + controlURL = UPNPRootDevice.getURL( (String)serviceCtx.getValue( "controlURL" ), baseDeviceURL ); + eventSubURL = UPNPRootDevice.getURL( (String)serviceCtx.getValue( "eventSubURL" ), baseDeviceURL ); + USN = serviceOwnerDevice.getUDN().concat( "::" ).concat( serviceType ); + } + + public String getServiceType() { + return serviceType; + } + + public String getServiceId() { + return serviceId; + } + + public String getUSN(){ + return USN; + } + + public URL getSCPDURL() { + return SCPDURL; + } + + public URL getControlURL() { + return controlURL; + } + + public URL getEventSubURL() { + return eventSubURL; + } + + public int getSpecVersionMajor() { + lazyInitiate(); + return specVersionMajor; + } + + public int getSpecVersionMinor() { + lazyInitiate(); + return specVersionMinor; + } + + public UPNPDevice getServiceOwnerDevice() { + return serviceOwnerDevice; + } + + /** + * Retreives a service action for its given name + * @param actionName the service action name + * @return a ServiceAction object or null if no matching action for this service has been found + */ + public ServiceAction getUPNPServiceAction( String actionName ) { + lazyInitiate(); + return (ServiceAction)UPNPServiceActions.get( actionName ); + } + + /** + * Retreives a service state variable for its given name + * @param stateVariableName the state variable name + * @return a ServiceStateVariable object or null if no matching state variable has been found + */ + public ServiceStateVariable getUPNPServiceStateVariable( String stateVariableName ) { + lazyInitiate(); + return (ServiceStateVariable)UPNPServiceStateVariables.get( stateVariableName ); + } + + public Iterator getAvailableActionsName() { + lazyInitiate(); + return UPNPServiceActions.keySet().iterator(); + } + + public int getAvailableActionsSize() { + lazyInitiate(); + return UPNPServiceActions.keySet().size(); + } + + public Iterator getAvailableStateVariableName() { + lazyInitiate(); + return UPNPServiceStateVariables.keySet().iterator(); + } + + public int getAvailableStateVariableSize() { + lazyInitiate(); + return UPNPServiceStateVariables.keySet().size(); + } + + private void parseSCPD() { + try { + DocumentContainer.registerXMLParser( DocumentContainer.MODEL_DOM, new JXPathParser() ); + UPNPService = new DocumentContainer( SCPDURL, DocumentContainer.MODEL_DOM ); + JXPathContext context = JXPathContext.newContext( this ); + Pointer rootPtr = context.getPointer( "UPNPService/scpd" ); + JXPathContext rootCtx = context.getRelativeContext( rootPtr ); + + specVersionMajor = Integer.parseInt( (String)rootCtx.getValue( "specVersion/major" ) ); + specVersionMinor = Integer.parseInt( (String)rootCtx.getValue( "specVersion/minor" ) ); + + parseServiceStateVariables( rootCtx ); + + Pointer actionsListPtr = rootCtx.getPointer( "actionList" ); + JXPathContext actionsListCtx = context.getRelativeContext( actionsListPtr ); + Double arraySize = (Double)actionsListCtx.getValue( "count( action )" ); + UPNPServiceActions = new HashMap(); + for ( int i = 1; i <= arraySize.intValue(); i++ ) { + ServiceAction action = new ServiceAction(); + action.name = (String)actionsListCtx.getValue( "action["+i+"]/name" ); + action.parent = this; + Pointer argumentListPtr = null; + try { + argumentListPtr = actionsListCtx.getPointer( "action["+i+"]/argumentList" ); + } catch ( JXPathException ex ) { + // there is no arguments list. + } + if ( argumentListPtr != null ) { + JXPathContext argumentListCtx = actionsListCtx.getRelativeContext( argumentListPtr ); + Double arraySizeArgs = (Double)argumentListCtx.getValue( "count( argument )" ); + + List orderedActionArguments = new ArrayList(); + for ( int z = 1; z <= arraySizeArgs.intValue(); z++ ) { + ServiceActionArgument arg = new ServiceActionArgument(); + arg.name = (String)argumentListCtx.getValue( "argument["+z+"]/name" ); + String direction = (String)argumentListCtx.getValue( "argument["+z+"]/direction" ); + arg.direction = direction.equals( ServiceActionArgument.DIRECTION_IN ) ? ServiceActionArgument.DIRECTION_IN : ServiceActionArgument.DIRECTION_OUT; + String stateVarName = (String)argumentListCtx.getValue( "argument["+z+"]/relatedStateVariable" ); + ServiceStateVariable stateVar = (ServiceStateVariable)UPNPServiceStateVariables.get( stateVarName ); + if ( stateVar == null ) { + throw new IllegalArgumentException( "Unable to find any state variable named " + stateVarName + " for service " + getServiceId() + " action " + action.name + " argument " + arg.name ); + } + arg.relatedStateVariable = stateVar; + orderedActionArguments.add( arg ); + } + + if ( arraySizeArgs.intValue() > 0 ) { + action.setActionArguments( orderedActionArguments ); + } + } + + UPNPServiceActions.put( action.getName(), action ); + } + parsedSCPD = true; + } catch ( Throwable t ) { + throw new RuntimeException( "Error during lazy SCDP file parsing at " + SCPDURL, t ); + } + } + + private void parseServiceStateVariables( JXPathContext rootContext ) { + Pointer serviceStateTablePtr = rootContext.getPointer( "serviceStateTable" ); + JXPathContext serviceStateTableCtx = rootContext.getRelativeContext( serviceStateTablePtr ); + Double arraySize = (Double)serviceStateTableCtx.getValue( "count( stateVariable )" ); + UPNPServiceStateVariables = new HashMap(); + for ( int i = 1; i <= arraySize.intValue(); i++ ) { + ServiceStateVariable srvStateVar = new ServiceStateVariable(); + String sendEventsLcl = null; + try { + sendEventsLcl = (String)serviceStateTableCtx.getValue( "stateVariable["+i+"]/@sendEvents" ); + } catch ( JXPathException defEx ) { + // sendEvents not provided defaulting according to specs to "yes" + sendEventsLcl = "yes"; + } + srvStateVar.parent = this; + srvStateVar.sendEvents = sendEventsLcl.equalsIgnoreCase( "no" ) ? false : true; + srvStateVar.name = (String)serviceStateTableCtx.getValue( "stateVariable["+i+"]/name" ); + srvStateVar.dataType = (String)serviceStateTableCtx.getValue( "stateVariable["+i+"]/dataType" ); + try { + srvStateVar.defaultValue = (String)serviceStateTableCtx.getValue( "stateVariable["+i+"]/defaultValue" ); + } catch ( JXPathException defEx ) { + // can happend since default value is not + } + Pointer allowedValuesPtr = null; + try { + allowedValuesPtr = serviceStateTableCtx.getPointer( "stateVariable["+i+"]/allowedValueList" ); + } catch ( JXPathException ex ) { + // there is no allowed values list. + } + if ( allowedValuesPtr != null ) { + JXPathContext allowedValuesCtx = serviceStateTableCtx.getRelativeContext( allowedValuesPtr ); + Double arraySizeAllowed = (Double)allowedValuesCtx.getValue( "count( allowedValue )" ); + srvStateVar.allowedvalues = new HashSet(); + for ( int z = 1; z <= arraySizeAllowed.intValue(); z++ ) { + String allowedValue = (String)allowedValuesCtx.getValue( "allowedValue["+z+"]" ); + srvStateVar.allowedvalues.add( allowedValue ); + } + } + + Pointer allowedValueRangePtr = null; + try { + allowedValueRangePtr = serviceStateTableCtx.getPointer( "stateVariable["+i+"]/allowedValueRange" ); + } catch ( JXPathException ex ) { + // there is no allowed values list, can happen + } + if ( allowedValueRangePtr != null ) { + + srvStateVar.minimumRangeValue = (String)serviceStateTableCtx.getValue( "stateVariable["+i+"]/allowedValueRange/minimum" ); + srvStateVar.maximumRangeValue = (String)serviceStateTableCtx.getValue( "stateVariable["+i+"]/allowedValueRange/maximum" ); + try { + srvStateVar.stepRangeValue = (String)serviceStateTableCtx.getValue( "stateVariable["+i+"]/allowedValueRange/step" ); + } catch ( JXPathException stepEx ) { + // can happend since step is not mandatory + } + } + UPNPServiceStateVariables.put( srvStateVar.getName(), srvStateVar ); + } + + } + + private void lazyInitiate() { + if ( !parsedSCPD ) + synchronized( this ) { + if ( !parsedSCPD ) + parseSCPD(); + } + } + + /** + * Used for JXPath parsing, do not use this method + * @return a Container object for Xpath parsing capabilities + */ + public Container getUPNPService() { + return UPNPService; + } + + public String getSCDPData() { + if ( SCPDURLData == null ) { + try { + + java.io.InputStream in = SCPDURL.openConnection().getInputStream(); + int readen = 0; + byte[] buff = new byte[512]; + StringBuffer strBuff = new StringBuffer(); + while( ( readen = in.read( buff ) ) != -1 ) { + strBuff.append( new String( buff, 0, readen ) ); + } + in.close(); + SCPDURLData = strBuff.toString(); + } catch ( IOException ioEx ) { + return null; + } + } + return SCPDURLData; + } +} diff --git a/source/net/sbbi/upnp/version.properties b/source/net/sbbi/upnp/version.properties new file mode 100644 index 000000000..d948172d8 --- /dev/null +++ b/source/net/sbbi/upnp/version.properties @@ -0,0 +1,8 @@ +# please make sur to update those values +# when crating a new release +# especially to create cvs changelogs +release.version=1.0.4 +release.version.cvs.tag=V1_0_4 +release.version.publish.date=20 Nov 2006 +last.release.version.cvs.tag=V1_0_3 +last.release.version.publish.date=14 Feb 2006 \ No newline at end of file