import java.awt.*; import java.awt.event.*; import java.io.*; import java.lang.reflect.*; import java.net.*; import java.util.*; import java.util.List; import java.util.zip.*; import javax.swing.*; import javax.swing.event.*; // CSE 142 Homework 8 (Critter Safari) // Authors: Marty Stepp, Stuart Reges // // Provides the main method for the simulation. // class CritterMain { public static void main(String[] args) { CritterModel model = new CritterModel(60, 50); // create simulation // add 25 of each animal to the simulation // (if you want to test individual animals, // comment the addAllCritterClasses line and // uncomment the various add line(s) below.) ClassUtils.addAllCritterClasses(model, 25); // model.add(25, Stone.class); // model.add(25, Bear.class); // model.add(25, Lion.class); // model.add(25, Tiger.class); // model.add(25, Wolf.class); new CritterGui(model).start(); // run the GUI } } // CSE 142 Homework 8 (Critter Safari) // Authors: Marty Stepp, Stuart Reges // // The overall graphical user interface for the Critter simulation. // class CritterGui implements ActionListener, ChangeListener, Observer, WindowListener { // class constants private static final String TITLE = "CSE 142 Critter Safari"; public static final String SAVE_STATE_FILE_NAME = "_hw8_gui_saved_settings.txt"; private static final long serialVersionUID = 0; private static final int DELAY = 100; // default MS between redraws private static final int DEFAULT_NUMBER_OF_CRITTERS = 25; // # of critters to add private static final int MAX_CLASS_NAME_LENGTH = 12; private static final boolean SHOULD_PRINT_EXCEPTIONS = true; // constants for saving/loading GUI state private static final boolean SHOULD_SAVE_SETTINGS = true; private static final String LAST_HOST_NAME_KEY = "lastHostName"; private static final String FPS_KEY = "fps"; private static final String ACCEPT_KEY = "accept"; private static final String ALWAYS_VALUE = "always"; private static final String ASK_VALUE = "ask"; private static final String NEVER_VALUE = "never"; // constant for loading files from the course web site private static final boolean ENABLE_LOAD_FROM_WEB = true; private static final String CODE_BASE = "http://www.cs.washington.edu/education/courses/cse142/07wi/homework/a8/student_wolves.zip"; // Helper method to create a JButton with the given properties. public static JButton createButton(String text, char mnemonic, ActionListener listen, Container panel) { JButton button = new JButton(text); if (mnemonic != '\0') { button.setMnemonic(mnemonic); } button.addActionListener(listen); if (panel != null) { panel.add(button); } return button; } // Helper method to create a JCheckBox with the given properties. public static JCheckBox createCheckBox(String text, char mnemonic, ActionListener listen, Container panel) { JCheckBox box = new JCheckBox(text); if (mnemonic != '\0') { box.setMnemonic(mnemonic); } box.addActionListener(listen); if (panel != null) { panel.add(box); } return box; } // Helper method to create a JRadioButton with the given properties. public static JRadioButton createRadioButton(String text, char mnemonic, boolean selected, ButtonGroup group, ActionListener listen, Container panel) { JRadioButton button = new JRadioButton(text, selected); if (mnemonic != '\0') { button.setMnemonic(mnemonic); } button.addActionListener(listen); if (panel != null) { panel.add(button); } if (group != null) { group.add(button); } return button; } // Helper method to create a JSlider with the given properties. public static JSlider createSlider(int min, int max, int initial, int majorTick, int minorTick, ChangeListener listen, Container panel) { JSlider slider = new JSlider(min, max, initial); slider.setMajorTickSpacing(majorTick); slider.setMinorTickSpacing(minorTick); slider.setSnapToTicks(true); slider.setPaintTicks(true); // slider.setPaintLabels(true); Dimension size = slider.getPreferredSize(); slider.setPreferredSize(new Dimension(size.width / 2, size.height)); slider.addChangeListener(listen); if (panel != null) { panel.add(slider); } return slider; } // fields private CritterModel model; private CritterPanel panel; private JFrame frame; private javax.swing.Timer timer; private JButton go, stop, tick, reset, loadFromWeb; private JSlider slider; private JComponent east; private JRadioButton always, never, ask; private String lastHostName = ""; private JLabel moves; // receives critters from others and lets me voluntarily send mine out // packets = [hostname, classname, critter text] private CritterNetworkManager networkSenderListener; // lets me host my wolf so others can reach out and request it from me // packets = [hostname, classname requested] private CritterNetworkManager networkServer; // keeps track of which classes already have a ClassPanel on the east side // of the window, so we know when we need to add a new one (on network receive etc) private Map counts; // Constructs a new GUI to display the given model of critters. public CritterGui(CritterModel model) { this.model = model; model.addObserver(this); // try to load settings from disk (fail silently) Properties props = null; if (SHOULD_SAVE_SETTINGS) { try { props = loadConfiguration(); } catch (IOException ioe) {} } // set up network listeners networkSenderListener = new CritterNetworkManager(); networkSenderListener.getReceiveEvent().addObserver(this); networkSenderListener.getErrorEvent().addObserver(this); networkServer = new CritterNetworkManager(CritterNetworkManager.DEFAULT_PORT_2); networkServer.getReceiveEvent().addObserver(this); networkServer.getErrorEvent().addObserver(this); // set up critter picture panel and set size panel = new CritterPanel(model); panel.setBorder(BorderFactory.createLineBorder(Color.BLACK)); // add the animation timer timer = new javax.swing.Timer(DELAY, this); timer.setCoalesce(true); // east panel to store critter class info counts = new TreeMap(); east = new JPanel(new GridLayout(0, 1)); east.setBorder(BorderFactory.createTitledBorder("Classes (Alive+Kill=Total):")); if (ENABLE_LOAD_FROM_WEB) { loadFromWeb = createButton("Load from Web...", 'L', this, east); } // timer controls JPanel southcenter = new JPanel(); go = createButton("Go", 'G', this, southcenter); go.setBackground(Color.GREEN); stop = createButton("Stop", 'S', this, southcenter); stop.setBackground(new Color(255, 96, 96)); tick = createButton("Tick", 'T', this, southcenter); tick.setBackground(Color.YELLOW); reset = createButton("Reset", 'R', this, southcenter); // slider for animation speed JPanel southwest = new JPanel(); southwest.add(new JLabel("Speed:")); slider = createSlider(1, 41, 1000 / DELAY, 20, 5, this, southwest); moves = new JLabel("0 moves"); southwest.add(moves); // checkbox JPanel southeast = new JPanel(); // new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 5)); southeast.setBorder(BorderFactory.createTitledBorder("Accept requests:")); ButtonGroup group = new ButtonGroup(); always = createRadioButton("Always", 'A', false, group, this, southeast); always.setToolTipText("When selected, automatically accepts critters sent to you " + "and automatically shares requested critters."); ask = createRadioButton("Ask", 'k', true, group, this, southeast); ask.setToolTipText("When selected, prompts you when critters are sent to you " + "and when requested to share your critters."); never = createRadioButton("Never", 'N', false, group, this, southeast); never.setToolTipText("When selected, never accepts critters sent to you " + "and refuses all requests to share your critters."); // south panel to hold various widgets Container south = new JPanel(new BorderLayout()); south.add(southcenter); south.add(southwest, BorderLayout.WEST); south.add(southeast, BorderLayout.EAST); JPanel center = new JPanel(); center.add(panel); // use saved settings, if any (fail silently) if (props != null) { try { slider.setValue(Integer.parseInt(props.getProperty(FPS_KEY))); String accept = props.getProperty(ACCEPT_KEY); if (accept.equals(ALWAYS_VALUE)) { always.setSelected(true); } else if (accept.equals(NEVER_VALUE)) { never.setSelected(true); } else if (accept.equals(ASK_VALUE)) { ask.setSelected(true); } lastHostName = props.getProperty(LAST_HOST_NAME_KEY, ""); // timer.setDelay(Integer.parseInt(props.getProperty(FPS_KEY))); } catch (Exception e) {} } // create frame and do layout frame = new JFrame(); frame.setTitle(TITLE + ": " + CritterNetworkManager.getHostName() + " " + CritterNetworkManager.getIpAddresses()); // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.addWindowListener(this); // frame.setResizable(false); frame.add(center, BorderLayout.CENTER); frame.add(south, BorderLayout.SOUTH); frame.add(east, BorderLayout.EAST); } // Responds to action events in the GUI. public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if (src == go) { timer.start(); } else if (src == stop) { timer.stop(); } else if (src == timer || (src == tick && !timer.isRunning())) { model.update(); } else if (src == reset) { model.reset(); } else if (src == loadFromWeb) { new Thread(new ZipDownloader()).start(); } } // Responds to change events on the slider. public void stateChanged(ChangeEvent e) { int fps = slider.getValue(); timer.setDelay(1000 / fps); } // Starts the simulation. Assumes all critters have already been added. public void start() { setCounts(); frame.pack(); frame.setVisible(true); // start network listeners try { networkSenderListener.start(); networkServer.start(); } catch (java.net.BindException e) { JOptionPane.showMessageDialog(frame, "Error: The network is already in use.\n\n" + "If you want to be able to send and receive critters over the network,\n" + "please close all instances of the Critter Safari and run it again.", "Network in use", JOptionPane.ERROR_MESSAGE); } catch (IOException e) { JOptionPane.showMessageDialog(frame, "Error starting network listener:\n" + e, "Network error", JOptionPane.ERROR_MESSAGE); if (SHOULD_PRINT_EXCEPTIONS) { e.printStackTrace(); } } frame.toFront(); } // Responds to Observable updates in the model. public void update(Observable o, Object arg) { if (o == model) { // model is notifying us of an update if (arg == CritterModel.Event.ADD || arg == CritterModel.Event.REMOVE_ALL || arg == CritterModel.Event.UPDATE) { setCounts(); moves.setText(model.getMoveCount() + " moves"); } // TODO: remove overall gui as observer of model? } else if (o == networkSenderListener.getReceiveEvent() && arg != null) { // we received a message (a class to load) if (never.isSelected()) { return; } String[] strings = (String[]) arg; loadClassText(strings); // setCounts(); } else if (o == networkServer.getReceiveEvent() && arg != null) { // we received a message (a request to send our wolf) if (never.isSelected()) { // refuse all requests return; } String[] strings = (String[]) arg; String hostName = strings[0]; String className = strings[1]; if (ask.isSelected()) { // "Always Accept" not checked, so ask to confirm int choice = JOptionPane.showConfirmDialog(frame, "Host \"" + hostName + "\" requests your " + className + " class. Send it?", "Critter send request", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (choice != JOptionPane.YES_OPTION) { // refuse request (send back a null answer) sendJavaFile(null, hostName); return; } } // user confirmed, so send the class data sendJavaFile(className, hostName); } else if (o == networkSenderListener.getErrorEvent() || o == networkServer.getErrorEvent()) { // something failed in a network send/receive attempt Exception e = (Exception) arg; String message; if (e instanceof UnknownHostException) { message = "Cannot reach target computer:\n\n" + e; } else if (e instanceof ConnectException) { message = "Target computer refused connection:\n\n" + e; } else if (e instanceof IOException) { message = "I/O error:\n" + e; } else { message = e.toString(); } JOptionPane.showMessageDialog(frame, message, "network error", JOptionPane.ERROR_MESSAGE); if (SHOULD_PRINT_EXCEPTIONS) { e.printStackTrace(); } } } // Called when the window is about to close. // Used to save the GUI's settings. public void windowClosing(WindowEvent e) { if (SHOULD_SAVE_SETTINGS) { try { saveConfiguration(); } catch (Exception ex) { if (SHOULD_PRINT_EXCEPTIONS) { ex.printStackTrace(); } } } System.exit(0); } // Required to implement WindowListener interface. public void windowActivated(WindowEvent e) {} public void windowClosed(WindowEvent e) {} public void windowDeactivated(WindowEvent e) {} public void windowDeiconified(WindowEvent e) {} public void windowIconified(WindowEvent e) {} public void windowOpened(WindowEvent e) {} // Helper method to read an integer input from a set of choices. private int getInput(String message, Object defaultValue, Object... choices) { Object countStr = JOptionPane.showInputDialog(frame, message, "Question", JOptionPane.QUESTION_MESSAGE, null, choices, defaultValue); if (countStr == null) { return -1; } try { return Integer.parseInt(countStr.toString()); } catch (NumberFormatException e) { return -1; } } // Helper method to read a String input (with the given initial String in the field) // and return a default value if an empty string is entered. private String getInput(String message, String initialValue, String defaultValue) { String input = JOptionPane.showInputDialog(frame, message, "Question", JOptionPane.QUESTION_MESSAGE, null, null, initialValue).toString(); if (input != null && input.length() == 0) { input = defaultValue; } return input; } /* // Loads a class received over the network. // The given array contains [host name, class name, encoded base64 classfile text] @SuppressWarnings("unchecked") private void loadClassEncoded(String[] strings) { String hostName = strings[0]; String className = strings[1]; String encodedFileText = strings[2]; if (encodedFileText == null) { // they refused our request and sent back a null JOptionPane.showMessageDialog(frame, hostName + " refused the request.", "Request refused", JOptionPane.ERROR_MESSAGE); return; } // find out how many critters to add to the world int count = DEFAULT_NUMBER_OF_CRITTERS; if (ask.isSelected()) { count = getInput("Received " + className + " from host \"" + hostName + "\".\nHow many to add? (Or Cancel to refuse this class)", DEFAULT_NUMBER_OF_CRITTERS, 0, 1, 25, 50, 100); if (count < 0) { return; } } // try to compile and load the received class, add it to simulation try { Class critterClass = (Class) ClassUtils .writeAndLoadEncodedClass(encodedFileText, className); model.add(count, critterClass); } catch (CritterModel.TooManyCrittersException e) { JOptionPane.showMessageDialog(frame, "Error: Not enough room to add all critters", "Too many critters", JOptionPane.ERROR_MESSAGE); } catch (CritterModel.InvalidCritterClassException e) { JOptionPane.showMessageDialog(frame, "Problem with critter class:\n" + e + "\n\n" + "This is probably DrJava's fault; DrJava has some issues with dynamically loading code over the network.\n" + "If you've got a " + className + ClassUtils.CLASS_EXTENSION + " file in your program's folder, the file got to you successfully, but\n" + "DrJava wasn't able to load it into the simulator.\n\n" + "Try closing and re-running the simulator; this will give DrJava a chance to see the new animal type and load it.\n" + "Hopefully, when you re-run the simulator, the " + className + " type will appear.", "Problem with critter class", JOptionPane.ERROR_MESSAGE); } catch (ClassNotFoundException cnfe) { JOptionPane.showMessageDialog(frame, "Error loading class:\n" + cnfe, "Error", JOptionPane.ERROR_MESSAGE); cnfe.printStackTrace(); } catch (IOException ioe) { JOptionPane.showMessageDialog(frame, "Error loading class:\n" + ioe, "Error", JOptionPane.ERROR_MESSAGE); ioe.printStackTrace(); } } */ // Loads a class received over the network. // The given array contains [host name, class name, class text] @SuppressWarnings("unchecked") private void loadClassText(String[] strings) { String hostName = strings[0]; String className = strings[1]; String fileText = strings[2]; if (fileText == null) { // they refused our request and sent back a null JOptionPane.showMessageDialog(frame, hostName + " refused the request.", "Request refused", JOptionPane.ERROR_MESSAGE); return; } // find out how many critters to add to the world int count = DEFAULT_NUMBER_OF_CRITTERS; if (ask.isSelected()) { count = getInput("Received " + className + " from host \"" + hostName + "\".\nHow many to add? (Or Cancel to refuse this class)", DEFAULT_NUMBER_OF_CRITTERS, 0, 1, 25, 50, 100); if (count < 0) { return; } } // try to compile and load the received class, add it to simulation try { Class critterClass = (Class) ClassUtils .writeAndLoadClass(fileText, className, true); model.add(count, critterClass); } catch (CritterModel.TooManyCrittersException e) { JOptionPane.showMessageDialog(frame, "Error: Not enough room to add all critters", "Too many critters", JOptionPane.ERROR_MESSAGE); } catch (CritterModel.InvalidCritterClassException e) { if (ClassUtils.isDrJavasFault(className)) { JOptionPane.showMessageDialog(frame, className + " received; simulator must be restarted.\n" + "Try closing the GUI and re-running CritterMain.", "Restart required", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(frame, "Problem with critter class:\n" + e + "\n\n" + "This is probably DrJava's fault; DrJava has some issues with dynamically loading code over the network.\n" + "If you've got a " + className + ClassUtils.CLASS_EXTENSION + " file in your program's folder, the file got to you successfully, but\n" + "DrJava wasn't able to load it into the simulator.\n\n" + "Try closing and re-running the simulator; this will give DrJava a chance to see the new animal type and load it.\n" + "Hopefully, when you re-run the simulator, the " + className + " type will appear.", "Problem with critter class", JOptionPane.ERROR_MESSAGE); if (SHOULD_PRINT_EXCEPTIONS) { e.printStackTrace(); } } } catch (ClassNotFoundException e) { JOptionPane.showMessageDialog(frame, "Unable to find the Java compiler.\n" + "If you aren't using DrJava, try running the simulator from there.", "Error", JOptionPane.ERROR_MESSAGE); if (SHOULD_PRINT_EXCEPTIONS) { e.printStackTrace(); } } catch (Exception e) { JOptionPane.showMessageDialog(frame, "Error loading class:\n" + e, "Error", JOptionPane.ERROR_MESSAGE); if (SHOULD_PRINT_EXCEPTIONS) { e.printStackTrace(); } } } private Properties loadConfiguration() throws IOException { Properties prop = new Properties(); prop.load(new FileInputStream(SAVE_STATE_FILE_NAME)); return prop; } private void saveConfiguration() throws IOException { Properties prop = new Properties(); int fps = slider.getValue(); prop.setProperty(LAST_HOST_NAME_KEY, lastHostName); prop.setProperty(FPS_KEY, String.valueOf(fps)); if (always.isSelected()) { prop.put(ACCEPT_KEY, ALWAYS_VALUE); } else if (ask.isSelected()) { prop.put(ACCEPT_KEY, ASK_VALUE); } else if (never.isSelected()) { prop.put(ACCEPT_KEY, NEVER_VALUE); } prop.store(new PrintStream(SAVE_STATE_FILE_NAME), "this is a comment"); } /* // sends the given class code to the given host computer // if className is null, sends null text to signify request refused private void sendClassFile(String className, String hostName) { try { String encodedFileText = null; String newClassName = className; if (className != null) { // new class name = old one + current user name? // e.g. "Wolf" --> "Wolf_Stepp" String userName = System.getProperty("user.name"); if (userName.length() > 0) { userName = userName.substring(0, 1).toUpperCase() + userName.substring(1).toLowerCase(); } newClassName += "_" + userName; encodedFileText = ClassUtils.renameCompileEncode(className, newClassName); } networkSenderListener.sendText(hostName, newClassName, encodedFileText); } catch (IOException e) { JOptionPane.showMessageDialog(frame, "Error reading file:\n" + e, "I/O Error", JOptionPane.ERROR_MESSAGE); } catch (ClassNotFoundException e) { JOptionPane.showMessageDialog(frame, "Error preparing class to send:\n" + e, "Class Error", JOptionPane.ERROR_MESSAGE); } } */ // sends the given class code to the given host computer // if className is null, sends null text to signify request refused private void sendJavaFile(String className, String hostName) { try { String fileText = null; String newClassName = className; if (className != null) { // new class name = old one + current user name? // e.g. "Wolf" --> "Wolf_Stepp" String userName = System.getProperty("user.name"); if (userName.length() > 0) { userName = userName.substring(0, 1).toUpperCase() + userName.substring(1).toLowerCase(); } newClassName += "_" + userName; // rename and read fileText = ClassUtils.readAndRename(className, newClassName); } networkSenderListener.sendText(hostName, newClassName, fileText); } catch (IOException ioe) { JOptionPane.showMessageDialog(frame, "Error reading file:\n" + ioe, "I/O Error", JOptionPane.ERROR_MESSAGE); } } // Adds right-hand column of labels showing how many of each type are alive. // Updates the counter labels to store the current count information. private void setCounts() { Set classNames = model.getClassNames(); if (classNames.size() == counts.size()) { return; // nothing to do } for (ClassPanel cpanel : counts.values()) { east.remove(cpanel); } counts.clear(); for (String className : classNames) { ClassPanel cpanel = new ClassPanel(className); east.add(cpanel); counts.put(className, cpanel); } east.validate(); frame.pack(); } // One of the east panels representing a critter class. private class ClassPanel extends JPanel implements ActionListener, Observer { private static final long serialVersionUID = 0; // fields private String className; private JButton send, request, delete; // Constructs a new ClassPanel to hold info about the given critter class. public ClassPanel(String className) { this.className = className; updateBorder(); model.addObserver(this); if (ClassUtils.isNetworkClass(className)) { setLayout(new BorderLayout(0, 0)); delete = createButton("Remove", '\0', this, null); delete.setForeground(Color.RED.darker()); delete.setToolTipText("Remove all animals of type " + className); add(delete, BorderLayout.NORTH); } else { setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); send = createButton("Send", '\0', this, this); request = createButton("Request", '\0', this, this); } } // Handles action events in this panel. public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if (src == send) { if (ClassUtils.isNetworkClass(className)) { return; // skip network classes } // ask what computer to send to String hostName = getInput("Send your " + className + " to what computer name or IP address?\n" + "(or leave blank to connect to local host)\n ", lastHostName, "localhost"); if (hostName != null) { // send class to computer lastHostName = (hostName.equals("localhost") ? "" : hostName); sendJavaFile(className, hostName); } } else if (src == request) { // ask what computer to request from String hostName = getInput("Request " + className + " from what computer name or IP address?\n" + "(or leave blank to request from local host)\n ", lastHostName, "localhost"); if (hostName != null) { // request class from computer lastHostName = (hostName.equals("localhost") ? "" : hostName); networkServer.requestClass(hostName, className); } } else if (src == delete) { // delete this network class from the system String classFileName = className + ClassUtils.CLASS_EXTENSION; model.removeAll(className); // set the .class file to be deleted when the VM exits // BUG: if they then send us that same file while GUI is running, // I'm unable to cancel the delete-on-exit request and it'll // get deleted when you close your GUI. Oh well. int choice = JOptionPane.showConfirmDialog(frame, "Delete the " + classFileName + " file from your disk?", "Delete class?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (choice == JOptionPane.YES_OPTION) { new File(classFileName).deleteOnExit(); } } } // Handles Observable updates from the model. public void update(Observable o, Object arg) { updateBorder(); } // Updates the border text to private void updateBorder() { int count = model.getCount(className); int kills = model.getKills(className); setBorder(BorderFactory.createTitledBorder(trimClassName() + " " + count + "+" + kills + "=" + (count + kills))); } private String trimClassName() { if (className.length() <= MAX_CLASS_NAME_LENGTH) { return className; } else { return className.substring(0, MAX_CLASS_NAME_LENGTH) + "~"; } } } // A small class to pop up a list of items and let the user select them. private class ListOptionPane extends JDialog implements ActionListener { private static final long serialVersionUID = 0; private JList list; private JButton ok, cancel; private boolean pressedOk = false; public ListOptionPane(Collection items) { super(frame, true); setTitle("Load..."); list = new JList(items.toArray()); list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); list.setVisibleRowCount(Math.min(12, items.size())); JPanel south = new JPanel(); ok = createButton("OK", 'O', this, south); cancel = createButton("Cancel", 'C', this, south); add(new JScrollPane(list)); add(south, BorderLayout.SOUTH); pack(); setLocation(frame.getX() + (frame.getWidth() - getWidth()) / 2, frame.getY() + (frame.getHeight() - getHeight()) / 2); } public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if (src == ok) { pressedOk = true; } else if (src == cancel) { setVisible(false); // dispose(); } setVisible(false); } public Object[] getSelectedValues() { return list.getSelectedValues(); } public boolean pressedOk() { return pressedOk; } } private class ZipDownloader implements Runnable { @SuppressWarnings("unchecked") public void run() { loadFromWeb.setEnabled(false); try { String filename = JOptionPane.showInputDialog("File to read?", CODE_BASE); if (filename == null || (filename = filename.trim()).length() == 0) { return; } Map zipFilesMap = ClassUtils.getZipFileContents(new URL(filename)); // remove ".class" from file names Set classNames = new TreeSet(); for (String fileName : zipFilesMap.keySet()) { classNames.add(fileName.replaceAll("\\.class", "")); } // filter out inner classes from list Set innerClasses = new TreeSet(); for (Iterator i = classNames.iterator(); i.hasNext(); ) { String className = i.next(); if (ClassUtils.isInnerClass(className)) { i.remove(); // String outerClassName = className.substring(0, className.indexOf('$')); innerClasses.add(className); } } // show dialog box to user so they can select wolf file(s) ListOptionPane dialog = new ListOptionPane(classNames); dialog.setVisible(true); if (!dialog.pressedOk()) { return; } Object[] selectedItems = dialog.getSelectedValues(); // include any necessary inner classes in list of files to download Set selectedClasses = new TreeSet(); for (Object item : selectedItems) { String className = item.toString(); selectedClasses.add(className); // include any inner classes associated with this class for (String innerClassName : innerClasses) { if (innerClassName.startsWith(className)) { selectedClasses.add(innerClassName); } } } // write the selected classes as files to disk for (String className : selectedClasses) { String fileName = className + ClassUtils.CLASS_EXTENSION; byte[] bytes = zipFilesMap.get(fileName); ClassUtils.writeBytes(bytes, fileName); } // load the classes into the JVM for (String className : selectedClasses) { if (ClassUtils.isInnerClass(className)) { continue; } String fileName = className + ClassUtils.CLASS_EXTENSION; Class critterClass = (Class) ClassUtils.loadClass(fileName); model.add(DEFAULT_NUMBER_OF_CRITTERS, critterClass); } } catch (IOException e) { JOptionPane.showMessageDialog(frame, "Error downloading ZIP data:\n" + e, "I/O error", JOptionPane.ERROR_MESSAGE); if (SHOULD_PRINT_EXCEPTIONS) { e.printStackTrace(); } } catch (Exception e) { JOptionPane.showMessageDialog(frame, "Error loading class data:\n" + e, "Class loading error", JOptionPane.ERROR_MESSAGE); if (SHOULD_PRINT_EXCEPTIONS) { e.printStackTrace(); } } loadFromWeb.setEnabled(true); } } } // CSE 142 Homework 8 (Critter Safari) // Authors: Marty Stepp, Stuart Reges, Steve Gribble // // The model of all critters in the simulation. // // Performance profiled with Java HProf. // To profile heap memory/object usage: // java -Xrunhprof CritterMain // To profile CPU cycles: // java -Xrunhprof:cpu=old,thread=y,depth=10,cutoff=0,format=a CritterMain // View HProf data with HPjmeter software (Google for it). // class CritterModel extends Observable implements Iterable { // class constants public static final String EMPTY = "."; // largest value that will be passed to the constructor of a critter that // takes an int as a parameter private static final int INT_PARAM_MAX = 16; private static final Color BROWN = new Color(128, 128, 64); // fields private final int height; private final int width; private final Critter[][] grid; private final String[][] display; private final Color[][] colorDisplay; private final Random rand; private final List critterList; private final Map locationMap; private final SortedMap classStateMap; private int moveCount; // fields to cache CritterInfo data (otherwise many are created on each update) private CritterInfoImpl info; private Point infoPoint; private Point neighborPoint; // Constructs a new model of the given size. public CritterModel(int width, int height) { // check for invalid model size if (width <= 0 || height <= 0) { throw new IllegalArgumentException(); } this.width = width; this.height = height; grid = new Critter[width][height]; display = new String[width][height]; colorDisplay = new Color[width][height]; updateDisplay(Event.NEW); rand = new Random(); moveCount = 0; // initialize various data structures critterList = new ArrayList(); locationMap = new HashMap(); classStateMap = new TreeMap(); } // Adds the given number of critters of the given type to the simulation. public synchronized void add(int number, Class critterClass) { // count # of critters of each class String className = critterClass.getName(); if (!classStateMap.containsKey(className)) { classStateMap.put(className, new CritterClassState(critterClass)); } try { // call private helper add method many times for (int i = 0; i < number; i++) { add(critterClass); } } catch (IllegalAccessException e) { throw new InvalidCritterClassException(e); } catch (InvocationTargetException e) { throw new InvalidCritterClassException(e); } catch (InstantiationException e) { throw new InvalidCritterClassException(e); } updateDisplay(Event.ADD); } // Returns the color that should be displayed on the given (x, y) location, // or black if nothing is there. public Color getColor(int x, int y) { return colorDisplay[x][y]; } // Returns a set of all names of Critter classes that exist in this model. public Set getClassNames() { return Collections.unmodifiableSet(classStateMap.keySet()); } // Returns a set of [class name, count] entry pairs in this model. public Set> getClassStates() { return Collections.unmodifiableSet(classStateMap.entrySet()); } // Returns how many critters of the given type exist in the world. public int getCount(String className) { if (classStateMap.containsKey(className)) { return classStateMap.get(className).count; } else { return 0; } } // Returns the height of this model. public int getHeight() { return height; } // Returns how many critters of the given type exist in the world. public int getKills(String className) { if (classStateMap.containsKey(className)) { return classStateMap.get(className).kills; } else { return 0; } } // Returns number of updates made to this model. public int getMoveCount() { return moveCount; } // Returns the String of text to display at the given (x, y) location. public String getString(int x, int y) { return display[x][y]; } // Returns the total number of critters in this model. public int getTotalCritterCount() { return locationMap.keySet().size(); } // Returns the width of this model. public int getWidth() { return width; } // Returns an iterator of the critters in this model. public Iterator iterator() { return Collections.unmodifiableList(critterList).iterator(); } // Restarts the model and reloads the critters. public synchronized void reset() { for (Map.Entry entry : classStateMap.entrySet()) { String className = entry.getKey(); // wipe the class entry for this animal type CritterClassState classState = entry.getValue(); // remove all animals of this type int count = entry.getValue().initialCount; removeAll(className, false); classState.reset(); // add them back Class critterClass = entry.getValue().critterClass; add(count, critterClass); } moveCount = 0; setChanged(); notifyObservers(Event.RESET); } // Removes all critters of the given type from the simulation. public synchronized void removeAll(String className) { removeAll(className, true); } // Removes all critters of the given type from the simulation; // if permanent is true, they won't revive even after a reset. private synchronized void removeAll(String className, boolean permanent) { for (Iterator i = critterList.iterator(); i.hasNext(); ) { Critter critter = i.next(); if (critter.getClass().getName().equals(className)) { // delete this critter i.remove(); Point location = locationMap.remove(critter); if (grid[location.x][location.y] == critter) { grid[location.x][location.y] = null; // display[location.x][location.y] = EMPTY; } } } if (permanent) { // TODO: this might cause a ConcurrentModificationException // if done while game is running... classStateMap.remove(className); } updateDisplay(Event.REMOVE_ALL); } // Moves the position of all critters and handles collisions. public synchronized void update() { moveCount++; // reorder the list to be fair about move/collision order Collections.shuffle(critterList); // move each critter to its new position if (info == null) { infoPoint = new Point(); neighborPoint = new Point(); info = new CritterInfoImpl(infoPoint); } for (int i = 0; i < critterList.size(); i++) { Critter critter1 = critterList.get(i); // move the critter Point location = locationMap.get(critter1); grid[location.x][location.y] = null; String critter1ToString = display[location.x][location.y]; display[location.x][location.y] = EMPTY; infoPoint.setLocation(location); int move = critter1.getMove(info); if (!isValidDirection(move)) { throw new InvalidDirectionException(critter1.getClass().getName() + " should not return " + move + " from getMove method"); } movePoint(location, move); Critter critter2 = grid[location.x][location.y]; Critter winner = critter1; String winnerToString = critter1ToString; if (critter2 != null) { // square is already occupied; fight! String critter2ToString = display[location.x][location.y]; winner = fight(critter1, critter2); Critter loser = (winner == critter1) ? critter2 : critter1; locationMap.remove(loser); int indexToRemove; if (winner == critter1) { indexToRemove = critterList.indexOf(critter2); winnerToString = critter1ToString; } else { indexToRemove = i; winnerToString = critter2ToString; } critterList.remove(indexToRemove); if (indexToRemove <= i) { i--; // so we won't skip a critter by mistake } // TODO: update the grid and display fields if necessary // put null color, "." on location of loser // problem: if winner is still there, should put his color/toString, // but then we'll get them again when we call updateDisplay... // should only call them once per update // decrement various counters for each critter type classStateMap.get(winner.getClass().getName()).kills++; classStateMap.get(loser.getClass().getName()).count--; } grid[location.x][location.y] = winner; display[location.x][location.y] = winnerToString; } updateDisplay(Event.UPDATE, true); } // Adds a single instance of the given type to this model. // If the critter's constructor needs any parameters, gives random values. private void add(Class critterClass) throws IllegalAccessException, InvocationTargetException, InstantiationException { if (getTotalCritterCount() >= width * height) { throw new TooManyCrittersException(); } // create critter Constructor ctor = getConstructor(critterClass); Object[] params = createRandomParameters(critterClass, ctor); // Critter critter = ctor.newInstance(params); Object obj = ctor.newInstance(params); Critter critter; if (obj instanceof Critter) { critter = (Critter) obj; } else { throw new InvalidCritterClassException("Object doesn't appear to implement the Critter interface correctly."); } critterList.add(critter); // place critter on board Point location = randomOpenLocation(); locationMap.put(critter, location); grid[location.x][location.y] = critter; // count # of critters of each class String className = critter.getClass().getName(); CritterClassState state = classStateMap.get(className); state.count++; state.initialCount++; } // Conducts a fight between the given two critters. // Returns which critter won the game. The other must die! private Critter fight(Critter critter1, Critter critter2) { int weapon1 = critter1.fight(critter2.toString()); verifyWeapon(critter1.getClass(), weapon1); int weapon2 = critter2.fight(critter1.toString()); verifyWeapon(critter2.getClass(), weapon2); if (weapon1 == Critter.ROAR && weapon2 == Critter.SCRATCH || weapon1 == Critter.SCRATCH && weapon2 == Critter.POUNCE || weapon1 == Critter.POUNCE && weapon2 == Critter.ROAR) { // playor 1 wins return critter1; } else if (weapon1 == weapon2) { // tie return Math.random() < 0.5 ? critter1 : critter2; } else { // player 2 wins return critter2; } } // Fills and returns an array of random values of the proper types // for the given constructor. private Object[] createRandomParameters(Class critterClass, Constructor ctor) { Class[] paramTypes = ctor.getParameterTypes(); Object[] params = new Object[paramTypes.length]; // build random parameters for (int j = 0; j < params.length; j++) { if (paramTypes[j] == Integer.TYPE) { params[j] = new Integer(rand.nextInt(INT_PARAM_MAX) + 1); } else if (paramTypes[j] == Color.class) { params[j] = randomColor(); } else { throw new InvalidCritterClassException("when constructing " + critterClass + ":\nbad constructor parameter type: " + paramTypes[j]); } } return params; } // Gets and returns the constructor for the given class by reflection. @SuppressWarnings("unchecked") private Constructor getConstructor(Class critterClass) { // TODO: change to getConstructor() (no warning) Constructor[] ctors = (Constructor[]) critterClass.getConstructors(); if (ctors.length != 1) { throw new InvalidCritterClassException("wrong number of constructors (" + ctors.length + ") for " + critterClass + "; must have only one constructor"); } return ctors[0]; } // Returns whether the given int represents a valid // movement direction such as NORTH or CENTER. private boolean isValidDirection(int dir) { return dir == Critter.NORTH || dir == Critter.SOUTH || dir == Critter.EAST || dir == Critter.WEST || dir == Critter.CENTER; } // Translates a point's coordinates 1 unit in a particular direction. private Point movePoint(Point p, int direction) { if (direction == Critter.NORTH) { p.y = (p.y + height - 1) % height; } else if (direction == Critter.SOUTH) { p.y = (p.y + 1) % height; } else if (direction == Critter.EAST) { p.x = (p.x + 1) % width; } else if (direction == Critter.WEST) { p.x = (p.x + width - 1) % width; } // else direction == Critter.CENTER return p; } // Returns a random color. private Color randomColor() { // return new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256)); double r = Math.random(); if (r < 0.333) { return Color.WHITE; } else if (r < 0.667) { return Color.BLACK; } else { return BROWN; } } // Returns a random point that is unoccupied by any critters. private Point randomOpenLocation() { Point p = new Point(); do { p.x = rand.nextInt(width); p.y = rand.nextInt(height); } while (grid[p.x][p.y] != null); return p; } // Updates the internal string array representing the text to display. // Also notifies observers of a new event. // Doesn't throw exceptions if colors or toStrings are null. private void updateDisplay(Event event) { updateDisplay(event, false); } // Updates the internal string array representing the text to display. // Also notifies observers of a new event. // Possibly throws exceptions if colors or toStrings are null. private void updateDisplay(Event event, boolean throwOnNull) { for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if (grid[i][j] == null) { display[i][j] = EMPTY; colorDisplay[i][j] = Color.BLACK; } else { display[i][j] = grid[i][j].toString(); if (throwOnNull && display[i][j] == null) { throw new IllegalArgumentException( grid[i][j].getClass().getName() + " returned a null toString result."); } colorDisplay[i][j] = grid[i][j].getColor(); if (throwOnNull && colorDisplay[i][j] == null) { throw new IllegalArgumentException( grid[i][j].getClass().getName() + " returned a null getColor result."); } } } } setChanged(); notifyObservers(event); } // Throws an exception if the given fight choice is not valid. private void verifyWeapon(Class critterClass, int weapon) { if (weapon != Critter.ROAR && weapon != Critter.POUNCE && weapon != Critter.SCRATCH) { throw new IllegalArgumentException(critterClass.getName() + " returned invalid weapon for fight; " + "must be one of ROAR, POUNCE, or SCRATCH: " + weapon); } } // Inner class to represent the state of a given critter class. public static class CritterClassState { public Class critterClass; private int count; private int initialCount; private int kills; // Constructs object to represent state of the given class. public CritterClassState(Class critterClass) { this.critterClass = critterClass; } // Returns how many animals are alive. public int getCount() { return count; } // Returns how many animals of this type have ever been created. public int getInitialCount() { return initialCount; } // Returns how many animals this type has killed. public int getKills() { return kills; } // Resets the state of this type. public void reset() { count = 0; initialCount = 0; kills = 0; } // For debugging. public String toString() { return critterClass.getName() + ":count=" + count + ",kills=" + kills; } } // Used to query a critter's state (position, neighbors, etc). // See CritterInfo.java for documentation of methods. private class CritterInfoImpl implements CritterInfo { private Point location; public CritterInfoImpl(Point location) { this.location = location; } public int getHeight() { return height; } public String getNeighbor(int direction) { if (!isValidDirection(direction)) { throw new InvalidDirectionException(direction); } neighborPoint.setLocation(location); movePoint(neighborPoint, direction); return display[neighborPoint.x][neighborPoint.y]; } public int getWidth() { return width; } public int getX() { return location.x; } public int getY() { return location.y; } // For debugging; dumps the state of this object as text. public String toString() { return "Info{location=(" + location.x + ", " + location.y + "),width=" + width + ",height=" + height + ",neighbors=[N=" + getNeighbor(Critter.NORTH) + ",S=" + getNeighbor(Critter.SOUTH) + ",W=" + getNeighbor(Critter.WEST) + ",E=" + getNeighbor(Critter.EAST) + ",C=" + getNeighbor(Critter.CENTER) + "]}"; } } // Used to signal various types to observers public enum Event { ADD, NEW, REMOVE_ALL, RESET, UPDATE } // An exception thrown when the model is unable to instantiate a class. public static class InvalidCritterClassException extends RuntimeException { private static final long serialVersionUID = 0; public InvalidCritterClassException(Exception e) { super(e); } public InvalidCritterClassException(String message) { super(message); } } // An exception thrown when a bad direction integer is passed. public static class InvalidDirectionException extends RuntimeException { private static final long serialVersionUID = 0; public InvalidDirectionException(int direction) { super(String.valueOf(direction)); } public InvalidDirectionException(String message) { super(message); } } // An exception thrown when the model becomes too full to fit more critters. public static class TooManyCrittersException extends RuntimeException { private static final long serialVersionUID = 0; } } // CSE 142 Homework 8 (Critter Safari) // Author: Marty Stepp // // Handles network responsibilities in the critter game. // // Critter-specific usage of the network manager object. class CritterNetworkManager extends NetworkManager { // class constants public static final int DEFAULT_PORT_1 = 5142; public static final int DEFAULT_PORT_2 = 5143; // Constructs a CritterNetworkManager at the default port. public CritterNetworkManager() { super(DEFAULT_PORT_1); } // Constructs a CritterNetworkManager at the given port. public CritterNetworkManager(int port) { super(port); } // Sends out a message to the given host requesting that they send // their class with the given name back to us. public void requestClass(String host, String className) { send(host, getHostName(), className); } // Sends out the text of the given class to the given host. public void sendText(String host, String className, String fileText) { send(host, getHostName(), className, fileText); } /* // Sends out the text of the given class to the given host. public void sendClass(String host, String className, byte[] bytes) { String encodedText = Base64.encodeToString(bytes); send(host, getHostName(), className, encodedText); } */ } // General reusable version of event-driven network manager object. class NetworkManager { // class constants private static final boolean DEBUG = false; private static String hostName; private static String ipAddress; private static String ipAddresses = ""; static { // use host name to name classes sent over the wire // also grab IP address(es) of local computer try { InetAddress localhost = InetAddress.getLocalHost(); hostName = localhost.getHostName(); ipAddress = localhost.getHostAddress(); for (InetAddress addr : InetAddress.getAllByName(hostName)) { ipAddresses += " " + addr.getHostAddress(); } ipAddresses = ipAddresses.trim(); } catch (UnknownHostException e) { hostName = "unknown_host"; ipAddress = ipAddresses = "127.0.0.1"; } } // Returns this computer's host name. public static String getHostName() { return hostName; } // Returns this computer's IP address. public static String getIpAddress() { return ipAddress; } // Returns this computer's IP address. public static String getIpAddresses() { return ipAddresses; } // fields private int port; private boolean shouldContinue; private List outQueue; // queue of messages to send private Thread sendThread, receiveThread; private Event receive; // events to represent messages received private Event error; // and errors that occur // Constructs a network manager at the given port. public NetworkManager(int port) { this.port = port; shouldContinue = true; outQueue = Collections.synchronizedList(new LinkedList()); receive = new Event(); error = new Event(); } // Returns this network manager's observable error event object, // so observers can be notified about errors and respond to them. public Event getErrorEvent() { return error; } // Returns this network manager's port. public int getPort() { return port; } // Returns this network manager's observable receive event object, // so observers can be notified when messages are received. public Event getReceiveEvent() { return receive; } // Sends a message to the given host containing the given strings. public void send(String host, String... strings) { synchronized (outQueue) { Message message = new Message(host, strings); outQueue.add(message); } } // Notifies this network manager that you want it to shut down // and stop listening for messages. public void stop() { shouldContinue = false; } // Starts listening for messages on the network. public void start() throws IOException { shouldContinue = true; if (sendThread == null) { sendThread = new Thread(new SendRunnable()); receiveThread = new Thread(new ReceiveRunnable()); } sendThread.start(); receiveThread.start(); } // A helper class to represent observable events. class Event extends Observable { // Convenience method to replace notifyObservers. public void fire() { notifyObservers(null); } // Convenience method to replace notifyObservers. public void fire(T arg) { setChanged(); super.notifyObservers(arg); } } // Listening thread that waits for messages to arrive. private class ReceiveRunnable implements Runnable { // fields private ServerSocket srvSock; public ReceiveRunnable() throws IOException { srvSock = new ServerSocket(port); } // Runs the listening thread. public void run() { try { while (shouldContinue) { // wait for a message to arrive Socket sock = srvSock.accept(); InputStream stream = sock.getInputStream(); ObjectInputStream ois = new ObjectInputStream(stream); // read message String[] strings = (String[]) ois.readObject(); if (DEBUG) System.out.println("Received on port " + port + ":\n" + Arrays.toString(strings)); // notify observers that message has arrived receive.fire(strings); } } catch (Exception e) { // notify observers that an error occurred error.fire(e); } } } // Sending thread that outputs messages from the output queue to the // network. private class SendRunnable implements Runnable { public void run() { while (shouldContinue) { // test-and-test-and-set paradigm if (!outQueue.isEmpty()) { synchronized (outQueue) { if (!outQueue.isEmpty()) { // grab message from the queue Message message = outQueue.remove(0); try { // send the message Socket sock = new Socket(message.host, port); OutputStream stream = sock.getOutputStream(); ObjectOutputStream oos = new ObjectOutputStream( stream); oos.writeObject(message.strings); if (DEBUG) System.out.println("Sent on port " + port + ":\n" + message); } catch (IOException e) { // notify observers that an error occurred error.fire(e); } } } } // wait 1 second between checks for messages so this thread // won't drain 100% of the CPU try { Thread.sleep(1000); } catch (InterruptedException e) { } } } } // Convenience classto represent messages about to be sent over the network. private static class Message implements Serializable { // class constants private static final long serialVersionUID = 0; // fields public String host; public String[] strings; // Constructs a message to the given host containing the given strings. public Message(String host, String[] strings) { this.host = host; this.strings = strings; } // Returns debug text about this message. public String toString() { return "Message{host=" + host + ", strings=" + Arrays.toString(strings) + "}"; } } } // CSE 142 Homework 8 (Critter Safari) // Authors: Marty Stepp, Stuart Reges // // A drawing surface that draws the state of all critters in the simulation. // class CritterPanel extends JPanel implements Observer { // class constants private static final long serialVersionUID = 0; private static final boolean ANTI_ALIAS = false; private static final int FONT_SIZE = 12; private static final Font FONT = new Font("Monospaced", Font.BOLD, FONT_SIZE + 4); // fields private CritterModel model; // Constucts a new panel to display the given model. public CritterPanel(CritterModel model) { this.model = model; model.addObserver(this); this.setFont(FONT); setBackground(Color.CYAN); setPreferredSize(new Dimension(FONT_SIZE * model.getWidth(), FONT_SIZE * (model.getHeight()) + FONT_SIZE/2)); } // Paints the critters on the panel. public void paintComponent(Graphics g) { super.paintComponent(g); // anti-aliasing if (ANTI_ALIAS) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } // because font is monospaced, all widths should be the same; // so we can get char width from any char (in this case x) // draw all critters for (int x = 0; x < model.getWidth(); x++) { for (int y = 0; y < model.getHeight(); y++) { int drawX = x * FONT_SIZE + 2; int drawY = (y + 1) * FONT_SIZE; Color color = model.getColor(x, y); drawShadowedString(g, model.getString(x, y), color, drawX, drawY); } } } // Responds to Observable updates to the model. public void update(Observable o, Object arg) { repaint(); } // Draws the given text with a dark shadow beneath it. private void drawShadowedString(Graphics g, String s, Color c, int x, int y) { g.setColor(Color.BLACK); if (s != null) { g.drawString(s, x + 1, y + 1); } if (c != null) { g.setColor(c); } if (s != null) { g.drawString(s, x, y); } } // Returns the RGB opposite of the given color. public Color getReverseColor(Color c) { return new Color(~c.getRGB()); } } // Provides a secure environment for the critters to play in. // Forbids them from reading files or network resources. class CritterSecurityManager extends SecurityManager { private boolean allow = false; public void setAllow(boolean allow) { this.allow = allow; } public void checkAccept(String host, int port) { // throw new SecurityException(host + " " + port); } public void checkConnect(String host, int port) { // throw new SecurityException(); } public void checkConnect(String host, int port, Object context) { // throw new SecurityException(); } public void checkDelete(String file) { // throw new SecurityException(); } public void checkExec(String cmd) { throw new SecurityException(); } public void checkLink(String lib) { // throw new SecurityException(); } public void checkListen(int port) { //if (port != CritterNetworkManager.DEFAULT_PORT_1 //&& port != CritterNetworkManager.DEFAULT_PORT_2) { // throw new SecurityException("Invalid port: " + port); //} } public void checkPropertiesAccess() { //throw new SecurityException(); } public void checkPropertyAccess(String property) { // throw new SecurityException(); } public void checkRead(FileDescriptor fd) { // throw new SecurityException(); } public void checkRead(String file) { // throw new SecurityException(); } public void checkRead(String file, Object context) { throw new SecurityException(); } public void checkWrite(FileDescriptor fd) { if (!allow) { throw new SecurityException(); } } public void checkWrite(String file) { if (file.endsWith(ClassUtils.JAVA_EXTENSION)) { throw new SecurityException(); } } } // CSE 142 Homework 8 (Critter Safari) // Author: Marty Stepp // // A bunch of methods used to dynamically load critter classes sent across // the web. Useful for running 1-on-1 critter tournaments. // class ClassUtils { // class constants public static final String CLASS_EXTENSION = ".class"; public static final String JAVA_EXTENSION = ".java"; private static final FileFilter CLASS_FILTER = new ExtensionFilter(CLASS_EXTENSION); // Adds 25 of each critter class type to the given model. // The only critter-specific code in here; a bit of a cheat // so that CritterMain.java doesn't have to have this icky code in it. public static void addAllCritterClasses(CritterModel model, int count) { for (Class critterClass : ClassUtils.getClasses( Critter.class, ".")) { model.add(count, critterClass); } } // Adds 25 of each Wolf class type to the given model. public static void addOtherWolfClasses(CritterModel model, int count) { for (Class critterClass : ClassUtils.getClasses( Critter.class, ".")) { if (isNetworkClass(critterClass.getName())) { model.add(count, critterClass); } } } // Returns whether the given name represents an inner class (has a $ sign). public static boolean isInnerClass(String className) { return className.indexOf('$') >= 0; } // Returns whether the given class is one that came from the network. // Excludes inner classes (ones with $ in their name). public static boolean isNetworkClass(String className) { return className.indexOf('_') >= 0 && !isInnerClass(className); } // DrJava makes nightmares for me ... public static boolean isDrJavasFault(String className) { return new File(className + ClassUtils.CLASS_EXTENSION).exists() && System.getProperties().toString().toLowerCase().indexOf("drjava") >= 0; } /* // Reads the .java file for the given "old" class, renames it to the "new" class name, // compiles the newly created .java file, reads the bytes of the .class file // just made, base64 encodes those bytes into a String, and returns that. Phew! public static String renameCompileEncode(String oldClassName, String newClassName) throws ClassNotFoundException, IOException { String fileText = readEntireFile(oldClassName + JAVA_EXTENSION); String newJavaFileName = renameAndWriteJavaFile(fileText, oldClassName, newClassName, false); String classFileName = compile(newJavaFileName); new File(newJavaFileName).delete(); byte[] bytes = readEntireFileBytes(classFileName); new File(classFileName).deleteOnExit(); String encodedFileText = Base64.encodeToString(bytes); return encodedFileText; } // An overall method that takes the given file text and dumps it // to the disk, renames it, recompiles it, loads the class into the JVM, // and returns the associated Class object. public static Class doEverythingText(String fileText, String oldClassName, String newClassName, boolean useTempFolder) throws ClassNotFoundException { newClassName = sanitizeClassName(newClassName); String newFileName = renameAndWriteJavaFile(fileText, oldClassName, newClassName, useTempFolder); String classFileName = compile(newFileName); new File(newFileName).delete(); if (useTempFolder) { new File(classFileName).deleteOnExit(); } return loadClass(classFileName); } */ public static Class writeAndLoadClass(String fileText, String className, boolean useTempFolder) throws IOException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { // write the modified text to a new file (possibly in temp dir) String javaFileName = className + JAVA_EXTENSION; if (useTempFolder) { javaFileName = System.getProperty("java.io.tmpdir") + javaFileName; } writeEntireFile(fileText, javaFileName); String classFileName = compile(javaFileName); new File(javaFileName).delete(); // move class to current directory new File(classFileName).renameTo(new File("./" + className + CLASS_EXTENSION)); return loadClass(classFileName); } /* public static Class writeAndLoadEncodedClass(String encodedClassText, String className) throws ClassNotFoundException, IOException { byte[] bytes = Base64.decodeToBytes(encodedClassText); String classFileName = className + CLASS_EXTENSION; writeBytes(bytes, classFileName); // newClassName = sanitizeClassName(newClassName); return loadClass(classFileName); } */ // Compiles the .java source file with the given file name, // and returns the file name of the newly compiled .class file. // Throws a RuntimeException if the compilation fails. // TODO: *** make this use JDK 1.6's new JavaCompiler interface public static String compile(String fileName) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { String folderName = getFolder(fileName); String sep = System.getProperty("path.separator"); String[] args = {"-classpath", "." + sep + folderName, fileName}; // int result = com.sun.tools.javac.Main.compile(args); Class compilerClass = Class.forName("com.sun.tools.javac.Main"); Method compileMethod = compilerClass.getMethod("compile", new String[0].getClass()); compileMethod.invoke(null, (Object) args); int result = 0; if (result != 0) { throw new RuntimeException("Compilation failed: error code " + result); } return removeExtension(fileName) + CLASS_EXTENSION; } // Downloads the file at the given URL and saves it to the local disk. // For example "www.foo.com/bar/baz/Mumble.txt" saves to "Mumble.txt". // Returns the file name of the saved local file. public static String downloadFile(URL url) throws IOException { String fileName = removeFolder(url.getFile()); if (!(new File(fileName).exists())) { OutputStream output = new PrintStream(fileName); InputStream input = new BufferedInputStream(url.openStream()); byte[] buffer = new byte[512000]; int numRead; long numWritten = 0; while ((numRead = input.read(buffer)) != -1) { output.write(buffer, 0, numRead); numWritten += numRead; } output.close(); } return fileName; } // Loads all classes that extend the given class from the given folder. @SuppressWarnings("unchecked") public static List> getClasses(Class superClass, String folderName) { List> list = new ArrayList>(); File folder = new File(folderName); if (!folder.exists() || !folder.canRead()) { // throw new IOException("Can't read folder: " + folderName); return list; } for (File file : folder.listFiles(CLASS_FILTER)) { String fileName = file.getName(); if (file.canRead() && !file.isDirectory() && fileName.endsWith(CLASS_EXTENSION)) { try { Class existingClass = Class.forName(removeExtension(fileName)); if (!existingClass.isInterface() && !Modifier.isAbstract(existingClass.getModifiers()) && superClass.isAssignableFrom(existingClass)) { // then this is a concrete class that implements the interface list.add((Class) existingClass); } } catch (Exception e) { System.out.println("error reading " + fileName + ":"); e.printStackTrace(); } } } return list; } // Downloads the contents of the .zip file at the given URL, and // returns them as a map from filenames to the bytes of each file. public static Map getZipFileContents(URL url) throws IOException { String fileName = downloadFile(url); ZipFile zip = new ZipFile(fileName); Map zipFilesMap = new TreeMap(); // read each file entry from the zip archive for (Enumeration enu = zip.entries(); enu.hasMoreElements(); ) { ZipEntry ze = enu.nextElement(); if (ze.isDirectory()) { continue; } int size = (int) ze.getSize(); if (size < 0) { continue; // -1 means unknown size. } InputStream input = zip.getInputStream(ze); byte[] b = new byte[size]; int offset = 0; while (size - offset > 0) { int bytesRead = input.read(b, offset, size - offset); if (bytesRead < 0) { break; } offset += bytesRead; } zipFilesMap.put(ze.getName(), b); } return zipFilesMap; } // Returns whether the given class implements the given interface. public static boolean classImplements(Class clazz, Class interfaceType) { for (Class c : clazz.getInterfaces()) { if (c == interfaceType) { return true; } } return false; } // Dynamically loads the compiled .class file with the given file name // into our JVM and returns its Class object. // Throws various reflectiony exceptions if the file is bad. public static Class loadClass(String fileName) throws ClassNotFoundException { String folderName = getFolder(fileName); File folder = new File(folderName); ClassLoader loader = ClassLoader.getSystemClassLoader(); ClassLoader urlLoader = loader; try { URL fileUrl = new URL("file:" + System.getProperty("user.dir") + File.separator + fileName); File currentDir = new File(System.getProperty("user.dir")); urlLoader = URLClassLoader.newInstance(new URL[] { folder.toURI().toURL(), currentDir.toURI().toURL(), fileUrl}, loader); } catch (MalformedURLException mfurle) { mfurle.printStackTrace(); // this will never happen } String className = removeExtension(removeFolder(fileName)); Class clazz = urlLoader.loadClass(className); return clazz; } // Reads the given file's text fully and returns it as a String. public static String readEntireFile(String fileName) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(fileName)); StringBuilder text = new StringBuilder(); while (reader.ready()) { text.append((char) reader.read()); } return text.toString(); } public static byte[] readEntireFileBytes(String fileName) throws IOException { File file = new File(fileName); ByteArrayOutputStream out = new ByteArrayOutputStream((int) file.length()); InputStream stream = new FileInputStream(fileName); BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); while (reader.ready()) { out.write(reader.read()); } return out.toByteArray(); } public static String readAndRename(String oldClassName, String newClassName) throws IOException { String fileName = oldClassName + JAVA_EXTENSION; String fileText = readEntireFile(fileName); // replace the class name in the code fileText = fileText.replaceAll(oldClassName, newClassName); return fileText; } // Treats fileText as the text of a Java source file, and // replaces occurrences of its class name with the given new class name, // then writes it to a new .java file with that name. // Returns the new file name. public static String renameAndWriteJavaFile(String fileText, String oldClassName, String newClassName, boolean useTempFolder) { // replace the class name in the code fileText = fileText.replaceAll(oldClassName, newClassName); // write the modified text to a new file String newFileName = newClassName + JAVA_EXTENSION; if (useTempFolder) { newFileName = System.getProperty("java.io.tmpdir") + newFileName; } writeEntireFile(fileText, newFileName); return newFileName; } public static void writeEntireFile(String text, String fileName) { try { PrintStream output = new PrintStream(fileName); output.print(text); output.close(); } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); // this will never happen; we're writing the file! } } // Removes any characters from given text that wouldn't be acceptable // in a Java class name. // Not perfect (e.g., doesn't prevent names that start with a number). public static String sanitizeClassName(String text) { text = text.replaceAll("[^A-Za-z0-9_$]+", "_"); return text; } public static void writeBytes(byte[] bytes, String fileName) throws IOException { FileOutputStream output = new FileOutputStream(fileName); output.write(bytes); output.close(); } // pre: no folders in fileName (no "foo/bar/Baz.java") private static String getFolder(String fileName) { int slash = fileName.lastIndexOf(File.separatorChar); if (slash < 0) { slash = fileName.lastIndexOf("/"); // fallback } if (slash >= 0) { return fileName.substring(0, slash + 1); } else { return "./"; } } // pre: no folders in fileName (no "foo/bar/Baz.java") private static String removeExtension(String fileName) { int dot = fileName.lastIndexOf("."); if (dot >= 0) { fileName = fileName.substring(0, dot); } return fileName; } // pre: no folders in fileName (no "foo/bar/Baz.java") private static String removeFolder(String fileName) { int slash = fileName.lastIndexOf(File.separatorChar); if (slash < 0) { slash = fileName.lastIndexOf("/"); // fallback } if (slash >= 0) { fileName = fileName.substring(slash + 1); } return fileName; } // inner class to filter files by extension public static class ExtensionFilter implements FileFilter { private String extension; public ExtensionFilter(String extension) { this.extension = extension; } public boolean accept(File f) { return f != null && f.exists() && f.canRead() && f.getName().endsWith(extension); } } }