package net.berack.upo.valpre.sim; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import org.objenesis.strategy.StdInstantiatorStrategy; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; /** * A class that represents a network of queues, each with its own servers. * The network in question is created by adding a node and then establishing * connections between nodes. In order to start a simulation, at least one node * must be a Source or must generate at least one event to be processed. */ public final class Net implements Iterable { private final List servers = new ArrayList<>(); private final HashMap indices = new HashMap<>(); private final List> connections = new ArrayList<>(); /** * Adds a new server node to the network. * The unique identifier for the nodes is the name and, if you try to add a node * that has the same name of another, then the method will return an exception * * @param node The server node to add. * @throws IllegalArgumentException if the node already exist * @return the index of the created node */ public int addNode(ServerNode node) { if (this.indices.containsKey(node)) throw new IllegalArgumentException("Node already exist"); var index = this.servers.size(); this.servers.add(node); this.indices.put(node, index); this.connections.add(new ArrayList<>()); return index; } /** * Adds a connection between the nodes with the given weight to select it. * The weight must be > 0 and the nodes must be already added to the net. * If the connection is already present then the new weight is used. * * @param parent The parent node. * @param child The child node to add. * @param weight The probability of the child node. * @throws NullPointerException if one of the two nodes are not in the net * @throws IllegalArgumentException if the weight is negative or zero */ public void addConnection(ServerNode parent, ServerNode child, double weight) { var nodeP = this.indices.get(parent); var nodeC = this.indices.get(child); this.addConnection(nodeP, nodeC, weight); } /** * Adds a connection between the nodes with the given weight to select it. * The weight must be > 0 and the nodes must be already added to the net. * If the connection is already present then the new weight is used. * * @param parent The parent node index. * @param child The child node index to add. * @param weight The probability of the child node. * @throws IndexOutOfBoundsException if one of the two nodes are not in the net * @throws IllegalArgumentException if the weight is negative or zero */ public void addConnection(int parent, int child, double weight) { if (weight <= 0) throw new IllegalArgumentException("Weight must be > 0"); var max = this.servers.size() - 1; if (parent < 0 || child < 0 || parent > max || child > max) throw new IndexOutOfBoundsException("One of the nodes does not exist"); var list = this.connections.get(parent); list.removeIf(conn -> conn.index == child); list.add(new Connection(child, weight)); } /** * Get the total number of the nodes in the net * * @return the size of the net */ public int size() { return this.servers.size(); } /** * Return the index of the node based on the name passed as input. * Note that this will iterate over all the nodes. * * @param name the name of the node * @return the node */ public int getNodeIndex(String name) { for (var entry : this.indices.entrySet()) { if (entry.getKey().name.equals(name)) return entry.getValue(); } return -1; } /** * Return a node based on the hash of the string name passed as input * * @param name the name of the node * @return the node */ public ServerNode getNode(String name) { var index = this.getNodeIndex(name); return index < 0 ? null : this.servers.get(index); } /** * Return a node based on the index, faster than recovering it by the name * * @param index the index of the node * @return the node * @throws IndexOutOfBoundsException if the index is not in the range */ public ServerNode getNode(int index) { return this.servers.get(index); } /** * Get a list of all the children of the parent. * In the list there is the node and the weight associated with. * * @param parent the parent node * @throws IndexOutOfBoundsException If the index is not in the range * @return the resultig node */ public List getChildren(int parent) { var children = new ArrayList(); for (var conn : this.connections.get(parent)) { var listEntry = new Connection(conn.index, conn.weight); children.add(listEntry); } return children; } /** * Normalizes the weights in each connections so that their sum equals 1. * This method should be called by the user if they have inserted weights that * are not summing to 1 or are unsure. */ public void normalizeWeights() { for (var node = 0; node < this.connections.size(); node++) { var list = this.connections.get(node); var sum = 0.0d; for (var conn : list) sum += conn.weight; var newOne = new ArrayList(); for (var conn : list) { var newWeight = conn.weight / sum; newOne.add(new Connection(conn.index, newWeight)); } this.connections.set(node, newOne); } } /** * Build the node states for the simulation. * This method is used to create the state of each node in the network. * Note that each call to this method will create a new state for each node. * * @return the array of node states */ public ServerNodeState[] buildNodeStates() { var states = new ServerNodeState[this.servers.size()]; for (var i = 0; i < states.length; i++) states[i] = new ServerNodeState(i, this); return states; } /** * Save the current net to a file. * The resulting file is saved with Kryo. * * @param file the name of the file * @throws FileNotFoundException if the path doesn't exist */ public void save(String file) throws FileNotFoundException { var kryo = new Kryo(); kryo.setRegistrationRequired(false); try (var out = new Output(new FileOutputStream(file))) { kryo.writeClassAndObject(out, this); } } @Override public Iterator iterator() { return this.servers.iterator(); } /** * Load the net from the file passed as input. * The net will be the same as the one saved. * * @param file the file to load * @return a new Net object * @throws KryoException if the file saved is not a net * @throws IOException if the file is not found */ public static Net load(String file) throws KryoException, IOException { try (var stream = new FileInputStream(file)) { return Net.load(stream); } } /** * Load the net from the stream passed as input. * The net will be the same as the one saved. * * @param stream the input stream to read * @return a new Net object * @throws KryoException if the file saved is not a net */ public static Net load(InputStream stream) throws KryoException { var kryo = new Kryo(); kryo.setRegistrationRequired(false); kryo.setInstantiatorStrategy(new StdInstantiatorStrategy()); try (var in = new Input(stream)) { return (Net) kryo.readClassAndObject(in); } } /** * Create a copy of the net passed as input. * The new net will have the same nodes and connections. * * @param net the net to copy * @return a new Net object */ public static Net copyOf(Net net) { var newNet = new Net(); for (var index = 0; index < net.size(); index++) { var node = net.servers.get(index); var conn = net.connections.get(index); conn = new ArrayList<>(conn); newNet.indices.put(node, index); newNet.servers.add(node); newNet.connections.add(conn); } return newNet; } @Override public String toString() { var builder = new StringBuilder(); try { for (var node : this.servers) { builder.append(node) .append(" -> "); for (var child : this.getChildren(this.indices.get(node))) { var childNode = this.servers.get(child.index); builder.append(childNode.name) .append("(") .append(child.weight) .append("), "); } builder.delete(builder.length() - 2, builder.length()) .append("\n"); } } catch (Exception e) { e.printStackTrace(); } return builder.toString(); } /** * A Static inner class used to represent the connection of a node */ public static class Connection { public final int index; public final double weight; private Connection(int index, double weight) { this.index = index; this.weight = weight; } } }