package unsw.blackout; import unsw.utils.Angle; import java.util.Optional; import java.util.Vector; public abstract class EntityBase { private String id; private Angle position; private double height; private Vector received_files = new Vector(); private Vector sent_files = new Vector(); /** * BaseEntity is a class that contains an ID and a type which represents any object in our network. */ EntityBase(String id, Angle position, double height) { this.id = id; this.position = position; this.height = height; } // Getters. public String getId() { return this.id; } public Angle getAngle() { return this.position; } public double getHeight() { return this.height; } public Vector getReceivedFiles() { return this.received_files; } public Vector getSentFiles() { return this.sent_files; } public void setAngle(Angle angle) { this.position = angle; } /** * Returns the velocity of the entity in km/s. */ public abstract double getVelocity(); /** * Returns the range of the entity in km. */ public abstract double getRange(); /** * Returns a boolean flagging if the entity may communicate with the other entity. * Warning: it is possible that one entity may allow, while the other does not. */ protected abstract boolean isSupportedEntity(EntityBase entity); /** * Returns a boolean flagging if the entity is physically in range and unobstructed * to another entity. */ protected abstract boolean isPhysicallyReachable(EntityBase entity); /** * Returns a boolean flagging if the entity may retransmit messages to other entities. */ protected boolean canRelay() { return false; } /** * Returns a boolean flagging if the entity should NOT delete files when out of range. */ protected boolean canTransient() { return false; } /** * Returns a boolean flagging if the entity may shrink files. */ protected boolean canShrink() { return false; } /** * Returns an optional integer limit on the number of files it may hold. * Returning no value means no limit. */ protected abstract Optional getFileStoreLimit(); /** * Returns an optional integer limit on the number of bytes it may hold across all files. * Returning no value means no limit. */ protected abstract Optional getByteStoreLimit(); /** * Returns an optional integer limit on the number of bytes downloadable per minute. * Returning no value means no limit. */ protected abstract Optional getByteDownloadSpeed(); /** * Returns an optional integer limit on the number of bytes uploadable per minute. * Returning no value means no limit. */ protected abstract Optional getByteUploadSpeed(); /** * Helper function to calculate an offset to the position class in radians. */ protected double getMoveOffsetRadians() { return (this.getVelocity() / this.getHeight()); } /** * Moves the entity to the expected position after 1 minute. */ public void move() { double offset = this.getMoveOffsetRadians(); double rposition = this.position.toRadians() + offset; this.position = Angle.fromRadians(rposition % Math.toRadians(360.0)); } /** * Helper function, returns the number of files that have not been fully transmitted in the vector. */ static private int getNumUncompletedFiles(Vector files) { int count = 0; for (File file : files) { if (file.hasFullyTransmitted()) { continue; } ++count; } return count; } /** * Gets the number of bytes in received files. */ protected int getNumBytesInFiles() { int count = 0; for (File file : this.received_files) { count += file.getContents().length(); } return count; } /** * Returns the speed a file should be downloaded per tick for fairness during multiple transfers. * Empty optional means infinite value. */ private Optional getDownloadBandwidth() { Optional download_speed = this.getByteDownloadSpeed(); if (download_speed.isEmpty()) { return Optional.empty(); } return Optional.of(download_speed.get() / EntityBase.getNumUncompletedFiles(this.getReceivedFiles())); } /** * Returns the speed a file should be uploaded per tick for fairness during multiple transfers. * Empty optional means infinite value. */ private Optional getUploadBandwidth() { Optional upload_speed = this.getByteUploadSpeed(); if (upload_speed.isEmpty()) { return Optional.empty(); } return Optional.of(upload_speed.get() / EntityBase.getNumUncompletedFiles(this.getSentFiles())); } /** * Heper function, returns the min of two optional integers, or a concrete value representing infinity. */ static private int getOptionalMin(Optional a, Optional b) { if (a.isPresent() && b.isPresent()) { return Math.min(a.get(), b.get()); } else if (a.isPresent()) { return a.get(); } else if (b.isPresent()) { return b.get(); } return Integer.MAX_VALUE; } public void removeSentFiles(Vector files) { for (File file : files) { getSentFiles().remove(file); } } /** * Transfers currently sending files by the expected values after 1 minute. * Returns false if the file should be deleted. */ public boolean maybeTransferFile(File file, EntityBase target) { // Delete and early out files if we are out of range - but keep if transient! if (!this.isPhysicallyReachable(target)) { if (!target.canTransient()) { target.getReceivedFiles().remove(file); return false; } return true; } Optional upload_bandwidth = this.getUploadBandwidth(); Optional download_bandwidth = target.getDownloadBandwidth(); // The amount we can send is the min of our upload bandwidth and their download bandwidth. int bandwidth = EntityBase.getOptionalMin(upload_bandwidth, download_bandwidth); file.addBytes(bandwidth); return true; } /** * Recursive DPS helper function to determine whether an entity may communicate with another. */ private boolean canCommunicateSearch(EntityBase target, Vector entities, Vector visited) { visited.add(this); for (EntityBase entity : entities) { if (entity.equals(this)) { continue; } if (visited.contains(entity)) { continue; } if (!this.isSupportedEntity(entity) || !entity.isSupportedEntity(this)) { continue; } if (!this.isPhysicallyReachable(entity)) { continue; } if (target.equals(entity)) { return true; } if (entity.canRelay()) { if (entity.canCommunicateSearch(target, entities, visited)) { return true; } } } return false; } /** * Determines whether an entity may communicate with another entity, including via hops from other entities. */ public boolean canCommunicate(EntityBase target, Vector entities) { Vector visited = new Vector(); return canCommunicateSearch(target, entities, visited); } /** * Helper to extract files via their filenames, returns an empty optional if it does not exist. */ static private Optional getFileFromFilename(String filename, Vector files) { for (File file : files) { if (file.getFilename().equals(filename)) { return Optional.of(file); } } return Optional.empty(); } /** * Sends a file to the entity, while respecting the constraints imposed by both entities. */ public void sendFileTo(String filename, EntityBase target) throws FileTransferException { Optional origin_file = EntityBase.getFileFromFilename(filename, this.getReceivedFiles()); if (origin_file.isEmpty()) { // throw if the file does not exist throw new FileTransferException.VirtualFileNotFoundException(filename); } if (!origin_file.get().hasFullyTransmitted()) { // throw if the file has not fully downloaded throw new FileTransferException.VirtualFileNotFoundException(filename); } // throw if the file already exist or is currently downloading, nicely handles both here if (EntityBase.getFileFromFilename(filename, target.getReceivedFiles()).isPresent()) { throw new FileTransferException.VirtualFileAlreadyExistsException(filename); } Optional upload_speed = this.getByteUploadSpeed(); // throw if we don't have enough bandwidth if (upload_speed.isPresent() && EntityBase.getNumUncompletedFiles(this.sent_files) >= upload_speed.get()) { throw new FileTransferException.VirtualFileNoBandwidthException(this.getId()); } Optional download_speed = target.getByteDownloadSpeed(); // throw if they don't have enough bandwidth if (download_speed.isPresent() && EntityBase.getNumUncompletedFiles(target.getReceivedFiles()) >= download_speed.get()) { throw new FileTransferException.VirtualFileNoBandwidthException(target.getId()); } // Throw if they can't store any more files. Optional download_file_limit = target.getFileStoreLimit(); if (download_file_limit.isPresent() && target.getReceivedFiles().size() >= download_file_limit.get()) { throw new FileTransferException.VirtualFileNoStorageSpaceException("Max Files Reached"); } // Throw if they don't have enough space. Optional download_byte_limit = target.getByteStoreLimit(); int filesize = origin_file.get().getContents().length(); if (download_byte_limit.isPresent() && target.getNumBytesInFiles() + filesize >= download_byte_limit.get()) { throw new FileTransferException.VirtualFileNoStorageSpaceException("Max Storage Reached"); } // Finally we upload the file. String contents = origin_file.get().getContents(); File upload_file = new File(filename, contents, 0, target.getId()); this.getSentFiles().add(upload_file); target.getReceivedFiles().add(upload_file); } // We know that our ID is unique, so that's the only thing we need to compare here. @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; EntityBase other = (EntityBase) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; return true; } }