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.model.meta;
17  
18  import org.fourthline.cling.model.Namespace;
19  import org.fourthline.cling.model.profile.RemoteClientInfo;
20  import org.fourthline.cling.model.resource.Resource;
21  import org.fourthline.cling.model.Validatable;
22  import org.fourthline.cling.model.ValidationError;
23  import org.fourthline.cling.model.ValidationException;
24  import org.fourthline.cling.model.types.DeviceType;
25  import org.fourthline.cling.model.types.ServiceId;
26  import org.fourthline.cling.model.types.ServiceType;
27  import org.fourthline.cling.model.types.UDN;
28  
29  import java.net.URI;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collection;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.logging.Level;
36  import java.util.logging.Logger;
37  
38  /**
39   * Describes either a root or embedded device.
40   *
41   * @author Christian Bauer
42   */
43  public abstract class Device<DI extends DeviceIdentity, D extends Device, S extends Service> implements Validatable {
44  
45      final private static Logger log = Logger.getLogger(Device.class.getName());
46  
47      final private DI identity;
48  
49      final private UDAVersion version;
50      final private DeviceType type;
51      final private DeviceDetails details;
52      final private Icon[] icons;
53      final protected S[] services;
54      final protected D[] embeddedDevices;
55  
56      // Package mutable state
57      private D parentDevice;
58  
59      public Device(DI identity) throws ValidationException {
60          this(identity, null, null, null, null, null);
61      }
62  
63      public Device(DI identity, DeviceType type, DeviceDetails details,
64                    Icon[] icons, S[] services) throws ValidationException {
65          this(identity, null, type, details, icons, services, null);
66      }
67  
68      public Device(DI identity, DeviceType type, DeviceDetails details,
69                    Icon[] icons, S[] services, D[] embeddedDevices) throws ValidationException {
70          this(identity, null, type, details, icons, services, embeddedDevices);
71      }
72  
73      public Device(DI identity, UDAVersion version, DeviceType type, DeviceDetails details,
74                    Icon[] icons, S[] services, D[] embeddedDevices) throws ValidationException {
75  
76          this.identity = identity;
77          this.version = version == null ? new UDAVersion() : version;
78          this.type = type;
79          this.details = details;
80  
81          // We don't fail device validation if icons were invalid, only log a warning. To
82          // comply with mutability rules (can't set icons field in validate() method), we
83          // validate the icons here before we set the field value
84          List<Icon> validIcons = new ArrayList<>();
85          if (icons != null) {
86              for (Icon icon : icons) {
87                  if (icon != null) {
88                      icon.setDevice(this); // Set before validate()!
89                      List<ValidationError> iconErrors = icon.validate();
90                      if(iconErrors.isEmpty()) {
91                          validIcons.add(icon);
92                      } else {
93                          log.warning("Discarding invalid '" + icon + "': " + iconErrors);
94                      }
95                  }
96              }
97          }
98          this.icons = validIcons.toArray(new Icon[validIcons.size()]);
99  
100         boolean allNullServices = true;
101         if (services != null) {
102             for (S service : services) {
103                 if (service != null) {
104                     allNullServices = false;
105                     service.setDevice(this);
106                 }
107             }
108         }
109         this.services = services == null || allNullServices ? null : services;
110 
111         boolean allNullEmbedded = true;
112         if (embeddedDevices != null) {
113             for (D embeddedDevice : embeddedDevices) {
114                 if (embeddedDevice != null) {
115                     allNullEmbedded = false;
116                     embeddedDevice.setParentDevice(this);
117                 }
118             }
119         }
120         this.embeddedDevices = embeddedDevices == null || allNullEmbedded  ? null : embeddedDevices;
121 
122         List<ValidationError> errors = validate();
123         if (errors.size() > 0) {
124             if (log.isLoggable(Level.FINEST)) {
125                 for (ValidationError error : errors) {
126                     log.finest(error.toString());
127                 }
128             }
129             throw new ValidationException("Validation of device graph failed, call getErrors() on exception", errors);
130         }
131     }
132 
133     public DI getIdentity() {
134         return identity;
135     }
136 
137     public UDAVersion getVersion() {
138         return version;
139     }
140 
141     public DeviceType getType() {
142         return type;
143     }
144 
145     public DeviceDetails getDetails() {
146         return details;
147     }
148 
149     public DeviceDetails getDetails(RemoteClientInfo info) {
150         return this.getDetails();
151     }
152 
153     public Icon[] getIcons() {
154         return icons;
155     }
156 
157     public boolean hasIcons() {
158         return getIcons() != null && getIcons().length > 0;
159     }
160 
161     public boolean hasServices() {
162         return getServices() != null && getServices().length > 0;
163     }
164 
165 
166     public boolean hasEmbeddedDevices() {
167         return getEmbeddedDevices() != null && getEmbeddedDevices().length > 0;
168     }
169 
170     public D getParentDevice() {
171         return parentDevice;
172     }
173 
174     void setParentDevice(D parentDevice) {
175         if (this.parentDevice != null)
176             throw new IllegalStateException("Final value has been set already, model is immutable");
177         this.parentDevice = parentDevice;
178     }
179 
180     public boolean isRoot() {
181         return getParentDevice() == null;
182     }
183 
184     public abstract S[] getServices();
185 
186     public abstract D[] getEmbeddedDevices();
187 
188     public abstract D getRoot();
189 
190     public abstract D findDevice(UDN udn);
191 
192     public D[] findEmbeddedDevices() {
193         return toDeviceArray(findEmbeddedDevices((D) this));
194     }
195 
196     public D[] findDevices(DeviceType deviceType) {
197         return toDeviceArray(find(deviceType, (D) this));
198     }
199 
200     public D[] findDevices(ServiceType serviceType) {
201         return toDeviceArray(find(serviceType, (D) this));
202     }
203 
204     public Icon[] findIcons() {
205         List<Icon> icons = new ArrayList<>();
206         if (hasIcons()) {
207             icons.addAll(Arrays.asList(getIcons()));
208         }
209         D[] embeddedDevices = findEmbeddedDevices();
210         for (D embeddedDevice : embeddedDevices) {
211             if (embeddedDevice.hasIcons()) {
212                 icons.addAll(Arrays.asList(embeddedDevice.getIcons()));
213             }
214         }
215         return icons.toArray(new Icon[icons.size()]);
216     }
217 
218     public S[] findServices() {
219         return toServiceArray(findServices(null, null, (D) this));
220     }
221 
222     public S[] findServices(ServiceType serviceType) {
223         return toServiceArray(findServices(serviceType, null, (D) this));
224     }
225 
226     protected D find(UDN udn, D current) {
227         if (current.getIdentity() != null && current.getIdentity().getUdn() != null) {
228             if (current.getIdentity().getUdn().equals(udn)) return current;
229         }
230         if (current.hasEmbeddedDevices()) {
231             for (D embeddedDevice : (D[]) current.getEmbeddedDevices()) {
232                 D match;
233                 if ((match = find(udn, embeddedDevice)) != null) return match;
234             }
235         }
236         return null;
237     }
238 
239     protected Collection<D> findEmbeddedDevices(D current) {
240         Collection<D> devices = new HashSet<>();
241         if (!current.isRoot() && current.getIdentity().getUdn() != null)
242             devices.add(current);
243 
244         if (current.hasEmbeddedDevices()) {
245             for (D embeddedDevice : (D[]) current.getEmbeddedDevices()) {
246                 devices.addAll(findEmbeddedDevices(embeddedDevice));
247             }
248         }
249         return devices;
250     }
251 
252     protected Collection<D> find(DeviceType deviceType, D current) {
253         Collection<D> devices = new HashSet<>();
254         // Type might be null if we just discovered the device and it hasn't yet been hydrated
255         if (current.getType() != null && current.getType().implementsVersion(deviceType)) {
256             devices.add(current);
257         }
258         if (current.hasEmbeddedDevices()) {
259             for (D embeddedDevice : (D[]) current.getEmbeddedDevices()) {
260                 devices.addAll(find(deviceType, embeddedDevice));
261             }
262         }
263         return devices;
264     }
265 
266     protected Collection<D> find(ServiceType serviceType, D current) {
267         Collection<S> services = findServices(serviceType, null, current);
268         Collection<D> devices = new HashSet<>();
269         for (Service service : services) {
270             devices.add((D) service.getDevice());
271         }
272         return devices;
273     }
274 
275     protected Collection<S> findServices(ServiceType serviceType, ServiceId serviceId, D current) {
276         Collection services = new HashSet<>();
277         if (current.hasServices()) {
278             for (Service service : current.getServices()) {
279                 if (isMatch(service, serviceType, serviceId))
280                     services.add(service);
281             }
282         }
283         Collection<D> embeddedDevices = findEmbeddedDevices(current);
284         if (embeddedDevices != null) {
285             for (D embeddedDevice : embeddedDevices) {
286                 if (embeddedDevice.hasServices()) {
287                     for (Service service : embeddedDevice.getServices()) {
288                         if (isMatch(service, serviceType, serviceId))
289                             services.add(service);
290                     }
291                 }
292             }
293         }
294         return services;
295     }
296 
297     public S findService(ServiceId serviceId) {
298         Collection<S> services = findServices(null, serviceId, (D) this);
299         return services.size() == 1 ? services.iterator().next() : null;
300     }
301 
302     public S findService(ServiceType serviceType) {
303         Collection<S> services = findServices(serviceType, null, (D) this);
304         return services.size() > 0 ? services.iterator().next() : null;
305     }
306 
307     public ServiceType[] findServiceTypes() {
308         Collection<S> services = findServices(null, null, (D) this);
309         Collection<ServiceType> col = new HashSet<>();
310         for (S service : services) {
311             col.add(service.getServiceType());
312         }
313         return col.toArray(new ServiceType[col.size()]);
314     }
315 
316     private boolean isMatch(Service s, ServiceType serviceType, ServiceId serviceId) {
317         boolean matchesType = serviceType == null || s.getServiceType().implementsVersion(serviceType);
318         boolean matchesId = serviceId == null || s.getServiceId().equals(serviceId);
319         return matchesType && matchesId;
320     }
321 
322     public boolean isFullyHydrated() {
323         S[] services = findServices();
324         for (S service : services) {
325             if (service.hasStateVariables()) return true;
326         }
327         return false;
328     }
329 
330     public String getDisplayString() {
331 
332         // The goal is to have a clean string with "<manufacturer> <model name> <model#>"
333 
334         String cleanModelName = null;
335         String cleanModelNumber = null;
336 
337         if (getDetails() != null && getDetails().getModelDetails() != null) {
338 
339             // Some vendors end the model name with the model number, let's remove that
340             ModelDetails modelDetails = getDetails().getModelDetails();
341             if (modelDetails.getModelName() != null) {
342                 cleanModelName = modelDetails.getModelNumber() != null && modelDetails.getModelName().endsWith(modelDetails.getModelNumber())
343                         ? modelDetails.getModelName().substring(0, modelDetails.getModelName().length() - modelDetails.getModelNumber().length())
344                         : modelDetails.getModelName();
345             }
346 
347             // Some vendors repeat the model name as the model number, no good
348             if (cleanModelName != null) {
349                 cleanModelNumber = modelDetails.getModelNumber() != null && !cleanModelName.startsWith(modelDetails.getModelNumber())
350                         ? modelDetails.getModelNumber()
351                         : "";
352             } else {
353                 cleanModelNumber = modelDetails.getModelNumber();
354             }
355         }
356 
357         StringBuilder sb = new StringBuilder();
358 
359         if (getDetails() != null && getDetails().getManufacturerDetails() != null) {
360 
361             // Some vendors repeat the manufacturer in model name, let's remove that too
362             if (cleanModelName != null && getDetails().getManufacturerDetails().getManufacturer() != null) {
363                 cleanModelName = cleanModelName.startsWith(getDetails().getManufacturerDetails().getManufacturer())
364                         ? cleanModelName.substring(getDetails().getManufacturerDetails().getManufacturer().length()).trim()
365                         : cleanModelName.trim();
366             }
367 
368             if (getDetails().getManufacturerDetails().getManufacturer() != null) {
369                 sb.append(getDetails().getManufacturerDetails().getManufacturer());
370             }
371         }
372 
373         sb.append((cleanModelName != null && cleanModelName.length() > 0 ? " " + cleanModelName : ""));
374         sb.append((cleanModelNumber != null && cleanModelNumber.length() > 0 ? " " + cleanModelNumber.trim() : ""));
375         return sb.toString();
376     }
377 
378     public List<ValidationError> validate() {
379         List<ValidationError> errors = new ArrayList<>();
380 
381         if (getType() != null) {
382 
383             // Only validate the graph if we have a device type - that means we validate only if there
384             // actually is a fully hydrated graph, not just a discovered device of which we haven't even
385             // retrieved the descriptor yet. This assumes that the descriptor will ALWAYS contain a device
386             // type. Now that is a risky assumption...
387 
388             errors.addAll(getVersion().validate());
389             
390             if(getIdentity() != null) {
391             	errors.addAll(getIdentity().validate());
392             }
393 
394             if (getDetails() != null) {
395                 errors.addAll(getDetails().validate());
396             }
397 
398             if (hasServices()) {
399                 for (Service service : getServices()) {
400                     if (service != null)
401                         errors.addAll(service.validate());
402                 }
403             }
404 
405             if (hasEmbeddedDevices()) {
406                 for (Device embeddedDevice : getEmbeddedDevices()) {
407                     if (embeddedDevice != null)
408                         errors.addAll(embeddedDevice.validate());
409                 }
410             }
411         }
412 
413         return errors;
414     }
415 
416     @Override
417     public boolean equals(Object o) {
418         if (this == o) return true;
419         if (o == null || getClass() != o.getClass()) return false;
420 
421         Device device = (Device) o;
422 
423         if (!identity.equals(device.identity)) return false;
424 
425         return true;
426     }
427 
428     @Override
429     public int hashCode() {
430         return identity.hashCode();
431     }
432 
433     public abstract D newInstance(UDN udn, UDAVersion version, DeviceType type, DeviceDetails details,
434                                   Icon[] icons, S[] services, List<D> embeddedDevices) throws ValidationException;
435 
436     public abstract S newInstance(ServiceType serviceType, ServiceId serviceId,
437                                   URI descriptorURI, URI controlURI, URI eventSubscriptionURI,
438                                   Action<S>[] actions, StateVariable<S>[] stateVariables) throws ValidationException;
439 
440     public abstract D[] toDeviceArray(Collection<D> col);
441 
442     public abstract S[] newServiceArray(int size);
443 
444     public abstract S[] toServiceArray(Collection<S> col);
445 
446     public abstract Resource[] discoverResources(Namespace namespace);
447 
448     @Override
449     public String toString() {
450         return "(" + getClass().getSimpleName() + ") Identity: " + getIdentity().toString() + ", Root: " + isRoot();
451     }
452 }