View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program 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.
14   */
15  
16  package org.fourthline.cling.protocol;
17  
18  import java.net.URL;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.List;
22  import java.util.Set;
23  import java.util.concurrent.CopyOnWriteArraySet;
24  import java.util.logging.Level;
25  import java.util.logging.Logger;
26  
27  import org.fourthline.cling.UpnpService;
28  import org.fourthline.cling.binding.xml.DescriptorBindingException;
29  import org.fourthline.cling.binding.xml.DeviceDescriptorBinder;
30  import org.fourthline.cling.binding.xml.ServiceDescriptorBinder;
31  import org.fourthline.cling.model.ValidationError;
32  import org.fourthline.cling.model.ValidationException;
33  import org.fourthline.cling.model.message.StreamRequestMessage;
34  import org.fourthline.cling.model.message.StreamResponseMessage;
35  import org.fourthline.cling.model.message.UpnpHeaders;
36  import org.fourthline.cling.model.message.UpnpRequest;
37  import org.fourthline.cling.model.meta.Icon;
38  import org.fourthline.cling.model.meta.RemoteDevice;
39  import org.fourthline.cling.model.meta.RemoteService;
40  import org.fourthline.cling.model.types.ServiceType;
41  import org.fourthline.cling.model.types.UDN;
42  import org.fourthline.cling.registry.RegistrationException;
43  import org.fourthline.cling.transport.RouterException;
44  import org.seamless.util.Exceptions;
45  
46  /**
47   * Retrieves all remote device XML descriptors, parses them, creates an immutable device and service metadata graph.
48   * <p>
49   * This implementation encapsulates all steps which are necessary to create a fully usable and populated
50   * device metadata graph of a particular UPnP device. It starts with an unhydrated and typically just
51   * discovered {@link org.fourthline.cling.model.meta.RemoteDevice}, the only property that has to be available is
52   * its {@link org.fourthline.cling.model.meta.RemoteDeviceIdentity}.
53   * </p>
54   * <p>
55   * This protocol implementation will then retrieve the device's XML descriptor, parse it, and retrieve and
56   * parse all service descriptors until all device and service metadata has been retrieved. The fully
57   * hydrated device is then added to the {@link org.fourthline.cling.registry.Registry}.
58   * </p>
59   * <p>
60   * Any descriptor retrieval, parsing, or validation error of the metadata will abort this protocol
61   * with a warning message in the log.
62   * </p>
63   *
64   * @author Christian Bauer
65   */
66  public class RetrieveRemoteDescriptors implements Runnable {
67  
68      final private static Logger log = Logger.getLogger(RetrieveRemoteDescriptors.class.getName());
69  
70      private final UpnpService upnpService;
71      private RemoteDevice rd;
72  
73      private static final Set<URL> activeRetrievals = new CopyOnWriteArraySet();
74      protected List<UDN> errorsAlreadyLogged = new ArrayList<>();
75  
76      public RetrieveRemoteDescriptors(UpnpService upnpService, RemoteDevice rd) {
77          this.upnpService = upnpService;
78          this.rd = rd;
79      }
80  
81      public UpnpService getUpnpService() {
82          return upnpService;
83      }
84  
85      public void run() {
86  
87          URL deviceURL = rd.getIdentity().getDescriptorURL();
88  
89          // Performance optimization, try to avoid concurrent GET requests for device descriptor,
90          // if we retrieve it once, we have the hydrated device. There is no different outcome
91          // processing this several times concurrently.
92  
93          if (activeRetrievals.contains(deviceURL)) {
94              log.finer("Exiting early, active retrieval for URL already in progress: " + deviceURL);
95              return;
96          }
97  
98          // Exit if it has been discovered already, could be we have been waiting in the executor queue too long
99          if (getUpnpService().getRegistry().getRemoteDevice(rd.getIdentity().getUdn(), true) != null) {
100             log.finer("Exiting early, already discovered: " + deviceURL);
101             return;
102         }
103 
104         try {
105             activeRetrievals.add(deviceURL);
106             describe();
107         } catch (RouterException ex) {
108             log.log(Level.WARNING,
109                 "Descriptor retrieval failed: " + deviceURL,
110                 ex
111             );
112         } finally {
113             activeRetrievals.remove(deviceURL);
114         }
115     }
116 
117     protected void describe() throws RouterException {
118 
119         // All of the following is a very expensive and time consuming procedure, thanks to the
120         // braindead design of UPnP. Several GET requests, several descriptors, several XML parsing
121         // steps - all of this could be done with one and it wouldn't make a difference. So every
122         // call of this method has to be really necessary and rare.
123 
124     	if(getUpnpService().getRouter() == null) {
125     		log.warning("Router not yet initialized");
126     		return ;
127     	}
128 
129     	StreamRequestMessage deviceDescRetrievalMsg;
130     	StreamResponseMessage deviceDescMsg;
131 
132     	try {
133 
134     		deviceDescRetrievalMsg =
135                 new StreamRequestMessage(UpnpRequest.Method.GET, rd.getIdentity().getDescriptorURL());
136 
137             // Extra headers
138             UpnpHeaders headers =
139                 getUpnpService().getConfiguration().getDescriptorRetrievalHeaders(rd.getIdentity());
140             if (headers != null)
141                 deviceDescRetrievalMsg.getHeaders().putAll(headers);
142 
143     		log.fine("Sending device descriptor retrieval message: " + deviceDescRetrievalMsg);
144             deviceDescMsg = getUpnpService().getRouter().send(deviceDescRetrievalMsg);
145 
146     	} catch(IllegalArgumentException ex) {
147     		// UpnpRequest constructor can throw IllegalArgumentException on invalid URI
148     		// IllegalArgumentException can also be thrown by Apache HttpClient on blank URI in send()
149             log.warning(
150                 "Device descriptor retrieval failed: "
151                 + rd.getIdentity().getDescriptorURL()
152                 + ", possibly invalid URL: " + ex);
153             return ;
154         }
155 
156         if (deviceDescMsg == null) {
157             log.warning(
158                 "Device descriptor retrieval failed, no response: " + rd.getIdentity().getDescriptorURL()
159             );
160             return;
161         }
162 
163         if (deviceDescMsg.getOperation().isFailed()) {
164             log.warning(
165                     "Device descriptor retrieval failed: "
166                             + rd.getIdentity().getDescriptorURL() +
167                             ", "
168                             + deviceDescMsg.getOperation().getResponseDetails()
169             );
170             return;
171         }
172 
173         if (!deviceDescMsg.isContentTypeTextUDA()) {
174             log.fine(
175                 "Received device descriptor without or with invalid Content-Type: "
176                     + rd.getIdentity().getDescriptorURL());
177             // We continue despite the invalid UPnP message because we can still hope to convert the content
178         }
179 
180         String descriptorContent = deviceDescMsg.getBodyString();
181         if (descriptorContent == null || descriptorContent.length() == 0) {
182             log.warning("Received empty device descriptor:" + rd.getIdentity().getDescriptorURL());
183             return;
184         }
185 
186         log.fine("Received root device descriptor: " + deviceDescMsg);
187         describe(descriptorContent);
188     }
189 
190     protected void describe(String descriptorXML) throws RouterException {
191 
192         boolean notifiedStart = false;
193         RemoteDevice describedDevice = null;
194         try {
195 
196             DeviceDescriptorBinder deviceDescriptorBinder =
197                     getUpnpService().getConfiguration().getDeviceDescriptorBinderUDA10();
198 
199             describedDevice = deviceDescriptorBinder.describe(
200                     rd,
201                     descriptorXML
202             );
203 
204             log.fine("Remote device described (without services) notifying listeners: " + describedDevice);
205             notifiedStart = getUpnpService().getRegistry().notifyDiscoveryStart(describedDevice);
206 
207             log.fine("Hydrating described device's services: " + describedDevice);
208             RemoteDevice hydratedDevice = describeServices(describedDevice);
209             if (hydratedDevice == null) {
210             	if(!errorsAlreadyLogged.contains(rd.getIdentity().getUdn())) {
211             		errorsAlreadyLogged.add(rd.getIdentity().getUdn());
212             		log.warning("Device service description failed: " + rd);
213             	}
214                 if (notifiedStart)
215                     getUpnpService().getRegistry().notifyDiscoveryFailure(
216                             describedDevice,
217                             new DescriptorBindingException("Device service description failed: " + rd)
218                     );
219                 return;
220             } else {
221                 log.fine("Adding fully hydrated remote device to registry: " + hydratedDevice);
222                 // The registry will do the right thing: A new root device is going to be added, if it's
223                 // already present or we just received the descriptor again (because we got an embedded
224                 // devices' notification), it will simply update the expiration timestamp of the root
225                 // device.
226                 getUpnpService().getRegistry().addDevice(hydratedDevice);
227             }
228 
229         } catch (ValidationException ex) {
230     		// Avoid error log spam each time device is discovered, errors are logged once per device.
231         	if(!errorsAlreadyLogged.contains(rd.getIdentity().getUdn())) {
232         		errorsAlreadyLogged.add(rd.getIdentity().getUdn());
233         		log.warning("Could not validate device model: " + rd);
234         		for (ValidationError validationError : ex.getErrors()) {
235         			log.warning(validationError.toString());
236         		}
237                 if (describedDevice != null && notifiedStart)
238                     getUpnpService().getRegistry().notifyDiscoveryFailure(describedDevice, ex);
239         	}
240 
241         } catch (DescriptorBindingException ex) {
242             log.warning("Could not hydrate device or its services from descriptor: " + rd);
243             log.warning("Cause was: " + Exceptions.unwrap(ex));
244             if (describedDevice != null && notifiedStart)
245                 getUpnpService().getRegistry().notifyDiscoveryFailure(describedDevice, ex);
246 
247         } catch (RegistrationException ex) {
248             log.warning("Adding hydrated device to registry failed: " + rd);
249             log.warning("Cause was: " + ex.toString());
250             if (describedDevice != null && notifiedStart)
251                 getUpnpService().getRegistry().notifyDiscoveryFailure(describedDevice, ex);
252         }
253     }
254 
255     protected RemoteDevice describeServices(RemoteDevice currentDevice)
256             throws RouterException, DescriptorBindingException, ValidationException {
257 
258         List<RemoteService> describedServices = new ArrayList<>();
259         if (currentDevice.hasServices()) {
260             List<RemoteService> filteredServices = filterExclusiveServices(currentDevice.getServices());
261             for (RemoteService service : filteredServices) {
262                 RemoteService svc = describeService(service);
263                  // Skip invalid services (yes, we can continue with only some services available)
264                 if (svc != null)
265                     describedServices.add(svc);
266                 else
267                     log.warning("Skipping invalid service '" + service + "' of: " + currentDevice);
268             }
269         }
270 
271         List<RemoteDevice> describedEmbeddedDevices = new ArrayList<>();
272         if (currentDevice.hasEmbeddedDevices()) {
273             for (RemoteDevice embeddedDevice : currentDevice.getEmbeddedDevices()) {
274                  // Skip invalid embedded device
275                 if (embeddedDevice == null)
276                     continue;
277                 RemoteDevice describedEmbeddedDevice = describeServices(embeddedDevice);
278                  // Skip invalid embedded services
279                 if (describedEmbeddedDevice != null)
280                     describedEmbeddedDevices.add(describedEmbeddedDevice);
281             }
282         }
283 
284         Icon[] iconDupes = new Icon[currentDevice.getIcons().length];
285         for (int i = 0; i < currentDevice.getIcons().length; i++) {
286             Icon icon = currentDevice.getIcons()[i];
287             iconDupes[i] = icon.deepCopy();
288         }
289 
290         // Yes, we create a completely new immutable graph here
291         return currentDevice.newInstance(
292                 currentDevice.getIdentity().getUdn(),
293                 currentDevice.getVersion(),
294                 currentDevice.getType(),
295                 currentDevice.getDetails(),
296                 iconDupes,
297                 currentDevice.toServiceArray(describedServices),
298                 describedEmbeddedDevices
299         );
300     }
301 
302     protected RemoteService describeService(RemoteService service)
303             throws RouterException, DescriptorBindingException, ValidationException {
304 
305     	URL descriptorURL;
306     	try {
307     		descriptorURL = service.getDevice().normalizeURI(service.getDescriptorURI());
308     	}  catch(IllegalArgumentException e) {
309     		log.warning("Could not normalize service descriptor URL: " + service.getDescriptorURI());
310     		return null;
311     	}
312 
313         StreamRequestMessage serviceDescRetrievalMsg = new StreamRequestMessage(UpnpRequest.Method.GET, descriptorURL);
314 
315         // Extra headers
316         UpnpHeaders headers =
317             getUpnpService().getConfiguration().getDescriptorRetrievalHeaders(service.getDevice().getIdentity());
318         if (headers != null)
319             serviceDescRetrievalMsg.getHeaders().putAll(headers);
320 
321         log.fine("Sending service descriptor retrieval message: " + serviceDescRetrievalMsg);
322         StreamResponseMessage serviceDescMsg = getUpnpService().getRouter().send(serviceDescRetrievalMsg);
323 
324         if (serviceDescMsg == null) {
325             log.warning("Could not retrieve service descriptor, no response: " + service);
326             return null;
327         }
328 
329         if (serviceDescMsg.getOperation().isFailed()) {
330             log.warning("Service descriptor retrieval failed: "
331                                 + descriptorURL
332                                 + ", "
333                                 + serviceDescMsg.getOperation().getResponseDetails());
334             return null;
335         }
336 
337         if (!serviceDescMsg.isContentTypeTextUDA()) {
338             log.fine("Received service descriptor without or with invalid Content-Type: " + descriptorURL);
339             // We continue despite the invalid UPnP message because we can still hope to convert the content
340         }
341 
342         String descriptorContent = serviceDescMsg.getBodyString();
343         if (descriptorContent == null || descriptorContent.length() == 0) {
344             log.warning("Received empty service descriptor:" + descriptorURL);
345             return null;
346         }
347 
348         log.fine("Received service descriptor, hydrating service model: " + serviceDescMsg);
349         ServiceDescriptorBinder serviceDescriptorBinder =
350                 getUpnpService().getConfiguration().getServiceDescriptorBinderUDA10();
351 
352         return serviceDescriptorBinder.describe(service, descriptorContent);
353     }
354 
355     protected List<RemoteService> filterExclusiveServices(RemoteService[] services) {
356         ServiceType[] exclusiveTypes = getUpnpService().getConfiguration().getExclusiveServiceTypes();
357 
358         if (exclusiveTypes == null || exclusiveTypes.length == 0)
359             return Arrays.asList(services);
360 
361         List<RemoteService> exclusiveServices = new ArrayList<>();
362         for (RemoteService discoveredService : services) {
363             for (ServiceType exclusiveType : exclusiveTypes) {
364                 if (discoveredService.getServiceType().implementsVersion(exclusiveType)) {
365                     log.fine("Including exclusive service: " + discoveredService);
366                     exclusiveServices.add(discoveredService);
367                 } else {
368                     log.fine("Excluding unwanted service: " + exclusiveType);
369                 }
370             }
371         }
372         return exclusiveServices;
373     }
374 
375 }