2 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
4 * Author: Jorn Baayen <jorn@openedhand.com>
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
16 * You should have received a copy of the GNU Library General Public
17 * License along with this library; if not, write to the
18 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
23 * SECTION:gupnp-control-point
24 * @short_description: Class for resource discovery.
26 * #GUPnPControlPoint handles device and service discovery. After creating
27 * a control point and activating it using gssdp_resource_browser_set_active(),
28 * the ::device-proxy-available, ::service-proxy-available,
29 * ::device-proxy-unavailable and ::service-proxy-unavailable signals will
30 * be emitted whenever the availability of a device or service matching
31 * the specified discovery target changes.
36 #include "gupnp-control-point.h"
37 #include "gupnp-context-private.h"
38 #include "gupnp-resource-factory-private.h"
39 #include "http-headers.h"
42 G_DEFINE_TYPE (GUPnPControlPoint,
44 GSSDP_TYPE_RESOURCE_BROWSER);
46 struct _GUPnPControlPointPrivate {
47 GUPnPResourceFactory *factory;
52 GHashTable *doc_cache;
59 PROP_RESOURCE_FACTORY,
63 DEVICE_PROXY_AVAILABLE,
64 DEVICE_PROXY_UNAVAILABLE,
65 SERVICE_PROXY_AVAILABLE,
66 SERVICE_PROXY_UNAVAILABLE,
70 static guint signals[SIGNAL_LAST];
73 GUPnPControlPoint *control_point;
77 char *description_url;
80 } GetDescriptionURLData;
83 get_description_url_data_free (GetDescriptionURLData *data)
85 data->control_point->priv->pending_gets =
86 g_list_remove (data->control_point->priv->pending_gets, data);
89 g_free (data->service_type);
90 g_free (data->description_url);
92 g_slice_free (GetDescriptionURLData, data);
96 gupnp_control_point_init (GUPnPControlPoint *control_point)
99 G_TYPE_INSTANCE_GET_PRIVATE (control_point,
100 GUPNP_TYPE_CONTROL_POINT,
101 GUPnPControlPointPrivate);
103 control_point->priv->doc_cache =
104 g_hash_table_new_full (g_str_hash,
110 /* Return TRUE if value == user_data */
112 find_doc (gpointer key,
116 return (value == user_data);
119 /* xmlDoc wrapper finalized */
121 doc_finalized (gpointer user_data,
122 GObject *where_the_object_was)
124 GUPnPControlPoint *control_point;
126 control_point = GUPNP_CONTROL_POINT (user_data);
128 g_hash_table_foreach_remove (control_point->priv->doc_cache,
130 where_the_object_was);
133 /* Release weak reference on xmlDoc wrapper */
135 weak_unref_doc (gpointer key,
139 g_object_weak_unref (G_OBJECT (value), doc_finalized, user_data);
143 gupnp_control_point_dispose (GObject *object)
145 GUPnPControlPoint *control_point;
146 GSSDPResourceBrowser *browser;
147 GObjectClass *object_class;
149 control_point = GUPNP_CONTROL_POINT (object);
150 browser = GSSDP_RESOURCE_BROWSER (control_point);
152 gssdp_resource_browser_set_active (browser, FALSE);
154 if (control_point->priv->factory) {
155 g_object_unref (control_point->priv->factory);
156 control_point->priv->factory = NULL;
159 /* Cancel any pending description file GETs */
160 while (control_point->priv->pending_gets) {
161 GetDescriptionURLData *data;
162 GUPnPContext *context;
163 SoupSession *session;
165 data = control_point->priv->pending_gets->data;
167 context = gupnp_control_point_get_context (control_point);
168 session = gupnp_context_get_session (context);
170 soup_session_cancel_message (session,
172 SOUP_STATUS_CANCELLED);
174 get_description_url_data_free (data);
177 /* Release weak references on remaining cached documents */
178 g_hash_table_foreach (control_point->priv->doc_cache,
183 object_class = G_OBJECT_CLASS (gupnp_control_point_parent_class);
184 object_class->dispose (object);
188 gupnp_control_point_finalize (GObject *object)
190 GUPnPControlPoint *control_point;
191 GObjectClass *object_class;
193 control_point = GUPNP_CONTROL_POINT (object);
195 g_hash_table_destroy (control_point->priv->doc_cache);
198 object_class = G_OBJECT_CLASS (gupnp_control_point_parent_class);
199 object_class->finalize (object);
203 find_service_node (GUPnPControlPoint *control_point,
205 const char *service_type)
209 l = control_point->priv->services;
212 GUPnPServiceInfo *info;
214 info = GUPNP_SERVICE_INFO (l->data);
216 if ((strcmp (gupnp_service_info_get_udn (info), udn) == 0) &&
217 (strcmp (gupnp_service_info_get_service_type (info),
228 find_device_node (GUPnPControlPoint *control_point,
233 l = control_point->priv->devices;
236 GUPnPDeviceInfo *info;
238 info = GUPNP_DEVICE_INFO (l->data);
240 if (strcmp (udn, gupnp_device_info_get_udn (info)) == 0)
250 create_and_report_service_proxy (GUPnPControlPoint *control_point,
254 const char *service_type,
255 const char *description_url,
258 GUPnPServiceProxy *proxy;
259 GUPnPResourceFactory *factory;
260 GUPnPContext *context;
262 if (find_service_node (control_point, udn, service_type) != NULL)
263 /* We already have a proxy for this service */
266 factory = gupnp_control_point_get_resource_factory (control_point);
267 context = gupnp_control_point_get_context (control_point);
270 proxy = gupnp_resource_factory_create_service_proxy (factory,
279 control_point->priv->services =
280 g_list_prepend (control_point->priv->services,
283 g_signal_emit (control_point,
284 signals[SERVICE_PROXY_AVAILABLE],
290 create_and_report_device_proxy (GUPnPControlPoint *control_point,
294 const char *description_url,
297 GUPnPDeviceProxy *proxy;
298 GUPnPResourceFactory *factory;
299 GUPnPContext *context;
301 if (find_device_node (control_point, udn) != NULL)
302 /* We already have a proxy for this device */
305 factory = gupnp_control_point_get_resource_factory (control_point);
306 context = gupnp_control_point_get_context (control_point);
308 proxy = gupnp_resource_factory_create_device_proxy (factory,
316 control_point->priv->devices = g_list_prepend
317 (control_point->priv->devices,
320 g_signal_emit (control_point,
321 signals[DEVICE_PROXY_AVAILABLE],
326 /* Search @element for matching services */
328 process_service_list (xmlNode *element,
329 GUPnPControlPoint *control_point,
332 const char *service_type,
333 const char *description_url,
336 g_object_ref (control_point);
338 for (element = element->children; element; element = element->next) {
342 if (strcmp ((char *) element->name, "service") != 0)
345 /* See if this is a matching service */
346 prop = xml_util_get_child_element_content (element,
351 match = (strcmp ((char *) prop, service_type) == 0);
360 create_and_report_service_proxy (control_point,
369 g_object_unref (control_point);
372 /* Recursively search @element for matching devices */
374 process_device_list (xmlNode *element,
375 GUPnPControlPoint *control_point,
378 const char *service_type,
379 const char *description_url,
382 g_object_ref (control_point);
384 for (element = element->children; element; element = element->next) {
389 if (strcmp ((char *) element->name, "device") != 0)
392 /* Recurse into children */
393 children = xml_util_get_element (element,
398 process_device_list (children,
407 /* See if this is a matching device */
408 prop = xml_util_get_child_element_content (element, "UDN");
412 match = (strcmp ((char *) prop, udn) == 0);
422 /* Dive into serviceList */
423 children = xml_util_get_element (element,
428 process_service_list (children,
437 create_and_report_device_proxy (control_point,
445 g_object_unref (control_point);
449 * Called when the description document is loaded.
452 description_loaded (GUPnPControlPoint *control_point,
455 const char *service_type,
456 const char *description_url)
461 /* Save the URL base, if any */
462 element = xml_util_get_element ((xmlNode *) doc->doc,
465 if (element == NULL) {
466 g_warning ("No 'root' element found in description document"
467 " '%s'. Ignoring device '%s'\n",
474 url_base = xml_util_get_child_element_content_uri (element,
478 url_base = soup_uri_new (description_url);
480 /* Iterate matching devices */
481 process_device_list (element,
490 soup_uri_free (url_base);
494 * Description URL downloaded.
497 got_description_url (SoupSession *session,
499 GetDescriptionURLData *data)
503 if (msg->status_code == SOUP_STATUS_CANCELLED)
506 /* Now, make sure again this document is not already cached. If it is,
507 * we re-use the cached one. */
508 doc = g_hash_table_lookup (data->control_point->priv->doc_cache,
509 data->description_url);
512 description_loaded (data->control_point,
516 data->description_url);
518 get_description_url_data_free (data);
524 if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
528 xml_doc = xmlRecoverMemory (msg->response_body->data,
529 msg->response_body->length);
531 doc = gupnp_xml_doc_new (xml_doc);
533 description_loaded (data->control_point,
537 data->description_url);
539 /* Insert into document cache */
541 (data->control_point->priv->doc_cache,
542 g_strdup (data->description_url),
545 /* Make sure the document is removed from the cache
547 g_object_weak_ref (G_OBJECT (doc),
549 data->control_point);
551 /* If no proxy was created, make sure doc is freed. */
552 g_object_unref (doc);
554 g_warning ("Failed to parse %s", data->description_url);
556 g_warning ("Failed to GET %s: %s",
557 data->description_url,
560 get_description_url_data_free (data);
564 * Downloads and parses (or takes from cache) @description_url,
566 * - A #GUPnPDeviceProxy for the device specified by @udn if @service_type
568 * - A #GUPnPServiceProxy for the service of type @service_type from the device
569 * specified by @udn if @service_type is not %NULL.
572 load_description (GUPnPControlPoint *control_point,
573 const char *description_url,
575 const char *service_type)
579 doc = g_hash_table_lookup (control_point->priv->doc_cache,
583 description_loaded (control_point,
589 /* Asynchronously download doc */
590 GUPnPContext *context;
591 SoupSession *session;
592 GetDescriptionURLData *data;
594 context = gupnp_control_point_get_context (control_point);
596 session = gupnp_context_get_session (context);
598 data = g_slice_new (GetDescriptionURLData);
600 data->message = soup_message_new (SOUP_METHOD_GET,
602 if (data->message == NULL) {
603 g_warning ("Invalid description URL: %s",
606 g_slice_free (GetDescriptionURLData, data);
611 http_request_set_accept_language (data->message);
613 data->control_point = control_point;
615 data->udn = g_strdup (udn);
616 data->service_type = g_strdup (service_type);
617 data->description_url = g_strdup (description_url);
619 control_point->priv->pending_gets =
620 g_list_prepend (control_point->priv->pending_gets,
623 soup_session_queue_message (session,
625 (SoupSessionCallback)
632 parse_usn (const char *usn,
642 *udn = *service_type = NULL;
644 /* Verify we have a valid USN */
645 if (strncmp (usn, "uuid:", strlen ("uuid:"))) {
646 g_warning ("Invalid USN: %s", usn);
652 bits = g_strsplit (usn, "::", -1);
655 count = g_strv_length (bits);
658 /* uuid:device-UUID */
664 } else if (count == 2) {
668 second_bits = g_strsplit (bits[1], ":", -1);
669 n_second_bits = g_strv_length (second_bits);
671 if (n_second_bits >= 2 &&
672 !strcmp (second_bits[0], "upnp") &&
673 !strcmp (second_bits[1], "rootdevice")) {
674 /* uuid:device-UUID::upnp:rootdevice */
679 } else if (n_second_bits >= 3 &&
680 !strcmp (second_bits[0], "urn")) {
681 /* uuid:device-UIID::urn:domain-name:service/device:
684 if (!strcmp (second_bits[2], "device")) {
688 } else if (!strcmp (second_bits[2], "service")) {
690 *service_type = bits[1];
696 g_strfreev (second_bits);
700 g_warning ("Invalid USN: %s", usn);
702 for (i = 0; i < count; i++) {
703 if ((bits[i] != *udn) &&
704 (bits[i] != *service_type))
714 gupnp_control_point_resource_available (GSSDPResourceBrowser *resource_browser,
716 const GList *locations)
718 GUPnPControlPoint *control_point;
719 char *udn, *service_type;
721 control_point = GUPNP_CONTROL_POINT (resource_browser);
723 /* Verify we have a location */
725 g_warning ("No Location header for device with USN %s", usn);
730 if (!parse_usn (usn, &udn, &service_type))
733 load_description (control_point,
739 g_free (service_type);
743 gupnp_control_point_resource_unavailable
744 (GSSDPResourceBrowser *resource_browser,
747 GUPnPControlPoint *control_point;
748 char *udn, *service_type;
750 control_point = GUPNP_CONTROL_POINT (resource_browser);
753 if (!parse_usn (usn, &udn, &service_type))
758 GList *l = find_service_node (control_point, udn, service_type);
761 GUPnPServiceProxy *proxy;
764 proxy = GUPNP_SERVICE_PROXY (l->data);
766 control_point->priv->services =
768 (control_point->priv->services, l);
770 g_signal_emit (control_point,
771 signals[SERVICE_PROXY_UNAVAILABLE],
775 g_object_unref (proxy);
778 GList *l = find_device_node (control_point, udn);
781 GUPnPDeviceProxy *proxy;
784 proxy = GUPNP_DEVICE_PROXY (l->data);
786 control_point->priv->devices =
788 (control_point->priv->devices, l);
790 g_signal_emit (control_point,
791 signals[DEVICE_PROXY_UNAVAILABLE],
795 g_object_unref (proxy);
800 g_free (service_type);
804 gupnp_control_point_set_property (GObject *object,
809 GUPnPControlPoint *control_point;
811 control_point = GUPNP_CONTROL_POINT (object);
813 switch (property_id) {
814 case PROP_RESOURCE_FACTORY:
815 control_point->priv->factory =
816 GUPNP_RESOURCE_FACTORY (g_value_dup_object (value));
819 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
825 gupnp_control_point_get_property (GObject *object,
830 GUPnPControlPoint *control_point;
832 control_point = GUPNP_CONTROL_POINT (object);
834 switch (property_id) {
835 case PROP_RESOURCE_FACTORY:
836 g_value_set_object (value,
837 gupnp_control_point_get_resource_factory (control_point));
840 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
846 gupnp_control_point_class_init (GUPnPControlPointClass *klass)
848 GObjectClass *object_class;
849 GSSDPResourceBrowserClass *browser_class;
851 object_class = G_OBJECT_CLASS (klass);
853 object_class->set_property = gupnp_control_point_set_property;
854 object_class->get_property = gupnp_control_point_get_property;
855 object_class->dispose = gupnp_control_point_dispose;
856 object_class->finalize = gupnp_control_point_finalize;
858 browser_class = GSSDP_RESOURCE_BROWSER_CLASS (klass);
860 browser_class->resource_available =
861 gupnp_control_point_resource_available;
862 browser_class->resource_unavailable =
863 gupnp_control_point_resource_unavailable;
865 g_type_class_add_private (klass, sizeof (GUPnPControlPointPrivate));
868 * GUPnPControlPoint:resource-factory:
870 * The resource factory to use. Set to NULL for default factory.
872 g_object_class_install_property
874 PROP_RESOURCE_FACTORY,
875 g_param_spec_object ("resource-factory",
877 "The resource factory to use",
878 GUPNP_TYPE_RESOURCE_FACTORY,
879 G_PARAM_CONSTRUCT_ONLY |
881 G_PARAM_STATIC_NAME |
882 G_PARAM_STATIC_NICK |
883 G_PARAM_STATIC_BLURB));
886 * GUPnPControlPoint::device-proxy-available:
887 * @control_point: The #GUPnPControlPoint that received the signal
888 * @proxy: The now available #GUPnPDeviceProxy
890 * The ::device-proxy-available signal is emitted whenever a new
891 * device has become available.
893 signals[DEVICE_PROXY_AVAILABLE] =
894 g_signal_new ("device-proxy-available",
895 GUPNP_TYPE_CONTROL_POINT,
897 G_STRUCT_OFFSET (GUPnPControlPointClass,
898 device_proxy_available),
901 g_cclosure_marshal_VOID__OBJECT,
904 GUPNP_TYPE_DEVICE_PROXY);
907 * GUPnPControlPoint::device-proxy-unavailable:
908 * @control_point: The #GUPnPControlPoint that received the signal
909 * @proxy: The now unavailable #GUPnPDeviceProxy
911 * The ::device-proxy-unavailable signal is emitted whenever a
912 * device is not available any more.
914 signals[DEVICE_PROXY_UNAVAILABLE] =
915 g_signal_new ("device-proxy-unavailable",
916 GUPNP_TYPE_CONTROL_POINT,
918 G_STRUCT_OFFSET (GUPnPControlPointClass,
919 device_proxy_unavailable),
922 g_cclosure_marshal_VOID__OBJECT,
925 GUPNP_TYPE_DEVICE_PROXY);
928 * GUPnPControlPoint::service-proxy-available:
929 * @control_point: The #GUPnPControlPoint that received the signal
930 * @proxy: The now available #GUPnPServiceProxy
932 * The ::service-proxy-available signal is emitted whenever a new
933 * service has become available.
935 signals[SERVICE_PROXY_AVAILABLE] =
936 g_signal_new ("service-proxy-available",
937 GUPNP_TYPE_CONTROL_POINT,
939 G_STRUCT_OFFSET (GUPnPControlPointClass,
940 service_proxy_available),
943 g_cclosure_marshal_VOID__OBJECT,
946 GUPNP_TYPE_SERVICE_PROXY);
949 * GUPnPControlPoint::service-proxy-unavailable:
950 * @control_point: The #GUPnPControlPoint that received the signal
951 * @proxy: The now unavailable #GUPnPServiceProxy
953 * The ::service-proxy-unavailable signal is emitted whenever a
954 * service is not available any more.
956 signals[SERVICE_PROXY_UNAVAILABLE] =
957 g_signal_new ("service-proxy-unavailable",
958 GUPNP_TYPE_CONTROL_POINT,
960 G_STRUCT_OFFSET (GUPnPControlPointClass,
961 service_proxy_unavailable),
964 g_cclosure_marshal_VOID__OBJECT,
967 GUPNP_TYPE_SERVICE_PROXY);
971 * gupnp_control_point_new:
972 * @context: A #GUPnPContext
973 * @target: The search target
975 * Create a new #GUPnPControlPoint with the specified @context and @target.
977 * @target should be a service or device name, such as
978 * <literal>urn:schemas-upnp-org:service:WANIPConnection:1</literal> or
979 * <literal>urn:schemas-upnp-org:device:MediaRenderer:1</literal>.
981 * Return value: A new #GUPnPControlPoint object.
984 gupnp_control_point_new (GUPnPContext *context,
987 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
988 g_return_val_if_fail (target, NULL);
990 return g_object_new (GUPNP_TYPE_CONTROL_POINT,
997 * gupnp_control_point_new_full:
998 * @context: A #GUPnPContext
999 * @factory: A #GUPnPResourceFactory
1000 * @target: The search target
1002 * Create a new #GUPnPControlPoint with the specified @context, @factory and
1005 * @target should be a service or device name, such as
1006 * <literal>urn:schemas-upnp-org:service:WANIPConnection:1</literal> or
1007 * <literal>urn:schemas-upnp-org:device:MediaRenderer:1</literal>.
1009 * Return value: A new #GUPnPControlPoint object.
1012 gupnp_control_point_new_full (GUPnPContext *context,
1013 GUPnPResourceFactory *factory,
1016 g_return_val_if_fail (GUPNP_IS_CONTEXT (context), NULL);
1017 g_return_val_if_fail (factory == NULL ||
1018 GUPNP_IS_RESOURCE_FACTORY (factory), NULL);
1019 g_return_val_if_fail (target, NULL);
1021 return g_object_new (GUPNP_TYPE_CONTROL_POINT,
1024 "resource-factory", factory,
1029 * gupnp_control_point_get_context:
1030 * @control_point: A #GUPnPControlPoint
1032 * Get the #GUPnPControlPoint associated with @control_point.
1034 * Returns: (transfer none): The #GUPnPContext.
1037 gupnp_control_point_get_context (GUPnPControlPoint *control_point)
1039 GSSDPClient *client;
1041 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1043 client = gssdp_resource_browser_get_client
1044 (GSSDP_RESOURCE_BROWSER (control_point));
1046 return GUPNP_CONTEXT (client);
1050 * gupnp_control_point_list_device_proxies:
1051 * @control_point: A #GUPnPControlPoint
1053 * Get the #GList of discovered #GUPnPDeviceProxy objects. Do not free the list
1056 * Return value: (element-type GUPnP.DeviceProxy) (transfer none): a #GList of
1057 * #GUPnPDeviceProxy objects.
1060 gupnp_control_point_list_device_proxies (GUPnPControlPoint *control_point)
1062 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1064 return (const GList *) control_point->priv->devices;
1068 * gupnp_control_point_list_service_proxies:
1069 * @control_point: A #GUPnPControlPoint
1071 * Get the #GList of discovered #GUPnPServiceProxy objects. Do not free the
1072 * list nor its elements.
1074 * Return value: (element-type GUPnP.ServiceProxy) (transfer none): a #GList
1075 * of #GUPnPServiceProxy objects.
1078 gupnp_control_point_list_service_proxies (GUPnPControlPoint *control_point)
1080 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1082 return (const GList *) control_point->priv->services;
1086 * gupnp_control_point_get_resource_factory:
1087 * @control_point: A #GUPnPControlPoint
1089 * Get the #GUPnPResourceFactory used by the @control_point.
1091 * Returns: (transfer none): A #GUPnPResourceFactory.
1093 GUPnPResourceFactory *
1094 gupnp_control_point_get_resource_factory (GUPnPControlPoint *control_point)
1096 g_return_val_if_fail (GUPNP_IS_CONTROL_POINT (control_point), NULL);
1098 if (control_point->priv->factory)
1099 return control_point->priv->factory;
1101 return gupnp_resource_factory_get_default ();