/////////////////////////////////////////////////////////////////////// // Java Utilities // // David Bailey 1/97 // // File: Utilities.java // // Description: A random collection of Java utility classes. // // Classes: StringUtil, Constrain, Picture, // NestedStreamTokenizer, DoubleEnumeration, // DataSource, ScrollingTextArea, TextFieldDialog, // TextEditDialog, Fmt, PriorityQueue import java.awt.*; import java.awt.image.*; import java.io.*; import java.net.*; import java.util.*; /////////////////////////////////////////////////////////////////////// // Stuff that ought to be in the String class... class StringUtil { // An equals routine which can handle either argument being null public static boolean equals(String a, String b) { return ((a == null && b == null) || a != null && b != null && a.equals(b)); } } /////////////////////////////////////////////////////////////////////// // Stream with a call to atomically read strings which may contain // newlines (hence can't use DataInputStream.readLine()). Producer // of strings must use AtomicStringOutputStream. // OBSOLETED BY OBJECTOUTOUTSTREAM IN 1.1 // class AtomicStringInputStream extends DataInputStream { // AtomicStringInputStream(InputStream i) { super(i); } // public String readAtomicString() throws IOException { // int len = readInt(); // byte[] buf = new byte[len]; // readFully(buf); // return new String(buf, 0); // } // } /////////////////////////////////////////////////////////////////////// // Stream with a call to atomically send strings which may contain // newlines (hence can't use DataInputStream.readLine() on receiving // end). Consumer of strings must use AtomicStringInputStream. // OBSOLETED BY OBJECTOUTOUTSTREAM IN 1.1 // class AtomicStringOutputStream extends DataOutputStream { // AtomicStringOutputStream(OutputStream i) { super(i); } // public void writeAtomicString(String s) throws IOException { // writeInt(s.length()); // writeBytes(s); // } // } /////////////////////////////////////////////////////////////////////// // Some static methods for adding components to a Container which // uses GridBagLayout. // Adapted from 'Java in a Nutshell' class AllComponents. class Constrain { public static void add(Container container, Component component, int grid_x, int grid_y, int grid_width, int grid_height, int fill, int anchor, double weight_x, double weight_y, int top, int left, int bottom, int right) { GridBagConstraints c = new GridBagConstraints(); c.gridx = grid_x; c.gridy = grid_y; c.gridwidth = grid_width; c.gridheight = grid_height; c.fill = fill; c.anchor = anchor; c.weightx = weight_x; c.weighty = weight_y; if (top + bottom + left + right > 0) c.insets = new Insets(top, left, bottom, right); ((GridBagLayout)container.getLayout()).setConstraints(component, c); container.add(component); } static public void add(Container container, Component component, int grid_x, int grid_y, int grid_width, int grid_height, int fill, int anchor, int top, int left, int bottom, int right) { add(container, component, grid_x, grid_y, grid_width, grid_height, fill, anchor, 0.0, 0.0, top, left, bottom, right); } static public void add(Container container, Component component, int grid_x, int grid_y, int grid_width, int grid_height, int top, int left, int bottom, int right) { add(container, component, grid_x, grid_y, grid_width, grid_height, GridBagConstraints.NONE, GridBagConstraints.CENTER, 0.0, 0.0, top, left, bottom, right); } } /////////////////////////////////////////////////////////////////////// // A panel containing an image. Image must be fully loaded before // creating Picture. class Picture extends Panel { private Image image; Picture(Image image) { this.image = image; } public void paint(Graphics g) { g.drawImage(image, 0, 0, this); } public Dimension getMinimumSize() { return new Dimension(image.getWidth(this), image.getHeight(this)); } public Dimension getPreferredSize() { return getMinimumSize(); } } /////////////////////////////////////////////////////////////////////// // NestedStreamTokenizer is like StreamTokenizer but adds a token type // which is a possibly-multi-line sub-string delimited by braces. // When such a token is found (TT_NESTED indicates this), the sval // field contains the nested string, sans braces. // This class also ignores Java-style // comments. class NestedStreamTokenizer extends StreamTokenizer { private static final char OPEN_BRACE_CHAR = '{'; private static final char CLOSE_BRACE_CHAR = '}'; public static final int TT_NESTED = -4; NestedStreamTokenizer(Reader r) { super(r); setUpSyntax(); } public int nextToken() throws IOException { int res = super.nextToken(); if (res == OPEN_BRACE_CHAR) { // Hack: must disable parsing of numbers since re-converting // them to strings might introduce exponential notation which // Java cannot read back in... resetSyntax(); StringBuffer sub = new StringBuffer(); int nestDepth = 1; while (nestDepth > 0) { res = super.nextToken(); if (res == OPEN_BRACE_CHAR) { nestDepth++; } else if (res == CLOSE_BRACE_CHAR) { nestDepth--; } else if (res == TT_EOF) { throw new IOException("unexpected eof in nested stream"); } if (nestDepth > 0) { if (res == TT_WORD || res == TT_NUMBER) { throw(new IOException("shouldn't be recognizing words or " + "numbers in a nested block")); } else { sub.append((char)res); } } } sval = sub.toString(); // Anti-Hack: Re-enable parsing of numbers, words, etc... setUpSyntax(); res = TT_NESTED; } else if (res == TT_WORD) { // attempt to convert to number // (this is a workaround because Java can't read exponential notation) try { nval = Fmt.getDbl(sval); res = TT_NUMBER; } catch (NumberFormatException e) { // leave it as a TT_WORD } } return res; } private void setUpSyntax() { resetSyntax(); slashSlashComments(true); // parseNumbers(); whitespaceChars((char)' ', (char)' '); whitespaceChars((char)'\t', (char)'\t'); whitespaceChars((char)'\n', (char)'\n'); eolIsSignificant(false); wordChars((char)'a', (char)'z'); wordChars((char)'A', (char)'Z'); wordChars((char)'0', (char)'9'); wordChars((char)'-', (char)'-'); wordChars((char)'_', (char)'_'); wordChars((char)'.', (char)'.'); // for numbers... } } /////////////////////////////////////////////////////////////////////// // DoubleEnumeration basically composes two Enumerations into one. class DoubleEnumeration implements Enumeration { private boolean aIsDone; private Enumeration a, b; DoubleEnumeration(Enumeration a, Enumeration b) { this.a = a; this.b = b; aIsDone = !a.hasMoreElements(); } public boolean hasMoreElements() { if (!aIsDone) { return a.hasMoreElements(); } else { return b.hasMoreElements(); } } public Object nextElement() { if (!aIsDone) { Object o = a.nextElement(); aIsDone = !a.hasMoreElements(); return o; } else { return b.nextElement(); } } } /////////////////////////////////////////////////////////////////////// // Interface to file contents which goes through either the file system // or through the web depending on how it's set up. class DataSource { private static boolean useWeb; private static String base; private static final String NOT_FOUND_MESSAGE = "404 Not Found"; public static void setUseWeb(String s) { useWeb = true; base = s; } public static void setUseFiles(String s) { useWeb = false; base = s; } // Returns array of entries in directory 's', excluding entries // ending with a '~'. public static String[] getDirectoryList(String s) throws MalformedURLException, IOException { if (useWeb) { // This is gross but should work... Vector entries = new Vector(); // Vector StringBuffer text = new StringBuffer(); InputStream is = new URL(base + s).openStream(); int ch = is.read(); while (ch != -1) { text.append((char)ch); ch = is.read(); } StringTokenizer tokEnum = new StringTokenizer(text.toString()); while (tokEnum.hasMoreTokens()) { String tok = tokEnum.nextToken(); if (tok.startsWith("NAME=\"") && !tok.endsWith("~\"")) { String e = tok.substring(6, tok.length() - 1); if (e.charAt(e.length() - 1) == '/') { e = e.substring(0, e.length() - 1); } entries.addElement(e); } } String[] res = new String[entries.size()]; for (int i = 0; i < entries.size(); i++) { res[i] = (String)entries.elementAt(i); } return res; } else { File f = new File(base + s); if (f.isDirectory()) { String[] fullList = f.list(); Vector v = new Vector(); for (int i = 0; i < fullList.length; i++) { if (!fullList[i].endsWith("~")) { v.addElement(fullList[i]); } } String[] res = new String[v.size()]; for (int i = 0; i < v.size(); i++) { res[i] = (String)v.elementAt(i); } return res; } else { // SHOULD THROW EXCEPTION? return null; } } } public static InputStream getInputStream(String s) throws MalformedURLException, IOException, FileNotFoundException { if (useWeb) { return new URL(base + s).openStream(); } else { return new FileInputStream(base + s); } } public static NestedStreamTokenizer getNestedStreamTokenizer(String s) throws MalformedURLException, IOException, FileNotFoundException { if (useWeb) { return new NestedStreamTokenizer( new StringReader(((String)new URL(base + s).getContent()))); } else { return new NestedStreamTokenizer(new FileReader(base + s)); } } // The Component ought to be the containing one, I suppose - but not sure. public static Image getLoadedImage(String s, Component c) throws MalformedURLException, InterruptedException { Image i; if (useWeb) { i = Toolkit.getDefaultToolkit().getImage(new URL(base + s)); } else { i = Toolkit.getDefaultToolkit().getImage(base + s); } MediaTracker t = new MediaTracker(c); t.addImage(i, 0); t.waitForAll(); return i; } public static boolean exists(String s) { if (useWeb) { // How else, but to try to load it and then see if the stream // contains "404 Not Found" ??? boolean exists; try { InputStream is = new URL(base + s).openStream(); StringBuffer text = new StringBuffer(); int ch = is.read(); while (ch != -1) { text.append((char)ch); ch = is.read(); } exists = (text.toString().indexOf(NOT_FOUND_MESSAGE) < 0); } catch (Exception e) { exists = false; } return exists; } else { return new File(base + s).exists(); } } } /////////////////////////////////////////////////////////////////////// // A TextArea which automatically scrolls to the bottom whenever // text is appended. class ScrollingTextArea extends TextArea { // // This is to prevent constant creation of new strings every time // // text is appended to the window. // private static final int INITIAL_CAPACITY = 250000; // of buf // private StringBuffer buf; // ScrollingTextArea(String s, int w, int h) { // super(w, h); // buf = new StringBuffer(INITIAL_CAPACITY); // if (s != null) setText(s); // } // public void append(String s) { // // Alas, this append probably triggers a string copy of the current // // text string: // buf.append(s); // super.setText(buf.toString()); // problem: flickering to top of buf // int len = buf.length(); // select(len, len); // } // public void setText(String s) { // buf.setLength(0); // buf.append(s); // super.setText(buf.toString()); // int len = buf.length(); // select(len, len); // } // ORIGINAL STUFF BEFORE BUFFERING TRICKS: ScrollingTextArea(String s, int w, int h) { super(w, h); if (s != null) setText(s); } public void append(String s) { super.append(s); int len = getSelectionStart() + s.length(); select(len, len); } public void setText(String s) { super.setText(s); int len = s.length(); select(len, len); } } /////////////////////////////////////////////////////////////////////// // For modal entering of a line of text. class TextFieldDialog extends Dialog { private TextField tf; private Button ok; private boolean isDone; TextFieldDialog(Frame parent, String title, String prompt) { super(parent, title, true); setLayout(new GridBagLayout()); Constrain.add(this, new Label(prompt), 0, 0, 1, 1, 10, 10, 10, 10); tf = new TextField(30); tf.setEditable(true); Constrain.add(this, tf, 0, 1, 1, 1, 10, 10, 10, 10); ok = new Button("OK"); Constrain.add(this, ok, 0, 2, 1, 1, 10, 10, 10, 10); pack(); } public void setText(String s) { tf.setText(s); } public boolean action(Event e, Object arg) { if (e.target == ok) { finishUp(); } return true; } public boolean handleEvent(Event e) { // Handle return key if (e.target == tf && e.id == Event.KEY_PRESS && e.key == 10 || e.id == Event.WINDOW_DESTROY) { finishUp(); return true; } return super.handleEvent(e); } private void finishUp() { isDone = true; setVisible(false); dispose(); } public String getText() { return tf.getText(); } } /////////////////////////////////////////////////////////////////////// // For modal editing of a string. Dialog is closed by selecting // one of three buttons indicated to cancel the edits, apply the edits, // or apply and save to disk the edits. The caller is responsible for // carrying out these behaviors. class TextEditDialog extends Dialog { private boolean editable; private TextArea textArea; private Panel south; private Button cancelButton, applyButton, saveButton; private boolean apply, save; private boolean isDone; TextEditDialog(Frame parent, String title, String text, boolean editable) { super(parent, title, true); this.editable = editable; setLayout(new GridBagLayout()); textArea = new TextArea(text, 25, 80); Constrain.add(this, textArea, 0, 0, 1, 1, 10, 10, 10, 10); south = new Panel(); south.setLayout(new BorderLayout(60, 10)); cancelButton = new Button("Cancel"); if (editable) { south.add("West", cancelButton); applyButton = new Button("Apply"); south.add("Center", applyButton); saveButton = new Button("Apply & Save As..."); south.add("East", saveButton); } else { textArea.setEditable(false); south.add("Center", cancelButton); } Constrain.add(this, south, 0, 1, 1, 1, 10, 10, 10, 10); pack(); } public boolean action(Event e, Object arg) { if (e.target == cancelButton) { finishUp(); } if (e.target == applyButton) { apply = true; finishUp(); } if (e.target == saveButton) { apply = true; save = true; finishUp(); } return true; } private void finishUp() { isDone = true; setVisible(false); dispose(); } public boolean apply() { return apply; } public boolean save() { return save; } public String getText() { return textArea.getText(); } } /////////////////////////////////////////////////////////////////////// // Try to implement adjustable significant-digits for printing doubles. class Fmt { private static int sigDigits = 3; public static void setSigDigits(int sd) { if (sd >= 1) { sigDigits = sd; } else { System.err.println("Warning: Fmt cannot set sigDigits to " + sd); } } public static int getSigDigits() { return sigDigits; } // Return a string version of 'd' with at most sigDigits digits // displayed. Handles both exponential and conventional notation. // Assumes exponentials returned by double.toString() are in normal // form (one non-zero digit left of decimal). public static String dbl(double d) { String s = new Double(d).toString(); if (s.equals("NaN") || s.equals("Infinity") || s.equals("-Infinity")) { return s; } s = s.toLowerCase(); if (s.indexOf('e') >= 0) { boolean neg = (s.charAt(0) == '-'); String negStr = (neg ? "-" : ""); int wholeIndex = (neg ? 1 : 0); int expIndex = s.indexOf('e'); String wholeStr = s.substring(wholeIndex, wholeIndex + 1); String fracStr = ""; if (sigDigits > 1) { fracStr = s.substring(wholeIndex + 1, Math.min(expIndex, wholeIndex + sigDigits + 1)); } String expStr = s.substring(expIndex); return negStr + wholeStr + fracStr + expStr; } else { boolean neg = (s.charAt(0) == '-'); String negStr = (neg ? "-" : ""); int wholeIndex = (neg ? 1 : 0); int decIndex = s.indexOf('.'); if (decIndex < 0) { decIndex = s.length(); s = s + ".0"; } StringBuffer wholeStrB = new StringBuffer(s.substring(wholeIndex, decIndex)); StringBuffer fracStrB = new StringBuffer(s.substring(decIndex)); for (int i = sigDigits; i < (decIndex - wholeIndex); i++) { wholeStrB.setCharAt(i, '0'); } int remainDigits; // Look out for cases like "0.123": if (wholeStrB.charAt(0) == '0') { remainDigits = sigDigits; } else { remainDigits = Math.max(0, sigDigits - (decIndex - wholeIndex)); } int charsToKeep = ((remainDigits > 0) ? (remainDigits + 1) : 0); fracStrB.setLength(Math.min(charsToKeep, s.length() - decIndex)); return negStr + wholeStrB + fracStrB; } } // Read a double, even if it's in exponential notation. public static double getDbl(String s) { s = s.toLowerCase(); int idx = s.indexOf('e'); if (idx >= 0) { double mantissa = Double.valueOf(s.substring(0, idx)).doubleValue(); int exponent = Integer.parseInt(s.substring(idx + 1)); return mantissa * Math.pow(10, (double)exponent); } else { return Double.valueOf(s).doubleValue(); } } } /////////////////////////////////////////////////////////////////////// // Priority Queue. HIGHEST priorities returned first. // (Based on Lewis & Denenberg 1991, p. 303.) class PriorityQueue implements Serializable { private Hashtable priorities; // maps Objects to Doubles private Vector heap; // array of Objects arranged as a partially-ordered // tree by using [2i+1] and [2i+2] as children of [i] PriorityQueue() { priorities = new Hashtable(); heap = new Vector(); } public void insert(Object o, double p) { priorities.put(o, new Double(p)); heap.setSize(heap.size() + 1); int i = heap.size() - 1; while (i > 0 && ((Double)priorities.get(heap.elementAt((i-1)/2))).doubleValue() < p) { heap.setElementAt(heap.elementAt((i-1)/2), i); i = (i-1)/2; } heap.setElementAt(o, i); } public Object peek() { if (heap.size() == 0) return null; return heap.elementAt(0); } public Object remove() { if (heap.size() == 0) return null; Object res = heap.elementAt(0); priorities.remove(res); if (heap.size() == 1) { heap.setSize(0); return res; } double lastObjPri = ((Double)priorities.get(heap.elementAt(heap.size() - 1))).doubleValue(); int i = 0; while (2*i+1 < heap.size() && ((Double)priorities.get(heap.elementAt(2*i+1))).doubleValue() > lastObjPri || 2*i+2 < heap.size() && ((Double)priorities.get(heap.elementAt(2*i+2))).doubleValue() > lastObjPri) { int j; if (2*i+2 < heap.size()) { if (((Double)priorities.get(heap.elementAt(2*i+1))).doubleValue() > ((Double)priorities.get(heap.elementAt(2*i+2))).doubleValue()) { j = 2*i+1; } else { j = 2*i+2; } } else { j = heap.size() - 1; } heap.setElementAt(heap.elementAt(j), i); i = j; } heap.setElementAt(heap.elementAt(heap.size() - 1), i); heap.setSize(heap.size() - 1); return res; } public boolean isEmpty() { return heap.size() == 0; } // public Object peek(int rank) { // if (heap.size() < rank) return null; // } // public Object remove(int rank) { // if (heap.size() < rank) return null; // } // public boolean hasAtLeast(int num) { // return heap.size() >= num; // } }