| /** |
| * Copyright 2011,2012 Big Switch Networks, Inc. |
| * Originally created by David Erickson, Stanford University |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may |
| * not use this file except in compliance with the License. You may obtain |
| * a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| **/ |
| |
| package net.floodlightcontroller.devicemanager.internal; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.TreeSet; |
| |
| import org.codehaus.jackson.map.annotate.JsonSerialize; |
| import org.openflow.util.HexString; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import net.floodlightcontroller.devicemanager.IDeviceService.DeviceField; |
| import net.floodlightcontroller.devicemanager.web.DeviceSerializer; |
| import net.floodlightcontroller.devicemanager.IDevice; |
| import net.floodlightcontroller.devicemanager.IEntityClass; |
| import net.floodlightcontroller.devicemanager.SwitchPort; |
| import net.floodlightcontroller.devicemanager.SwitchPort.ErrorStatus; |
| import net.floodlightcontroller.packet.Ethernet; |
| import net.floodlightcontroller.packet.IPv4; |
| import net.floodlightcontroller.topology.ITopologyService; |
| |
| /** |
| * Concrete implementation of {@link IDevice} |
| * @author readams |
| */ |
| @JsonSerialize(using=DeviceSerializer.class) |
| public class Device implements IDevice { |
| protected final static Logger log = |
| LoggerFactory.getLogger(Device.class); |
| |
| protected Long deviceKey; |
| protected DeviceManagerImpl deviceManager; |
| |
| protected Entity[] entities; |
| protected IEntityClass entityClass; |
| |
| protected String macAddressString; |
| |
| /** |
| * These are the old attachment points for the device that were |
| * valid no more than INACTIVITY_TIME ago. |
| */ |
| protected List<AttachmentPoint> oldAPs; |
| /** |
| * The current attachment points for the device. |
| */ |
| protected List<AttachmentPoint> attachmentPoints; |
| // ************ |
| // Constructors |
| // ************ |
| |
| /** |
| * Create a device from an entities |
| * @param deviceManager the device manager for this device |
| * @param deviceKey the unique identifier for this device object |
| * @param entity the initial entity for the device |
| * @param entityClass the entity classes associated with the entity |
| */ |
| public Device(DeviceManagerImpl deviceManager, |
| Long deviceKey, |
| Entity entity, |
| IEntityClass entityClass) { |
| this.deviceManager = deviceManager; |
| this.deviceKey = deviceKey; |
| this.entities = new Entity[] {entity}; |
| this.macAddressString = |
| HexString.toHexString(entity.getMacAddress(), 6); |
| this.entityClass = entityClass; |
| Arrays.sort(this.entities); |
| |
| this.oldAPs = null; |
| this.attachmentPoints = null; |
| |
| if (entity.getSwitchDPID() != null && |
| entity.getSwitchPort() != null){ |
| long sw = entity.getSwitchDPID(); |
| short port = entity.getSwitchPort().shortValue(); |
| |
| if (deviceManager.isValidAttachmentPoint(sw, port)) { |
| AttachmentPoint ap; |
| ap = new AttachmentPoint(sw, port, |
| entity.getLastSeenTimestamp().getTime()); |
| |
| this.attachmentPoints = new ArrayList<AttachmentPoint>(); |
| this.attachmentPoints.add(ap); |
| } |
| } |
| } |
| |
| /** |
| * Create a device from a set of entities |
| * @param deviceManager the device manager for this device |
| * @param deviceKey the unique identifier for this device object |
| * @param entities the initial entities for the device |
| * @param entityClass the entity class associated with the entities |
| */ |
| public Device(DeviceManagerImpl deviceManager, |
| Long deviceKey, |
| Collection<AttachmentPoint> oldAPs, |
| Collection<AttachmentPoint> attachmentPoints, |
| Collection<Entity> entities, |
| IEntityClass entityClass) { |
| this.deviceManager = deviceManager; |
| this.deviceKey = deviceKey; |
| this.entities = entities.toArray(new Entity[entities.size()]); |
| this.oldAPs = null; |
| this.attachmentPoints = null; |
| if (oldAPs != null) { |
| this.oldAPs = |
| new ArrayList<AttachmentPoint>(oldAPs); |
| } |
| if (attachmentPoints != null) { |
| this.attachmentPoints = |
| new ArrayList<AttachmentPoint>(attachmentPoints); |
| } |
| this.macAddressString = |
| HexString.toHexString(this.entities[0].getMacAddress(), 6); |
| this.entityClass = entityClass; |
| Arrays.sort(this.entities); |
| } |
| |
| /** |
| * Construct a new device consisting of the entities from the old device |
| * plus an additional entity |
| * @param device the old device object |
| * @param newEntity the entity to add. newEntity must be have the same |
| * entity class as device |
| */ |
| public Device(Device device, |
| Entity newEntity) { |
| this.deviceManager = device.deviceManager; |
| this.deviceKey = device.deviceKey; |
| this.entities = Arrays.<Entity>copyOf(device.entities, |
| device.entities.length + 1); |
| this.entities[this.entities.length - 1] = newEntity; |
| Arrays.sort(this.entities); |
| this.oldAPs = null; |
| if (device.oldAPs != null) { |
| this.oldAPs = |
| new ArrayList<AttachmentPoint>(device.oldAPs); |
| } |
| this.attachmentPoints = null; |
| if (device.attachmentPoints != null) { |
| this.attachmentPoints = |
| new ArrayList<AttachmentPoint>(device.attachmentPoints); |
| } |
| |
| this.macAddressString = |
| HexString.toHexString(this.entities[0].getMacAddress(), 6); |
| |
| this.entityClass = device.entityClass; |
| } |
| |
| /** |
| * Given a list of attachment points (apList), the procedure would return |
| * a map of attachment points for each L2 domain. L2 domain id is the key. |
| * @param apList |
| * @return |
| */ |
| private Map<Long, AttachmentPoint> getAPMap(List<AttachmentPoint> apList) { |
| |
| if (apList == null) return null; |
| ITopologyService topology = deviceManager.topology; |
| |
| // Get the old attachment points and sort them. |
| List<AttachmentPoint>oldAP = new ArrayList<AttachmentPoint>(); |
| if (apList != null) oldAP.addAll(apList); |
| |
| // Remove invalid attachment points before sorting. |
| List<AttachmentPoint>tempAP = |
| new ArrayList<AttachmentPoint>(); |
| for(AttachmentPoint ap: oldAP) { |
| if (deviceManager.isValidAttachmentPoint(ap.getSw(), ap.getPort())){ |
| tempAP.add(ap); |
| } |
| } |
| oldAP = tempAP; |
| |
| Collections.sort(oldAP, deviceManager.apComparator); |
| |
| // Map of attachment point by L2 domain Id. |
| Map<Long, AttachmentPoint> apMap = new HashMap<Long, AttachmentPoint>(); |
| |
| for(int i=0; i<oldAP.size(); ++i) { |
| AttachmentPoint ap = oldAP.get(i); |
| // if this is not a valid attachment point, continue |
| if (!deviceManager.isValidAttachmentPoint(ap.getSw(), |
| ap.getPort())) |
| continue; |
| |
| long id = topology.getL2DomainId(ap.getSw()); |
| apMap.put(id, ap); |
| } |
| |
| if (apMap.isEmpty()) return null; |
| return apMap; |
| } |
| |
| /** |
| * Remove all attachment points that are older than INACTIVITY_INTERVAL |
| * from the list. |
| * @param apList |
| * @return |
| */ |
| private boolean removeExpiredAttachmentPoints(List<AttachmentPoint>apList) { |
| |
| List<AttachmentPoint> expiredAPs = new ArrayList<AttachmentPoint>(); |
| |
| if (apList == null) return false; |
| |
| for(AttachmentPoint ap: apList) { |
| if (ap.getLastSeen() + AttachmentPoint.INACTIVITY_INTERVAL < |
| System.currentTimeMillis()) |
| expiredAPs.add(ap); |
| } |
| if (expiredAPs.size() > 0) { |
| apList.removeAll(expiredAPs); |
| return true; |
| } else return false; |
| } |
| |
| /** |
| * Get a list of duplicate attachment points, given a list of old attachment |
| * points and one attachment point per L2 domain. Given a true attachment |
| * point in the L2 domain, say trueAP, another attachment point in the |
| * same L2 domain, say ap, is duplicate if: |
| * 1. ap is inconsistent with trueAP, and |
| * 2. active time of ap is after that of trueAP; and |
| * 3. last seen time of ap is within the last INACTIVITY_INTERVAL |
| * @param oldAPList |
| * @param apMap |
| * @return |
| */ |
| List<AttachmentPoint> getDuplicateAttachmentPoints(List<AttachmentPoint>oldAPList, |
| Map<Long, AttachmentPoint>apMap) { |
| ITopologyService topology = deviceManager.topology; |
| List<AttachmentPoint> dupAPs = new ArrayList<AttachmentPoint>(); |
| long timeThreshold = System.currentTimeMillis() - |
| AttachmentPoint.INACTIVITY_INTERVAL; |
| |
| if (oldAPList == null || apMap == null) |
| return dupAPs; |
| |
| for(AttachmentPoint ap: oldAPList) { |
| long id = topology.getL2DomainId(ap.getSw()); |
| AttachmentPoint trueAP = apMap.get(id); |
| |
| if (trueAP == null) continue; |
| boolean c = (topology.isConsistent(trueAP.getSw(), trueAP.getPort(), |
| ap.getSw(), ap.getPort())); |
| boolean active = (ap.getActiveSince() > trueAP.getActiveSince()); |
| boolean last = ap.getLastSeen() > timeThreshold; |
| if (!c && active && last) { |
| dupAPs.add(ap); |
| } |
| } |
| |
| return dupAPs; |
| } |
| |
| /** |
| * Update the known attachment points. This method is called whenever |
| * topology changes. The method returns true if there's any change to |
| * the list of attachment points -- which indicates a possible device |
| * move. |
| * @return |
| */ |
| protected boolean updateAttachmentPoint() { |
| boolean moved = false; |
| |
| if (attachmentPoints == null || attachmentPoints.isEmpty()) |
| return false; |
| |
| List<AttachmentPoint> apList = new ArrayList<AttachmentPoint>(); |
| if (attachmentPoints != null) apList.addAll(attachmentPoints); |
| Map<Long, AttachmentPoint> newMap = getAPMap(apList); |
| if (newMap == null || newMap.size() != apList.size()) { |
| moved = true; |
| } |
| |
| // Prepare the new attachment point list. |
| if (moved) { |
| List<AttachmentPoint> newAPList = |
| new ArrayList<AttachmentPoint>(); |
| if (newMap != null) newAPList.addAll(newMap.values()); |
| this.attachmentPoints = newAPList; |
| } |
| |
| // Set the oldAPs to null. |
| this.oldAPs = null; |
| return moved; |
| } |
| |
| /** |
| * Update the list of attachment points given that a new packet-in |
| * was seen from (sw, port) at time (lastSeen). The return value is true |
| * if there was any change to the list of attachment points for the device |
| * -- which indicates a device move. |
| * @param sw |
| * @param port |
| * @param lastSeen |
| * @return |
| */ |
| protected boolean updateAttachmentPoint(long sw, short port, long lastSeen){ |
| ITopologyService topology = deviceManager.topology; |
| List<AttachmentPoint> oldAPList; |
| List<AttachmentPoint> apList; |
| boolean oldAPFlag = false; |
| |
| if (!deviceManager.isValidAttachmentPoint(sw, port)) return false; |
| AttachmentPoint newAP = new AttachmentPoint(sw, port, lastSeen); |
| |
| //Copy the oldAP and ap list. |
| apList = new ArrayList<AttachmentPoint>(); |
| if (attachmentPoints != null) apList.addAll(attachmentPoints); |
| oldAPList = new ArrayList<AttachmentPoint>(); |
| if (oldAPs != null) oldAPList.addAll(oldAPs); |
| |
| // if the sw, port is in old AP, remove it from there |
| // and update the lastSeen in that object. |
| if (oldAPList.contains(newAP)) { |
| int index = oldAPList.indexOf(newAP); |
| newAP = oldAPList.remove(index); |
| newAP.setLastSeen(lastSeen); |
| this.oldAPs = oldAPList; |
| oldAPFlag = true; |
| } |
| |
| // newAP now contains the new attachment point. |
| |
| // Get the APMap is null or empty. |
| Map<Long, AttachmentPoint> apMap = getAPMap(apList); |
| if (apMap == null || apMap.isEmpty()) { |
| apList.add(newAP); |
| attachmentPoints = apList; |
| return true; |
| } |
| |
| long id = topology.getL2DomainId(sw); |
| AttachmentPoint oldAP = apMap.get(id); |
| |
| if (oldAP == null) // No attachment on this L2 domain. |
| { |
| apList = new ArrayList<AttachmentPoint>(); |
| apList.addAll(apMap.values()); |
| apList.add(newAP); |
| this.attachmentPoints = apList; |
| return true; // new AP found on an L2 island. |
| } |
| |
| // There is already a known attachment point on the same L2 island. |
| // we need to compare oldAP and newAP. |
| if (oldAP.equals(newAP)) { |
| // nothing to do here. just the last seen has to be changed. |
| if (newAP.lastSeen > oldAP.lastSeen) { |
| oldAP.setLastSeen(newAP.lastSeen); |
| } |
| this.attachmentPoints = |
| new ArrayList<AttachmentPoint>(apMap.values()); |
| return false; // nothing to do here. |
| } |
| |
| int x = deviceManager.apComparator.compare(oldAP, newAP); |
| if (x < 0) { |
| // newAP replaces oldAP. |
| apMap.put(id, newAP); |
| this.attachmentPoints = |
| new ArrayList<AttachmentPoint>(apMap.values()); |
| |
| oldAPList = new ArrayList<AttachmentPoint>(); |
| if (oldAPs != null) oldAPList.addAll(oldAPs); |
| oldAPList.add(oldAP); |
| this.oldAPs = oldAPList; |
| if (!topology.isInSameBroadcastDomain(oldAP.getSw(), oldAP.getPort(), |
| newAP.getSw(), newAP.getPort())) |
| return true; // attachment point changed. |
| } else if (oldAPFlag) { |
| // retain oldAP as is. Put the newAP in oldAPs for flagging |
| // possible duplicates. |
| oldAPList = new ArrayList<AttachmentPoint>(); |
| if (oldAPs != null) oldAPList.addAll(oldAPs); |
| // Add ot oldAPList only if it was picked up from the oldAPList |
| oldAPList.add(newAP); |
| this.oldAPs = oldAPList; |
| } |
| return false; |
| } |
| |
| /** |
| * Delete (sw,port) from the list of list of attachment points |
| * and oldAPs. |
| * @param sw |
| * @param port |
| * @return |
| */ |
| public boolean deleteAttachmentPoint(long sw, short port) { |
| AttachmentPoint ap = new AttachmentPoint(sw, port, 0); |
| |
| if (this.oldAPs != null) { |
| ArrayList<AttachmentPoint> apList = new ArrayList<AttachmentPoint>(); |
| apList.addAll(this.oldAPs); |
| int index = apList.indexOf(ap); |
| if (index > 0) { |
| apList.remove(index); |
| this.oldAPs = apList; |
| } |
| } |
| |
| if (this.attachmentPoints != null) { |
| ArrayList<AttachmentPoint> apList = new ArrayList<AttachmentPoint>(); |
| apList.addAll(this.attachmentPoints); |
| int index = apList.indexOf(ap); |
| if (index > 0) { |
| apList.remove(index); |
| this.attachmentPoints = apList; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean deleteAttachmentPoint(long sw) { |
| boolean deletedFlag; |
| ArrayList<AttachmentPoint> apList; |
| ArrayList<AttachmentPoint> modifiedList; |
| |
| // Delete the APs on switch sw in oldAPs. |
| deletedFlag = false; |
| apList = new ArrayList<AttachmentPoint>(); |
| if (this.oldAPs != null) |
| apList.addAll(this.oldAPs); |
| modifiedList = new ArrayList<AttachmentPoint>(); |
| |
| for(AttachmentPoint ap: apList) { |
| if (ap.getSw() == sw) { |
| deletedFlag = true; |
| } else { |
| modifiedList.add(ap); |
| } |
| } |
| |
| if (deletedFlag) { |
| this.oldAPs = modifiedList; |
| } |
| |
| // Delete the APs on switch sw in attachmentPoints. |
| deletedFlag = false; |
| apList = new ArrayList<AttachmentPoint>(); |
| if (this.attachmentPoints != null) |
| apList.addAll(this.attachmentPoints); |
| modifiedList = new ArrayList<AttachmentPoint>(); |
| |
| for(AttachmentPoint ap: apList) { |
| if (ap.getSw() == sw) { |
| deletedFlag = true; |
| } else { |
| modifiedList.add(ap); |
| } |
| } |
| |
| if (deletedFlag) { |
| this.attachmentPoints = modifiedList; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| |
| @Override |
| public SwitchPort[] getAttachmentPoints() { |
| return getAttachmentPoints(false); |
| } |
| |
| @Override |
| public SwitchPort[] getAttachmentPoints(boolean includeError) { |
| List<SwitchPort> sp = new ArrayList<SwitchPort>(); |
| SwitchPort [] returnSwitchPorts = new SwitchPort[] {}; |
| if (attachmentPoints == null) return returnSwitchPorts; |
| if (attachmentPoints.isEmpty()) return returnSwitchPorts; |
| |
| |
| // copy ap list. |
| List<AttachmentPoint> apList; |
| apList = new ArrayList<AttachmentPoint>(); |
| if (attachmentPoints != null) apList.addAll(attachmentPoints); |
| // get AP map. |
| Map<Long, AttachmentPoint> apMap = getAPMap(apList); |
| |
| if (apMap != null) { |
| for(AttachmentPoint ap: apMap.values()) { |
| SwitchPort swport = new SwitchPort(ap.getSw(), |
| ap.getPort()); |
| sp.add(swport); |
| } |
| } |
| |
| if (!includeError) |
| return sp.toArray(new SwitchPort[sp.size()]); |
| |
| List<AttachmentPoint> oldAPList; |
| oldAPList = new ArrayList<AttachmentPoint>(); |
| |
| if (oldAPs != null) oldAPList.addAll(oldAPs); |
| |
| if (removeExpiredAttachmentPoints(oldAPList)) |
| this.oldAPs = oldAPList; |
| |
| List<AttachmentPoint> dupList; |
| dupList = this.getDuplicateAttachmentPoints(oldAPList, apMap); |
| if (dupList != null) { |
| for(AttachmentPoint ap: dupList) { |
| SwitchPort swport = new SwitchPort(ap.getSw(), |
| ap.getPort(), |
| ErrorStatus.DUPLICATE_DEVICE); |
| sp.add(swport); |
| } |
| } |
| return sp.toArray(new SwitchPort[sp.size()]); |
| } |
| |
| // ******* |
| // IDevice |
| // ******* |
| |
| @Override |
| public Long getDeviceKey() { |
| return deviceKey; |
| } |
| |
| @Override |
| public long getMACAddress() { |
| // we assume only one MAC per device for now. |
| return entities[0].getMacAddress(); |
| } |
| |
| @Override |
| public String getMACAddressString() { |
| return macAddressString; |
| } |
| |
| @Override |
| public Short[] getVlanId() { |
| if (entities.length == 1) { |
| if (entities[0].getVlan() != null) { |
| return new Short[]{ entities[0].getVlan() }; |
| } else { |
| return new Short[] { Short.valueOf((short)-1) }; |
| } |
| } |
| |
| TreeSet<Short> vals = new TreeSet<Short>(); |
| for (Entity e : entities) { |
| if (e.getVlan() == null) |
| vals.add((short)-1); |
| else |
| vals.add(e.getVlan()); |
| } |
| return vals.toArray(new Short[vals.size()]); |
| } |
| |
| static final EnumSet<DeviceField> ipv4Fields = EnumSet.of(DeviceField.IPV4); |
| |
| @Override |
| public Integer[] getIPv4Addresses() { |
| // XXX - TODO we can cache this result. Let's find out if this |
| // is really a performance bottleneck first though. |
| |
| TreeSet<Integer> vals = new TreeSet<Integer>(); |
| for (Entity e : entities) { |
| if (e.getIpv4Address() == null) continue; |
| |
| // We have an IP address only if among the devices within the class |
| // we have the most recent entity with that IP. |
| boolean validIP = true; |
| Iterator<Device> devices = |
| deviceManager.queryClassByEntity(entityClass, ipv4Fields, e); |
| while (devices.hasNext()) { |
| Device d = devices.next(); |
| if (deviceKey.equals(d.getDeviceKey())) |
| continue; |
| for (Entity se : d.entities) { |
| if (se.getIpv4Address() != null && |
| se.getIpv4Address().equals(e.getIpv4Address()) && |
| se.getLastSeenTimestamp() != null && |
| 0 < se.getLastSeenTimestamp(). |
| compareTo(e.getLastSeenTimestamp())) { |
| validIP = false; |
| break; |
| } |
| } |
| if (!validIP) |
| break; |
| } |
| |
| if (validIP) |
| vals.add(e.getIpv4Address()); |
| } |
| |
| return vals.toArray(new Integer[vals.size()]); |
| } |
| |
| @Override |
| public Short[] getSwitchPortVlanIds(SwitchPort swp) { |
| TreeSet<Short> vals = new TreeSet<Short>(); |
| for (Entity e : entities) { |
| if (e.switchDPID == swp.getSwitchDPID() |
| && e.switchPort == swp.getPort()) { |
| if (e.getVlan() == null) |
| vals.add(Ethernet.VLAN_UNTAGGED); |
| else |
| vals.add(e.getVlan()); |
| } |
| } |
| return vals.toArray(new Short[vals.size()]); |
| } |
| |
| @Override |
| public Date getLastSeen() { |
| Date d = null; |
| for (int i = 0; i < entities.length; i++) { |
| if (d == null || |
| entities[i].getLastSeenTimestamp().compareTo(d) > 0) |
| d = entities[i].getLastSeenTimestamp(); |
| } |
| return d; |
| } |
| |
| // *************** |
| // Getters/Setters |
| // *************** |
| |
| @Override |
| public IEntityClass getEntityClass() { |
| return entityClass; |
| } |
| |
| public Entity[] getEntities() { |
| return entities; |
| } |
| |
| // *************** |
| // Utility Methods |
| // *************** |
| |
| /** |
| * Check whether the device contains the specified entity |
| * @param entity the entity to search for |
| * @return the index of the entity, or <0 if not found |
| */ |
| protected int entityIndex(Entity entity) { |
| return Arrays.binarySearch(entities, entity); |
| } |
| |
| // ****** |
| // Object |
| // ****** |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + Arrays.hashCode(entities); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) return true; |
| if (obj == null) return false; |
| if (getClass() != obj.getClass()) return false; |
| Device other = (Device) obj; |
| if (!deviceKey.equals(other.deviceKey)) return false; |
| if (!Arrays.equals(entities, other.entities)) return false; |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("Device [deviceKey="); |
| builder.append(deviceKey); |
| builder.append(", entityClass="); |
| builder.append(entityClass.getName()); |
| builder.append(", MAC="); |
| builder.append(macAddressString); |
| builder.append(", IPs=["); |
| boolean isFirst = true; |
| for (Integer ip: getIPv4Addresses()) { |
| if (!isFirst) |
| builder.append(", "); |
| isFirst = false; |
| builder.append(IPv4.fromIPv4Address(ip)); |
| } |
| builder.append("], APs="); |
| builder.append(Arrays.toString(getAttachmentPoints(true))); |
| builder.append("]"); |
| return builder.toString(); |
| } |
| } |