Added Static Routing Northbound integration tests for all available REST APIs.
[controller.git] / opendaylight / hosttracker_new / implementation / src / main / java / org / opendaylight / controller / hosttracker / internal / Device.java
1 /*
2  * Copyright (c) 2011,2012 Big Switch Networks, Inc.
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the
5  * "License"); you may not use this file except in compliance with the
6  * License. You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/legal/epl-v10.html
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13  * implied. See the License for the specific language governing
14  * permissions and limitations under the License.
15  *
16  * This file incorporates work covered by the following copyright and
17  * permission notice:
18  *
19  *    Originally created by David Erickson, Stanford University
20  *
21  *    Licensed under the Apache License, Version 2.0 (the "License");
22  *    you may not use this file except in compliance with the
23  *    License. You may obtain a copy of the License at
24  *
25  *         http://www.apache.org/licenses/LICENSE-2.0
26  *
27  *    Unless required by applicable law or agreed to in writing,
28  *    software distributed under the License is distributed on an "AS
29  *    IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
30  *    express or implied. See the License for the specific language
31  *    governing permissions and limitations under the License.
32  */
33
34 package org.opendaylight.controller.hosttracker.internal;
35
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.Collections;
40 import java.util.Date;
41 import java.util.EnumSet;
42 import java.util.HashMap;
43 import java.util.Iterator;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.TreeSet;
47
48 import org.opendaylight.controller.hosttracker.Entity;
49 import org.opendaylight.controller.hosttracker.IDevice;
50 import org.opendaylight.controller.hosttracker.IDeviceService.DeviceField;
51 import org.opendaylight.controller.hosttracker.IEntityClass;
52 import org.opendaylight.controller.hosttracker.SwitchPort;
53 import org.opendaylight.controller.hosttracker.SwitchPort.ErrorStatus;
54 import org.opendaylight.controller.sal.core.NodeConnector;
55 import org.opendaylight.controller.sal.utils.HexEncode;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 /**
60  * Concrete implementation of {@link IDevice}
61  *
62  * @author readams
63  */
64 public class Device implements IDevice {
65     protected static Logger log = LoggerFactory.getLogger(Device.class);
66     public static final short VLAN_UNTAGGED = (short) 0xffff;
67
68     private final Long deviceKey;
69     protected final DeviceManagerImpl deviceManager;
70
71     protected final Entity[] entities;
72     private final IEntityClass entityClass;
73
74     protected final String macAddressString;
75     // the vlan Ids from the entities of this device
76     protected final Short[] vlanIds;
77     protected volatile String dhcpClientName;
78
79     /**
80      * These are the old attachment points for the device that were valid no
81      * more than INACTIVITY_TIME ago.
82      */
83     protected volatile List<AttachmentPoint> oldAPs;
84     /**
85      * The current attachment points for the device.
86      */
87     protected volatile List<AttachmentPoint> attachmentPoints;
88
89     // ************
90     // Constructors
91     // ************
92
93     /**
94      * Create a device from an entities
95      *
96      * @param deviceManager
97      *            the device manager for this device
98      * @param deviceKey
99      *            the unique identifier for this device object
100      * @param entity
101      *            the initial entity for the device
102      * @param entityClass
103      *            the entity classes associated with the entity
104      */
105     public Device(DeviceManagerImpl deviceManager, Long deviceKey,
106             Entity entity, IEntityClass entityClass) {
107         this.deviceManager = deviceManager;
108         this.deviceKey = deviceKey;
109         this.entities = new Entity[] { entity };
110         this.macAddressString = HexEncode.longToHexString(entity
111                 .getMacAddress());
112         this.entityClass = entityClass;
113         Arrays.sort(this.entities);
114
115         this.dhcpClientName = null;
116         this.oldAPs = null;
117         this.attachmentPoints = null;
118
119         if (entity.getPort() != null) {
120             NodeConnector port = entity.getPort();
121
122             if (deviceManager.isValidAttachmentPoint(port)) {
123                 AttachmentPoint ap;
124                 ap = new AttachmentPoint(port, entity.getLastSeenTimestamp()
125                         .getTime());
126
127                 this.attachmentPoints = new ArrayList<AttachmentPoint>();
128                 this.attachmentPoints.add(ap);
129             }
130         }
131         vlanIds = computeVlandIds();
132     }
133
134     /**
135      * Create a device from a set of entities
136      *
137      * @param deviceManager
138      *            the device manager for this device
139      * @param deviceKey
140      *            the unique identifier for this device object
141      * @param entities
142      *            the initial entities for the device
143      * @param entityClass
144      *            the entity class associated with the entities
145      */
146     public Device(DeviceManagerImpl deviceManager, Long deviceKey,
147             String dhcpClientName, Collection<AttachmentPoint> oldAPs,
148             Collection<AttachmentPoint> attachmentPoints,
149             Collection<Entity> entities, IEntityClass entityClass) {
150         this.deviceManager = deviceManager;
151         this.deviceKey = deviceKey;
152         this.dhcpClientName = dhcpClientName;
153         this.entities = entities.toArray(new Entity[entities.size()]);
154         this.oldAPs = null;
155         this.attachmentPoints = null;
156         if (oldAPs != null) {
157             this.oldAPs = new ArrayList<AttachmentPoint>(oldAPs);
158         }
159         if (attachmentPoints != null) {
160             this.attachmentPoints = new ArrayList<AttachmentPoint>(
161                     attachmentPoints);
162         }
163         this.macAddressString = HexEncode.longToHexString(this.entities[0]
164                 .getMacAddress());
165         this.entityClass = entityClass;
166         Arrays.sort(this.entities);
167         vlanIds = computeVlandIds();
168     }
169
170     /**
171      * Construct a new device consisting of the entities from the old device
172      * plus an additional entity. The caller needs to ensure that the additional
173      * entity is not already present in the array
174      *
175      * @param device
176      *            the old device object
177      * @param newEntity
178      *            the entity to add. newEntity must be have the same entity
179      *            class as device
180      * @param if positive indicates the index in the entities array were the new
181      *        entity should be inserted. If negative we will compute the correct
182      *        insertion point
183      */
184     public Device(Device device, Entity newEntity, int insertionpoint) {
185         this.deviceManager = device.deviceManager;
186         this.deviceKey = device.deviceKey;
187         this.dhcpClientName = device.dhcpClientName;
188
189         this.entities = new Entity[device.entities.length + 1];
190         if (insertionpoint < 0) {
191             insertionpoint = -(Arrays.binarySearch(device.entities, newEntity) + 1);
192         }
193         if (insertionpoint > 0) {
194             // insertion point is not the beginning:
195             // copy up to insertion point
196             System.arraycopy(device.entities, 0, this.entities, 0,
197                     insertionpoint);
198         }
199         if (insertionpoint < device.entities.length) {
200             // insertion point is not the end
201             // copy from insertion point
202             System.arraycopy(device.entities, insertionpoint, this.entities,
203                     insertionpoint + 1, device.entities.length - insertionpoint);
204         }
205         this.entities[insertionpoint] = newEntity;
206         /*
207          * this.entities = Arrays.<Entity>copyOf(device.entities,
208          * device.entities.length + 1); this.entities[this.entities.length - 1]
209          * = newEntity; Arrays.sort(this.entities);
210          */
211         this.oldAPs = null;
212         if (device.oldAPs != null) {
213             this.oldAPs = new ArrayList<AttachmentPoint>(device.oldAPs);
214         }
215         this.attachmentPoints = null;
216         if (device.attachmentPoints != null) {
217             this.attachmentPoints = new ArrayList<AttachmentPoint>(
218                     device.attachmentPoints);
219         }
220
221         this.macAddressString = HexEncode.longToHexString(this.entities[0]
222                 .getMacAddress());
223
224         this.entityClass = device.entityClass;
225         vlanIds = computeVlandIds();
226     }
227
228     private Short[] computeVlandIds() {
229         if (entities.length == 1) {
230             if (entities[0].getVlan() != null) {
231                 return new Short[] { entities[0].getVlan() };
232             } else {
233                 return new Short[] { Short.valueOf((short) -1) };
234             }
235         }
236
237         TreeSet<Short> vals = new TreeSet<Short>();
238         for (Entity e : entities) {
239             if (e.getVlan() == null)
240                 vals.add((short) -1);
241             else
242                 vals.add(e.getVlan());
243         }
244         return vals.toArray(new Short[vals.size()]);
245     }
246
247     /**
248      * Given a list of attachment points (apList), the procedure would return a
249      * map of attachment points for each L2 domain. L2 domain id is the key.
250      *
251      * @param apList
252      * @return
253      */
254     private Map<Long, AttachmentPoint> getAPMap(List<AttachmentPoint> apList) {
255
256         if (apList == null)
257             return null;
258         // ITopologyService topology = deviceManager.topology;
259
260         // Get the old attachment points and sort them.
261         List<AttachmentPoint> oldAP = new ArrayList<AttachmentPoint>();
262         if (apList != null)
263             oldAP.addAll(apList);
264
265         // Remove invalid attachment points before sorting.
266         List<AttachmentPoint> tempAP = new ArrayList<AttachmentPoint>();
267         for (AttachmentPoint ap : oldAP) {
268             if (deviceManager.isValidAttachmentPoint(ap.getPort())) {
269                 tempAP.add(ap);
270             }
271         }
272         oldAP = tempAP;
273
274         Collections.sort(oldAP, deviceManager.apComparator);
275
276         // Map of attachment point by L2 domain Id.
277         Map<Long, AttachmentPoint> apMap = new HashMap<Long, AttachmentPoint>();
278
279         for (int i = 0; i < oldAP.size(); ++i) {
280             AttachmentPoint ap = oldAP.get(i);
281             // if this is not a valid attachment point, continue
282             if (!deviceManager.isValidAttachmentPoint(ap.getPort()))
283                 continue;
284
285             // long id = topology.getL2DomainId(ap.getSw());
286             // XXX - Missing functionality
287             long id = 0;
288
289             apMap.put(id, ap);
290         }
291
292         if (apMap.isEmpty())
293             return null;
294         return apMap;
295     }
296
297     /**
298      * Remove all attachment points that are older than INACTIVITY_INTERVAL from
299      * the list.
300      *
301      * @param apList
302      * @return
303      */
304     private boolean removeExpiredAttachmentPoints(List<AttachmentPoint> apList) {
305
306         List<AttachmentPoint> expiredAPs = new ArrayList<AttachmentPoint>();
307
308         if (apList == null)
309             return false;
310
311         for (AttachmentPoint ap : apList) {
312             if (ap.getLastSeen() + AttachmentPoint.INACTIVITY_INTERVAL < System
313                     .currentTimeMillis())
314                 expiredAPs.add(ap);
315         }
316         if (expiredAPs.size() > 0) {
317             apList.removeAll(expiredAPs);
318             return true;
319         } else
320             return false;
321     }
322
323     /**
324      * Get a list of duplicate attachment points, given a list of old attachment
325      * points and one attachment point per L2 domain. Given a true attachment
326      * point in the L2 domain, say trueAP, another attachment point in the same
327      * L2 domain, say ap, is duplicate if: 1. ap is inconsistent with trueAP,
328      * and 2. active time of ap is after that of trueAP; and 3. last seen time
329      * of ap is within the last INACTIVITY_INTERVAL
330      *
331      * @param oldAPList
332      * @param apMap
333      * @return
334      */
335     List<AttachmentPoint> getDuplicateAttachmentPoints(
336             List<AttachmentPoint> oldAPList, Map<Long, AttachmentPoint> apMap) {
337         // ITopologyService topology = deviceManager.topology;
338         List<AttachmentPoint> dupAPs = new ArrayList<AttachmentPoint>();
339         long timeThreshold = System.currentTimeMillis()
340                 - AttachmentPoint.INACTIVITY_INTERVAL;
341
342         if (oldAPList == null || apMap == null)
343             return dupAPs;
344
345         for (AttachmentPoint ap : oldAPList) {
346             // XXX - Missing functionality
347             // long id = topology.getL2DomainId(ap.getSw());
348             long id = 0;
349             AttachmentPoint trueAP = apMap.get(id);
350
351             if (trueAP == null)
352                 continue;
353             // XXX - Missing functionality
354             // boolean c = (topology.isConsistent(trueAP.getSw(),
355             // trueAP.getPort(),
356             // ap.getSw(), ap.getPort()));
357             boolean c = true;
358             boolean active = (ap.getActiveSince() > trueAP.getActiveSince());
359             boolean last = ap.getLastSeen() > timeThreshold;
360             if (!c && active && last) {
361                 dupAPs.add(ap);
362             }
363         }
364
365         return dupAPs;
366     }
367
368     /**
369      * Update the known attachment points. This method is called whenever
370      * topology changes. The method returns true if there's any change to the
371      * list of attachment points -- which indicates a possible device move.
372      *
373      * @return
374      */
375     protected boolean updateAttachmentPoint() {
376         boolean moved = false;
377         this.oldAPs = attachmentPoints;
378         if (attachmentPoints == null || attachmentPoints.isEmpty())
379             return false;
380
381         List<AttachmentPoint> apList = new ArrayList<AttachmentPoint>();
382         if (attachmentPoints != null)
383             apList.addAll(attachmentPoints);
384         Map<Long, AttachmentPoint> newMap = getAPMap(apList);
385         if (newMap == null || newMap.size() != apList.size()) {
386             moved = true;
387         }
388
389         // Prepare the new attachment point list.
390         if (moved) {
391             log.info("updateAttachmentPoint: ap {}  newmap {} ",
392                     attachmentPoints, newMap);
393             List<AttachmentPoint> newAPList = new ArrayList<AttachmentPoint>();
394             if (newMap != null)
395                 newAPList.addAll(newMap.values());
396             this.attachmentPoints = newAPList;
397         }
398
399         // Set the oldAPs to null.
400         return moved;
401     }
402
403     /**
404      * Update the list of attachment points given that a new packet-in was seen
405      * from (sw, port) at time (lastSeen). The return value is true if there was
406      * any change to the list of attachment points for the device -- which
407      * indicates a device move.
408      *
409      * @param sw
410      * @param port
411      * @param lastSeen
412      * @return
413      */
414     protected boolean updateAttachmentPoint(NodeConnector port, long lastSeen) {
415         // ITopologyService topology = deviceManager.topology;
416         List<AttachmentPoint> oldAPList;
417         List<AttachmentPoint> apList;
418         boolean oldAPFlag = false;
419
420         if (!deviceManager.isValidAttachmentPoint(port))
421             return false;
422         AttachmentPoint newAP = new AttachmentPoint(port, lastSeen);
423         // Copy the oldAP and ap list.
424         apList = new ArrayList<AttachmentPoint>();
425         if (attachmentPoints != null)
426             apList.addAll(attachmentPoints);
427         oldAPList = new ArrayList<AttachmentPoint>();
428         if (oldAPs != null)
429             oldAPList.addAll(oldAPs);
430
431         // if the sw, port is in old AP, remove it from there
432         // and update the lastSeen in that object.
433         if (oldAPList.contains(newAP)) {
434             int index = oldAPList.indexOf(newAP);
435             newAP = oldAPList.remove(index);
436             newAP.setLastSeen(lastSeen);
437             this.oldAPs = oldAPList;
438             oldAPFlag = true;
439         }
440
441         // newAP now contains the new attachment point.
442
443         // Get the APMap is null or empty.
444         Map<Long, AttachmentPoint> apMap = getAPMap(apList);
445         if (apMap == null || apMap.isEmpty()) {
446             apList.add(newAP);
447             attachmentPoints = apList;
448             // there are no old attachment points - since the device exists,
449             // this
450             // may be because the host really moved (so the old AP port went
451             // down);
452             // or it may be because the switch restarted (so old APs were
453             // nullified).
454             // For now we will treat both cases as host moved.
455             return true;
456         }
457
458         // XXX - Missing functionality
459         // long id = topology.getL2DomainId(sw);
460         long id = 0;
461         AttachmentPoint oldAP = apMap.get(id);
462
463         if (oldAP == null) // No attachment on this L2 domain.
464         {
465             apList = new ArrayList<AttachmentPoint>();
466             apList.addAll(apMap.values());
467             apList.add(newAP);
468             this.attachmentPoints = apList;
469             return true; // new AP found on an L2 island.
470         }
471
472         // There is already a known attachment point on the same L2 island.
473         // we need to compare oldAP and newAP.
474         if (oldAP.equals(newAP)) {
475             // nothing to do here. just the last seen has to be changed.
476             if (newAP.lastSeen > oldAP.lastSeen) {
477                 oldAP.setLastSeen(newAP.lastSeen);
478             }
479             this.attachmentPoints = new ArrayList<AttachmentPoint>(
480                     apMap.values());
481             return false; // nothing to do here.
482         }
483
484         int x = deviceManager.apComparator.compare(oldAP, newAP);
485         if (x < 0) {
486             // newAP replaces oldAP.
487             apMap.put(id, newAP);
488             this.attachmentPoints = new ArrayList<AttachmentPoint>(
489                     apMap.values());
490
491             oldAPList = new ArrayList<AttachmentPoint>();
492             if (oldAPs != null)
493                 oldAPList.addAll(oldAPs);
494             oldAPList.add(oldAP);
495             this.oldAPs = oldAPList;
496             // XXX - Missing functionality
497             // if (!topology.isInSameBroadcastDomain(oldAP.getSw(),
498             // oldAP.getPort(),
499             // newAP.getSw(), newAP.getPort()))
500             // return true; // attachment point changed.
501             return true;
502         } else if (oldAPFlag) {
503             // retain oldAP as is. Put the newAP in oldAPs for flagging
504             // possible duplicates.
505             oldAPList = new ArrayList<AttachmentPoint>();
506             if (oldAPs != null)
507                 oldAPList.addAll(oldAPs);
508             // Add to oldAPList only if it was picked up from the oldAPList
509             oldAPList.add(newAP);
510             this.oldAPs = oldAPList;
511             // XXX - Missing functionality
512             // if (!topology.isInSameBroadcastDomain(oldAP.getSw(),
513             // oldAP.getPort(),
514             // newAP.getSw(), newAP.getPort()))
515             // return true; // attachment point changed.
516             return true;
517         }
518         return false;
519     }
520
521     /**
522      * Delete (sw,port) from the list of list of attachment points and oldAPs.
523      *
524      * @param sw
525      * @param port
526      * @return
527      */
528     public boolean deleteAttachmentPoint(NodeConnector port) {
529         AttachmentPoint ap = new AttachmentPoint(port, 0);
530
531         if (this.oldAPs != null) {
532             ArrayList<AttachmentPoint> apList = new ArrayList<AttachmentPoint>();
533             apList.addAll(this.oldAPs);
534             int index = apList.indexOf(ap);
535             if (index > 0) {
536                 apList.remove(index);
537                 this.oldAPs = apList;
538             }
539         }
540
541         if (this.attachmentPoints != null) {
542             ArrayList<AttachmentPoint> apList = new ArrayList<AttachmentPoint>();
543             apList.addAll(this.attachmentPoints);
544             int index = apList.indexOf(ap);
545             if (index > 0) {
546                 apList.remove(index);
547                 this.attachmentPoints = apList;
548                 return true;
549             }
550         }
551         return false;
552     }
553
554     // *******
555     // IDevice
556     // *******
557
558     @Override
559     public SwitchPort[] getOldAP() {
560         List<SwitchPort> sp = new ArrayList<SwitchPort>();
561         SwitchPort[] returnSwitchPorts = new SwitchPort[] {};
562         if (oldAPs == null)
563             return returnSwitchPorts;
564         if (oldAPs.isEmpty())
565             return returnSwitchPorts;
566
567         // copy ap list.
568         List<AttachmentPoint> oldAPList;
569         oldAPList = new ArrayList<AttachmentPoint>();
570
571         if (oldAPs != null)
572             oldAPList.addAll(oldAPs);
573         removeExpiredAttachmentPoints(oldAPList);
574
575         if (oldAPList != null) {
576             for (AttachmentPoint ap : oldAPList) {
577                 SwitchPort swport = new SwitchPort(ap.getPort());
578                 sp.add(swport);
579             }
580         }
581         return sp.toArray(new SwitchPort[sp.size()]);
582     }
583
584     @Override
585     public SwitchPort[] getAttachmentPoints() {
586         return getAttachmentPoints(false);
587     }
588
589     @Override
590     public SwitchPort[] getAttachmentPoints(boolean includeError) {
591         List<SwitchPort> sp = new ArrayList<SwitchPort>();
592         SwitchPort[] returnSwitchPorts = new SwitchPort[] {};
593         if (attachmentPoints == null)
594             return returnSwitchPorts;
595         if (attachmentPoints.isEmpty())
596             return returnSwitchPorts;
597
598         // copy ap list.
599         List<AttachmentPoint> apList = attachmentPoints;
600
601         if (apList != null) {
602             for (AttachmentPoint ap : apList) {
603                 SwitchPort swport = new SwitchPort(ap.getPort());
604                 sp.add(swport);
605             }
606         }
607
608         if (!includeError)
609             return sp.toArray(new SwitchPort[sp.size()]);
610
611         List<AttachmentPoint> oldAPList;
612         oldAPList = new ArrayList<AttachmentPoint>();
613
614         if (oldAPs != null)
615             oldAPList.addAll(oldAPs);
616
617         if (removeExpiredAttachmentPoints(oldAPList))
618             this.oldAPs = oldAPList;
619
620         List<AttachmentPoint> dupList;
621         // get AP map.
622         Map<Long, AttachmentPoint> apMap = getAPMap(apList);
623         dupList = this.getDuplicateAttachmentPoints(oldAPList, apMap);
624         if (dupList != null) {
625             for (AttachmentPoint ap : dupList) {
626                 SwitchPort swport = new SwitchPort(ap.getPort(),
627                         ErrorStatus.DUPLICATE_DEVICE);
628                 sp.add(swport);
629             }
630         }
631         return sp.toArray(new SwitchPort[sp.size()]);
632     }
633
634     @Override
635     public Long getDeviceKey() {
636         return deviceKey;
637     }
638
639     @Override
640     public long getMACAddress() {
641         // we assume only one MAC per device for now.
642         return entities[0].getMacAddress();
643     }
644
645     @Override
646     public String getMACAddressString() {
647         return macAddressString;
648     }
649
650     @Override
651     public Short[] getVlanId() {
652         return Arrays.copyOf(vlanIds, vlanIds.length);
653     }
654
655     static final EnumSet<DeviceField> ipv4Fields = EnumSet.of(DeviceField.IPV4);
656
657     @Override
658     public Integer[] getIPv4Addresses() {
659         // XXX - TODO we can cache this result. Let's find out if this
660         // is really a performance bottleneck first though.
661
662         TreeSet<Integer> vals = new TreeSet<Integer>();
663         for (Entity e : entities) {
664             if (e.getIpv4Address() == null)
665                 continue;
666
667             // We have an IP address only if among the devices within the class
668             // we have the most recent entity with that IP.
669             boolean validIP = true;
670             Iterator<Device> devices = deviceManager.queryClassByEntity(
671                     entityClass, ipv4Fields, e);
672             while (devices.hasNext()) {
673                 Device d = devices.next();
674                 if (deviceKey.equals(d.getDeviceKey()))
675                     continue;
676                 for (Entity se : d.entities) {
677                     if (se.getIpv4Address() != null
678                             && se.getIpv4Address().equals(e.getIpv4Address())
679                             && se.getLastSeenTimestamp() != null
680                             && 0 < se.getLastSeenTimestamp().compareTo(
681                                     e.getLastSeenTimestamp())) {
682                         validIP = false;
683                         break;
684                     }
685                 }
686                 if (!validIP)
687                     break;
688             }
689
690             if (validIP)
691                 vals.add(e.getIpv4Address());
692         }
693
694         return vals.toArray(new Integer[vals.size()]);
695     }
696
697     @Override
698     public Short[] getSwitchPortVlanIds(SwitchPort swp) {
699         TreeSet<Short> vals = new TreeSet<Short>();
700         for (Entity e : entities) {
701             if (e.getPort().equals(swp.getPort())) {
702                 if (e.getVlan() == null)
703                     vals.add(VLAN_UNTAGGED);
704                 else
705                     vals.add(e.getVlan());
706             }
707         }
708         return vals.toArray(new Short[vals.size()]);
709     }
710
711     @Override
712     public Date getLastSeen() {
713         Date d = null;
714         for (int i = 0; i < entities.length; i++) {
715             if (d == null
716                     || entities[i].getLastSeenTimestamp().compareTo(d) > 0)
717                 d = entities[i].getLastSeenTimestamp();
718         }
719         return d;
720     }
721
722     // ***************
723     // Getters/Setters
724     // ***************
725
726     @Override
727     public IEntityClass getEntityClass() {
728         return entityClass;
729     }
730
731     public Entity[] getEntities() {
732         return entities;
733     }
734
735     public String getDHCPClientName() {
736         return dhcpClientName;
737     }
738
739     // ***************
740     // Utility Methods
741     // ***************
742
743     /**
744      * Check whether the device contains the specified entity
745      *
746      * @param entity
747      *            the entity to search for
748      * @return the index of the entity, or <0 if not found
749      */
750     protected int entityIndex(Entity entity) {
751         return Arrays.binarySearch(entities, entity);
752     }
753
754     // ******
755     // Object
756     // ******
757
758     @Override
759     public int hashCode() {
760         final int prime = 31;
761         int result = 1;
762         result = prime * result + Arrays.hashCode(entities);
763         return result;
764     }
765
766     @Override
767     public boolean equals(Object obj) {
768         if (this == obj)
769             return true;
770         if (obj == null)
771             return false;
772         if (getClass() != obj.getClass())
773             return false;
774         Device other = (Device) obj;
775         if (!deviceKey.equals(other.deviceKey))
776             return false;
777         if (!Arrays.equals(entities, other.entities))
778             return false;
779         return true;
780     }
781
782     @Override
783     public String toString() {
784         StringBuilder builder = new StringBuilder();
785         builder.append("Device [deviceKey=");
786         builder.append(deviceKey);
787         builder.append(", entityClass=");
788         builder.append(entityClass.getName());
789         builder.append(", MAC=");
790         builder.append(macAddressString);
791         builder.append(", IPs=[");
792         boolean isFirst = true;
793         for (Integer ip : getIPv4Addresses()) {
794             if (!isFirst)
795                 builder.append(", ");
796             isFirst = false;
797             // builder.append(IPv4.fromIPv4Address(ip));
798             builder.append(ip);
799         }
800         builder.append("], APs=");
801         builder.append(Arrays.toString(getAttachmentPoints(true)));
802         builder.append("]");
803         return builder.toString();
804     }
805 }