a9a304f31504a960aca3b07df3c037f3a2ef4452
[profile/ivi/rygel.git] / src / librygel-core / rygel-root-device-factory.vala
1 /*
2  * Copyright (C) 2008-2010 Nokia Corporation.
3  * Copyright (C) 2008 Zeeshan Ali (Khattak) <zeeshanak@gnome.org>.
4  * Copyright (C) 2007 OpenedHand Ltd.
5  * Copyright (C) 2012 Openismus GmbH.
6  *
7  * Authors: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
8  *                                <zeeshan.ali@nokia.com>
9  *          Jorn Baayen <jorn@openedhand.com>
10  *          Jens Georg <jensg@openismus.com>
11  *
12  * This file is part of Rygel.
13  *
14  * Rygel is free software; you can redistribute it and/or modify
15  * it under the terms of the GNU Lesser General Public License as published by
16  * the Free Software Foundation; either version 2 of the License, or
17  * (at your option) any later version.
18  *
19  * Rygel is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU Lesser General Public License for more details.
23  *
24  * You should have received a copy of the GNU Lesser General Public License
25  * along with this program; if not, write to the Free Software Foundation,
26  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27  */
28
29 using GUPnP;
30
31 public errordomain RootDeviceFactoryError {
32     XML_PARSE,
33 }
34
35 /**
36  * Factory for RootDevice objects. Give it a plugin and it will create a
37  * Root device for that.
38  */
39 public class Rygel.RootDeviceFactory {
40     public GUPnP.Context context;
41
42     private Configuration config;
43
44     private string desc_dir;
45
46     public RootDeviceFactory (GUPnP.Context context) throws GLib.Error {
47         this.config = MetaConfig.get_default ();
48         this.context = context;
49
50         /* We store the modified descriptions in the user's config dir */
51         var config_dir = Environment.get_user_config_dir ();
52         this.ensure_dir_exists (config_dir);
53         this.desc_dir = Path.build_filename (config_dir,
54                                              Environment.get_application_name ());
55         this.ensure_dir_exists (this.desc_dir);
56     }
57
58     public RootDevice create (Plugin plugin) throws GLib.Error {
59         var desc_path = Path.build_filename (this.desc_dir,
60                                              plugin.name + ".xml");
61         var template_path = plugin.desc_path;
62
63         /* Create the description xml */
64         var doc = this.create_desc (plugin, desc_path, template_path);
65
66         var device = new RootDevice (this.context,
67                                      plugin,
68                                      doc,
69                                      desc_path,
70                                      BuildConfig.DATA_DIR);
71         plugin.apply_hacks (device, desc_path);
72
73         return device;
74     }
75
76     private XMLDoc create_desc (Plugin plugin,
77                                 string desc_path,
78                                 string template_path) throws GLib.Error {
79         var doc = this.get_latest_doc (desc_path, template_path);
80
81         /* Modify description to include Plugin-specific stuff */
82         this.prepare_desc_for_plugin (doc, plugin);
83
84         var file = new DescriptionFile.from_xml_document (doc);
85         file.save (desc_path);
86
87         return doc;
88     }
89
90     private void prepare_desc_for_plugin (XMLDoc doc, Plugin plugin) {
91         Xml.Node *device_element;
92
93         device_element = XMLUtils.get_element ((Xml.Node *) doc.doc,
94                                                "root",
95                                                "device",
96                                                null);
97         if (device_element == null) {
98             warning (_("XML node '%s' not found."), "/root/device");
99
100             return;
101         }
102
103         /* First, set the Friendly name and UDN */
104         this.set_friendly_name_and_udn (device_element,
105                                         plugin.name,
106                                         plugin.title);
107         this.set_dlnacap (device_element);
108
109         if (plugin.description != null) {
110             this.set_description (device_element, plugin.description);
111         }
112
113         /* Then list each icon */
114         this.add_icons_to_desc (device_element, plugin);
115
116         /* Then list each service */
117         this.add_services_to_desc (device_element, plugin);
118     }
119
120     /**
121      * Fills the description doc @doc with a friendly name, and UDN from gconf.
122      * If these keys are not present in gconf, they are set with default values.
123      */
124     private void set_friendly_name_and_udn (Xml.Node *device_element,
125                                             string    plugin_name,
126                                             string    plugin_title) {
127         /* friendlyName */
128         Xml.Node *element = XMLUtils.get_element (device_element,
129                                                   "friendlyName",
130                                                   null);
131         if (element == null) {
132             warning (_("XML node '%s' not found."),
133                        "/root/device/friendlyName");
134
135             return;
136         }
137
138         string title;
139         try {
140             title = this.config.get_title (plugin_name);
141         } catch (GLib.Error err) {
142             title = plugin_title;
143         }
144
145         title = title.replace ("@REALNAME@", Environment.get_real_name ());
146         title = title.replace ("@USERNAME@", Environment.get_user_name ());
147         title = title.replace ("@HOSTNAME@", Environment.get_host_name ());
148
149         element->set_content (title);
150
151         /* UDN */
152         element = XMLUtils.get_element (device_element, "UDN");
153         if (element == null) {
154             warning (_("XML node '%s' not found."), "/root/device/UDN");
155
156             return;
157         }
158
159         var udn = element->get_content ();
160         if (udn == null || udn == "") {
161             udn = this.generate_random_udn ();
162
163             element->set_content (udn);
164         }
165     }
166
167     private void set_dlnacap (Xml.Node *device_element) {
168         var element = XMLUtils.get_element (device_element,
169                                             "X_DLNACAP",
170                                             null);
171
172         var content = "";
173         var allow_upload = true;
174         var allow_delete = false;
175
176         try {
177             allow_upload = config.get_allow_upload ();
178             allow_delete = config.get_allow_deletion ();
179         } catch (Error error) { }
180
181         if (allow_upload) {
182             content += "av-upload,image-upload,audio-upload";
183         }
184
185         if (allow_delete) {
186             content += ",create-item-with-OCM-destroy-item";
187         }
188         element->set_content (content);
189     }
190
191     private void set_description (Xml.Node *device_element,
192                                   string    description) {
193         Xml.Node *element = XMLUtils.get_element (device_element,
194                                                   "modelDescription",
195                                                   null);
196         if (element == null) {
197             device_element->new_child (null, "modelDescription", description);
198         }
199
200         element->set_content (description);
201     }
202
203     private void add_services_to_desc (Xml.Node *device_element,
204                                        Plugin    plugin) {
205         Xml.Node *service_list_node = XMLUtils.get_element (device_element,
206                                                             "serviceList",
207                                                             null);
208         if (service_list_node == null) {
209             warning (_("XML node '%s' not found."), "/root/device/serviceList");
210
211             return;
212         }
213
214         // Clear the existing service list first
215         service_list_node->set_content ("");
216
217         foreach (ResourceInfo resource_info in plugin.resource_infos) {
218             // FIXME: We only support plugable services for now
219             if (resource_info.type.is_a (typeof (Service))) {
220                     this.add_service_to_desc (service_list_node,
221                                               plugin.name,
222                                               resource_info);
223             }
224         }
225     }
226
227     private void add_service_to_desc (Xml.Node    *service_list_node,
228                                       string       plugin_name,
229                                       ResourceInfo resource_info) {
230         // Now create the service node
231         Xml.Node *service_node = service_list_node->new_child (null, "service");
232
233         service_node->new_child (null, "serviceType", resource_info.upnp_type);
234         service_node->new_child (null, "serviceId", resource_info.upnp_id);
235
236         /* Now the relative (to base URL) URLs*/
237         string url = "/" + resource_info.description_path;
238         service_node->new_child (null, "SCPDURL", url);
239
240         url = "/Event/" + plugin_name + "/" + resource_info.type.name ();
241         service_node->new_child (null, "eventSubURL", url);
242
243         url = "/Control/" + plugin_name + "/" + resource_info.type.name ();
244         service_node->new_child (null, "controlURL", url);
245     }
246
247     private void add_icons_to_desc (Xml.Node *device_element,
248                                     Plugin    plugin) {
249         var icons = plugin.icon_infos;
250
251         if (icons == null || icons.size == 0) {
252             debug ("No icon provided by plugin '%s'. Using Rygel logo.",
253                    plugin.name);
254
255             icons = plugin.default_icons;
256         }
257
258         Xml.Node *icon_list_node = XMLUtils.get_element (device_element,
259                                                          "iconList",
260                                                          null);
261         if (icon_list_node == null) {
262             icon_list_node = device_element->new_child (null, "iconList", null);
263         } else {
264             // Clear the existing icon list first
265             icon_list_node->set_content ("");
266         }
267
268         foreach (var icon in icons) {
269             add_icon_to_desc (icon_list_node, icon, plugin);
270         }
271     }
272
273     private void add_icon_to_desc (Xml.Node *icon_list_node,
274                                    IconInfo  icon_info,
275                                    Plugin    plugin) {
276         // Create the service node
277         Xml.Node *icon_node = icon_list_node->new_child (null, "icon");
278
279         string width = icon_info.width.to_string ();
280         string height = icon_info.height.to_string ();
281         string depth = icon_info.depth.to_string ();
282
283         icon_node->new_child (null, "mimetype", icon_info.mime_type);
284         icon_node->new_child (null, "width", width);
285         icon_node->new_child (null, "height", height);
286         icon_node->new_child (null, "depth", depth);
287
288         var uri = icon_info.uri;
289
290         if (uri.has_prefix ("file://")) {
291             // /PLUGIN_NAME-WIDTHxHEIGHTxDEPTH.png
292             var remote_path = "/" + plugin.name + "-" +
293                               width + "x" +
294                               height + "x" +
295                               depth + "." + icon_info.file_extension;
296             var local_path = uri.substring (7);
297
298             this.context.host_path (local_path, remote_path);
299             icon_node->new_child (null, "url", remote_path);
300         } else {
301             uri = uri.replace ("@ADDRESS@", this.context.host_ip);
302             icon_node->new_child (null, "url", uri);
303         }
304     }
305
306     private XMLDoc get_latest_doc (string path1,
307                                    string path2) throws GLib.Error {
308         var file = File.new_for_path (path1);
309         if (!file.query_exists (null)) {
310             return new XMLDoc.from_path (path2);
311         }
312
313         var info = file.query_info (FileAttribute.TIME_MODIFIED,
314                                     FileQueryInfoFlags.NONE);
315         var mod1 = info.get_attribute_uint64 (FileAttribute.TIME_MODIFIED);
316
317         file = File.new_for_path (path2);
318         info = file.query_info (FileAttribute.TIME_MODIFIED,
319                                 FileQueryInfoFlags.NONE);
320         var mod2 = info.get_attribute_uint64 (FileAttribute.TIME_MODIFIED);
321
322         if (mod1 > mod2) {
323             // If we fail to load the derived description file, try the
324             // template instead.
325             try {
326                 return new XMLDoc.from_path (path1);
327             } catch (Error error) {
328                 return new XMLDoc.from_path (path2);
329             }
330         } else {
331             return new XMLDoc.from_path (path2);
332         }
333     }
334
335     private void ensure_dir_exists (string dir_path) throws Error {
336         var file = File.new_for_path (dir_path);
337         if (!file.query_exists (null)) {
338             file.make_directory (null);
339         }
340     }
341
342     private string generate_random_udn () {
343         var udn = new uchar[50];
344         var id = new uchar[16];
345
346         /* Generate new UUID */
347         UUID.generate (id);
348         UUID.unparse (id, udn);
349
350         return "uuid:" + (string) udn;
351     }
352 }