/********************************************************************** * * Phind.java -- the Phind java applet * * Copyright 1997-2000 Gordon W. Paynter * Copyright 2000 The New Zealand Digital Library Project * * A component of the Greenstone digital library software * from the New Zealand Digital Library Project at the * University of Waikato, New Zealand. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * *********************************************************************/ /********************************************************************* To use the applet, you'll need to embed it in a web page like this: The Phind java applet. There are a bunch of other parameters; these are described in the getParameters method below. It is all done for you in Greenstone in the document.dm macro file (the _phindapplet_ macro). You may have problems with Java applet security. Java applet's can only open socket connections (including the HTTP connections the applet uses to get data) to the same server the applet was loaded from. This means that your phindcgi, library, and (optional) backdrop URLs must be on the same machine that your web page was loaded from. ********************************************************************** The applet comprises several classes: 1. Phind (this file) is the applet class, loaded by the browser. It also handles network connections. 2. ResultDisplay is a Panel that sits in the applet and displays things; displays are connected in a doubly-linked list. 3. ResultBox holds the results of a query. Result boxes are shown to the user through ResultDisplays. Another doubly-linked list. 4. ResultTitle acts as a caption to a ResultBox describing its contents. 5. ResultCanvas is what the ResultBox data is drawn on. It draws the words on the screen, and handles user input from the mouse. 6. ResultItem represents a single result object (a phrase or document). 7. PhindTitle is for drawing backdrops in ResultDisplays. **********************************************************************/ package org.nzdl.gsdl.Phind; import java.awt.BorderLayout; import java.awt.Button; import java.awt.Choice; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Event; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridLayout; import java.awt.Image; import java.awt.Label; import java.awt.Panel; import java.awt.TextField; import java.net.URL; import java.net.URLConnection; import java.io.DataInputStream; import java.io.InputStream; import java.net.Socket; //import java.net.SocketTimeoutException; import java.net.InetAddress; import java.net.UnknownHostException; import java.io.IOException; import java.io.InterruptedIOException; import java.util.Vector; import java.util.Date; import org.nzdl.gsdl.Phind.URLUTF8Encoder; public class Phind extends java.applet.Applet { // What is the collection called? public String collection; // Which phind classifier are we using? (There may be more than one.) public String classifier; // Internet address of phind resources public String library_address, phindcgi_address; // Initial serach term public String initialSearch; // Number of phrases to retrieve at any one time public int phraseBlockSize; // Appearance parameters public boolean vertical; public int depth; public boolean righttoleft; // Font public int fontSize; public String fontName; public Font plainFont, boldFont; // Do we want a background image in the applet? public boolean showImage; public String backdrop_address; public Image backgroundImage; public boolean showBorder; // Colours public Color panel_fg, panel_bg, column_1_fg, column_1_bg, column_2_fg, column_2_bg, highlight_fg, highlight_bg, thesaurus_fg, thesaurus_bg, thesaurus_bar_fg, thesaurus_bar_bg, expansion_fg, expansion_bg, expansion_bar_fg, expansion_bar_bg, document_fg, document_bg, document_bar_fg, document_bar_bg, message_fg, message_bg; // Column dimensions int column_1_width, column_2_width; // Where do we open new windows String searchWindowName, documentWindowName; // the mode of operation int mode; final int initMode = 0; final int idleMode = 1; final int searchMode = 2; // Elements of the control panel boolean showControlPanel; Label titleLabel; TextField wordField; Button searchButton, prevButton, nextButton; // Holders for the downloaded data Panel resultPanel; ResultDisplay firstDisplay, lastDisplay; // The time at which the last query finished Date lastQueryEndTime; // lastQueryEndTime is stored to ensure a 1 second gap between a query // returning and a new one beginning. It is needed because the FAO // folks in Rome have a huge lag, and frquently click several times // while they wait; these clicks are turned into new queries, which // they await again. It is no elegant solution, but it seems like the // easiest, given that I don't know threads. // 1. The search button is easy to disable, and is disabled when a // socket connection is in progress. // 2. ResutCanvas widgets can'r be similarly disabled because the // browser hides or wipes them, which looks very bad. // 3. I cannot just ignore clicks on canvasses because the browser // caches the clicks while the socket connection is going on, and then // sends them through afterwards, when the canvas is accepting clicks // again. // 4. Current sequence of events is to record the time the last query // ends, then whenever a click happens make sure a second has past. if // you double-click the the first query is sent, returns, end-tie is // set, and the second (and any others made during query time) is // *immediately* processed, but since 1 second hasn't past it is // ignored. /** We're adding a timeout to the loop used to read the result from the * server - partially because of the 'fails to return anything' death * spiral - but also because its a good idea anyway. Why DataInputStream * doesn't have a TimeOutException, given its most often used to get data * from a URL, I don't know. Lets default it to 60 seconds. * DLConsulting 12-07-2004 */ private int timeout = 60; public String getAppletInfo() { return "Phind by Gordon Paynter (paynter@cs.waikato.ac.nz). Copyright 1997-2000."; } public void init() { mode = initMode; System.out.println("Java vendor: " + System.getProperty("java.vendor")); System.out.println("Java version: " + System.getProperty("java.version")); // Read applet parameters getParameters(); // Initialise the user interface setBackground(panel_bg); lastQueryEndTime = new Date(); // fonts used to output text plainFont = new Font(fontName, Font.PLAIN, fontSize); boldFont = new Font(fontName, Font.BOLD, fontSize); // The phind applet layout manager setLayout(new BorderLayout()); // Panel containing the displays is in the center of the display resultPanel = new Panel(); if (vertical) { resultPanel.setLayout(new GridLayout(depth,1,0,2)); } else { System.out.println("horizontal"); resultPanel.setLayout(new GridLayout(1,depth,2,0)); } add("Center", resultPanel); // Create ResultDisplays and place into the interface ResultDisplay d1, d2 = null; firstDisplay = new ResultDisplay(this, null); resultPanel.add(firstDisplay); if (depth == 1) { lastDisplay = firstDisplay; } else { d1 = firstDisplay; for (int i = 2; i <= depth; i++) { d2 = new ResultDisplay(this, d1); resultPanel.add(d2); d1 = d2; } lastDisplay = d2; } // The control panel initialiseControlPanel(); // lets get started then setStatus("Welcome to Phind."); mode = idleMode; // Perform initial search, if requested if (initialSearch.length() > 0) { searchForWord(initialSearch); } } // Display a message in the status bar void setStatus(String status) { showStatus(status); } // The user performs an action in the interface public boolean action(Event evt, Object arg) { if (evt.target == searchButton) { searchForWord(getSearchTerm()); } else if (evt.target == wordField) { searchForWord(getSearchTerm()); } else if (evt.target == prevButton) { shiftPrevious(); } else if (evt.target == nextButton) { shiftNext(); } else { System.out.println("unknown action: " + evt.toString() + ", object: " + arg.toString()); } return true; } // Search for a word // // Called on two occasions: // when the "Search" Button is pressed, or // to perform an "initial search" void searchForWord(String searchWord) { if (mode == idleMode) { setSearchTerm(searchWord); // we need to keep the original version of the search word to // display for the title of the result box for the case where // there are no results. Otherwise you end up with URL encoded // version being displayed. String originalSearchWord = searchWord; // Convert the String from UTF8 charaters into // an encoding that is okay for a URL. searchWord = URLUTF8Encoder.encode(searchWord); // Look up the word if (searchWord.length() > 1) { setStatus("searching for \"" + searchWord + "\""); firstDisplay.emptyContents(); ResultBox result = lookupPhraseOnServer(null, false, searchWord, originalSearchWord, 2); // if there is an error, return if (result == null) { setStatus("No results for \"" + searchWord + "\""); return; } // display the result result.display = firstDisplay.display(result); result.resize(result.display.size()); result.paintAll(result.getGraphics()); } enablePreviousAndNext(); } } // Search for a phrase // // If querymode is 2, the user has clicked on a phrase. // If querymode is 3, the user has requested more phrases. // If querymode is 4, the user has requested more documents. void searchForPhrase(ResultBox source, String key, String phrase, int queryMode) { // System.out.println("searchForPhrase: " + key + " " + phrase + " " + queryMode); if (mode == idleMode) { // If we are going to replace the first ResultDisplay, then empty it if (queryMode <= 2) { if (source.display.next != null) source.display.next.emptyContents(); } // look up the word setStatus("Searching for \"" + phrase + "\""); ResultBox result = lookupPhraseOnServer(source, true, key, phrase, queryMode); if (result == null) { setStatus("No result for \"" + phrase + "\""); return; } // If this is not already displayed, display it in the last free spot if (queryMode <= 2) { result.display = lastDisplay.display(result); result.resize(result.display.size()); result.paintAll(result.getGraphics()); } enablePreviousAndNext(); } } // Look up a phrase (or symbol) on the server // // Arguments are the source of the query (a ResultBox, or null if the // query comes from hitting the search button), the key to search for // (the text of a phrase or a symbol number), the phrase as a string, // and the query mode. // Query modes are: // 0 = obsolete // 1 = obsolete // 2 = get first N phrases and URLs, // 3 = get another N phrases into same window // 4 = get another N documents into same window // 5 = get another N thesaurus links into same window ResultBox lookupPhraseOnServer(ResultBox source, boolean keyKnown, String key, String phrase, int queryMode) { disableSearchButton(); mode = searchMode; ResultBox r = null; if (queryMode <= 2) { r = new ResultBox(this, collection, key, phrase, source); } else if ((queryMode == 3) || (queryMode == 4) || (queryMode == 5)) { r = source; } try { queryServer(keyKnown, key, queryMode, r); } catch (Exception e) { System.out.println("Phind query error: " + e.toString()); setStatus("Query error: " + e.toString()); mode = idleMode; enableSearchButton(); return null; } // The query is finished setStatus(r.c.numberOfItems + " results for \"" + phrase + "\""); mode = idleMode; enableSearchButton(); lastQueryEndTime = new Date(); return r; } // Query the phindcgi program // // Send a query (a word or symbol number) to the server // and pass the response to a ResultBox. void queryServer(boolean keyKnown, String word, int queryMode, ResultBox area) throws IOException { // Build the query String query = phindcgi_address + "pxml=1&c=" + collection + "&pc=" + classifier; if (keyKnown) { query = query + "&ppnum=" + word; } else { query = query + "&pptext=" + word; } // Specify the set of results to return int first_e = 0; int last_e = 0; int first_d = 0; int last_d = 0; int first_l = 0; int last_l = 0; // the initial query if (queryMode <= 2) { last_e = phraseBlockSize; last_d = phraseBlockSize; last_l = phraseBlockSize; } // add phrases to an existing result set else if (queryMode == 3) { first_e = area.nextPhraseBlock * phraseBlockSize; area.nextPhraseBlock++; last_e = area.nextPhraseBlock * phraseBlockSize; } // add documents to existing result set else if (queryMode == 4) { first_d = area.nextDocumentBlock * phraseBlockSize; area.nextDocumentBlock++; last_d = area.nextDocumentBlock * phraseBlockSize; } // add thesaurus links to existing result set else if (queryMode == 5) { first_l = area.nextThesaurusLinkBlock * phraseBlockSize; area.nextThesaurusLinkBlock++; last_l = area.nextThesaurusLinkBlock * phraseBlockSize; } query = query + "&pfe=" + first_e + "&ple=" + last_e + "&pfd=" + first_d + "&pld=" + last_d + "&pfl=" + first_l + "&pll=" + last_l; // Send the query to the phindcgi program System.out.println("sending query: " + query); /** Start timeout counter. * DLConsulting 12-07-2004 */ int time = 0; try { URL phindcgi = new URL(query); // DataInputStream in = new DataInputStream(phindcgi.openStream()); URLConnection conn = phindcgi.openConnection(); DataInputStream in = new DataInputStream(conn.getInputStream()); byte[] buffer; int availableBytes = 0; while (!area.finished) { // Lovely old method, which would work just fine if Sun supported their own API and made // in.available() work. /* availableBytes = in.available(); if (availableBytes == 0) { // if i had threads i'd do a wait here for 1 second /** Ah-ha but we do have threads here - at least one thread * anyway! Note that we want to prevent interrupted * exceptions flinging us out of the read loop. * We also check that the time count hasn't reached out * timeout threshold yet. If so, make use of the nifty * execption handling. * DLConsulting 12-07-2004 * Start... System.err.println("No bytes available"); if(time >= timeout) { throw new InterruptedIOException( "No data recieved in " + time + " seconds. Connection timed-out."); } try { Thread.sleep(1000); } catch(InterruptedException exception) { System.err.println("Unexpected InterruptedException. Non-fatal."); exception.printStackTrace(); } time++; /** ...End } else { System.err.println("Preparing to read " + availableBytes + " bytes"); buffer = new byte[availableBytes]; in.read(buffer); System.err.println("Done reading"); area.parseBytes(buffer); } */ // New - and hopefully safer - method. // Create a new SafeReader, and then wait while it reads // we can watch for timeouts as necessary int timeout = 0; SafeReader reader = new SafeReader(in); reader.start(); while(!reader.isComplete()) { // Wait timeout time. try { Thread.sleep(1000); } catch(InterruptedException exception) { System.err.println("test.init(): Unexpected InterruptedException. Non-fatal."); exception.printStackTrace(); } // Then see if the readers read anything if(reader.poll() == 0) { timeout++; // Have we reached timeout? if(timeout > reader.getTimeout()) { // Nothing been read. Kill the reader reader.interrupt(); // And throw a hissy fit throw new InterruptedIOException("No data recieved in " + reader.getTimeout() + " seconds. Connection timed-out."); } // Otherwise, wait a little longer. } // Otherwise we can continue waiting and polling } // Process the buffer System.err.println("Done reading"); area.parseBytes(reader.getBuffer()); } in.close(); } catch (Exception e) { System.err.println( "Error sending query to phindcgi: " + e); } area.repaint(); } // Tidy up URLs // // Ensure a URL address (as string) has a protocol, host, and file. // // If the URL is a CGI script URL, it should be tidied up so that it is // appropriate to tage attrib=value pairs on the end. This means it // must either end with a "?" or (if it contains a question-mark // internally) end with a "&". String tidy_URL(String address, boolean isCGI) { // System.err.println("tidy URL: " + address); // make sure the URL has protocol, host, and file if (address.startsWith("http")) { // the address has all the necessary components } else if (address.startsWith("/")) { // there is no protocol and host URL document = getDocumentBase(); String protocol = document.getProtocol(); String host = document.getHost(); String port = ""; if (document.getPort()!=-1) { port = ":" + document.getPort(); } address = protocol + "://" + host + port + address; } else { // this URL is relative to the directory the document is in URL document = getDocumentBase(); String protocol = document.getProtocol(); String host = document.getHost(); String directory = document.getFile(); int end = directory.lastIndexOf('/'); String port = ""; if (document.getPort() != -1) { port = ":" + document.getPort(); } directory = directory.substring(0, end + 1); address = protocol + "://" + host + port + directory + address; } // if the URL is a cgi script, make sure it has a "?" in it, // and that it ends with a "?" or "&" if (isCGI) { if (address.indexOf((int) '?') == -1) { address = address + "?"; } else if (!address.endsWith("?")) { address = address + "&"; } } return address; } // Open an arbitrary web page void displayWebPage(String address, String window) { try { URL url= new URL(address); if (window.length() > 0) { getAppletContext().showDocument(url, window); } else { getAppletContext().showDocument(url); } } catch (Exception e) { System.out.println("Cannot open web page: " + e.toString()); } } // Get the applet parameters void getParameters() { // What is this collection called? collection = parameterValue("collection"); System.out.println("Phind collection: " + collection); // Which of the collection's classifiers are we using? classifier = parameterValue("classifier", "1"); System.out.println("Phind classifier: " + classifier); // Where is the phind CGI script phindcgi_address = parameterValue("phindcgi"); phindcgi_address = tidy_URL(phindcgi_address, true); System.out.println("Phind phindcgi: " + phindcgi_address); // Where is the Greenstone library library_address = parameterValue("library"); library_address = tidy_URL(library_address, true); System.out.println("Phind library: " + library_address); // Is there a default search term? initialSearch = parameterValue("initial_search", ""); // Should we display the control panel showControlPanel = true; if (parameterValue("control_panel", "show").toLowerCase().equals("hide")) { showControlPanel = false; } // Should we show a background image? backdrop_address = parameterValue("backdrop", ""); if (backdrop_address.length() > 0) { backdrop_address = tidy_URL(backdrop_address, false); System.out.println("Phind backdrop URL: " + backdrop_address); try { URL backdrop_url = new URL(backdrop_address); backgroundImage = getImage(backdrop_url); showImage = true; } catch (Exception e) { System.out.println("Phind could not load " + backdrop_address); showImage = false; } } // Should we draw a border? showBorder = parameterValue("border", "on").equals("off"); // Are the windows arranged vertically or horizontally if (parameterValue("orientation", "vertical").toLowerCase().startsWith("hori")) { vertical = false; } else { vertical = true; } // How many phind windows are there? depth = parameterValue("depth", 3); // is the text right to left? String rtl = parameterValue("textorientation", "ltr"); if (rtl.equals("rtl")) { righttoleft = true; } else { righttoleft = false; } // Result sort order // Standard is "LlEeDd", expansion-first is "EeLlDd" String order = parameterValue("resultorder", "standard"); if (!order.equals("standard")) { int next = 20; ResultItem.sortMessage = next; for (int x = 0; x < order.length(); x++) { if (order.charAt(x) == ',') { next--; } else if (order.charAt(x) == 'L') { ResultItem.sortLinkItem = next; } else if (order.charAt(x) == 'l') { ResultItem.sortMoreLinks = next; } else if (order.charAt(x) == 'E') { ResultItem.sortPhraseItem = next; } else if (order.charAt(x) == 'e') { ResultItem.sortMorePhrases = next; } else if (order.charAt(x) == 'D') { ResultItem.sortDocumentItem = next; } else if (order.charAt(x) == 'd') { ResultItem.sortMoreDocuments = next; } } System.out.println("link: " + ResultItem.sortLinkItem); System.out.println("exps: " + ResultItem.sortPhraseItem); System.out.println("docs: " + ResultItem.sortDocumentItem); } // How many phrases should we fetch at any given time? phraseBlockSize = parameterValue("blocksize", 10); // What font should we use? fontSize = parameterValue("fontsize", 10); fontName = parameterValue("fontname", "Helvetica"); // Column dimensions column_1_width = parameterValue("first_column_width", 6); column_2_width = parameterValue("second_column_width", column_1_width); // Where do we open new windows searchWindowName = parameterValue("search_window", "phindsearch"); documentWindowName = parameterValue("document_window", "phinddoc"); // Colours panel_fg = parameterValue("panel_fg", Color.black); panel_bg = parameterValue("panel_bg", Color.white); highlight_bg = parameterValue("highlight_bg", Color.yellow); expansion_fg = parameterValue("expansion_fg", Color.black); thesaurus_fg = parameterValue("thesaurus_fg", new Color(0, 100, 0)); document_fg = parameterValue("document_fg", Color.blue); thesaurus_bar_fg = parameterValue("thesaurus_bar_fg", Color.black); expansion_bar_fg = parameterValue("expansion_bar_fg", Color.black); document_bar_fg = parameterValue("document_bar_fg", Color.black); thesaurus_bar_bg = parameterValue("thesaurus_bar_bg", new Color(160, 160, 190)); expansion_bar_bg = parameterValue("expansion_bar_bg", new Color(255, 200, 200)); document_bar_bg = parameterValue("document_bar_bg", new Color(150, 193, 156)); column_1_fg = parameterValue("first_column_fg", Color.black); column_1_bg = parameterValue("first_column_bg", new Color(235, 245, 235)); column_2_fg = parameterValue("second_column_fg", Color.black); column_2_bg = parameterValue("second_column_bg", new Color(200, 220, 200)); message_fg = parameterValue("message_fg", Color.black); message_bg = parameterValue("message_bg", Color.white); // Colours I don't use, yet // thesaurus_bg = parameterValue("thesaurus_bg", Color.white); // expansion_bg = parameterValue("expansion_bg", Color.white); // document_bg = parameterValue("document_bg", Color.white); /** How long should the read loop wait before timing out? * DLConsulting 12-07-2004 */ timeout = parameterValue("timeout", 60); } // Get the value of a parameter given its name. // There are many types of parameters, hence the variety of functions. // Get a REQUIRED string. Stop the applet if we cannot. String parameterValue(String name) { try { return getParameter(name); } catch (Exception e) { System.err.println("Phind: you must give a parameter for \"" + name + "\". Stopping."); stop(); } return ""; } // Get an optional parameter. Return a default if we cannot. String parameterValue(String name, String defaultValue) { String text = getParameter(name); if (text == null) { return defaultValue; } System.out.println("Phind " + name + ": " + text); return text; } int parameterValue(String name, int defaultValue) { int value; try { value = Integer.parseInt(getParameter(name)); } catch (Exception e) { return defaultValue; } System.out.println("Phind " + name + ": " + value); return value; } Color parameterValue(String name, Color defaultValue) { String text = getParameter(name); if (text == null) { return defaultValue; } text = text.toLowerCase(); // a number of the form "#ddffee" if (text.startsWith("#") && (text.length() == 7)) { text = text.substring(1); int r, g, b; try { r = Integer.parseInt(text.substring(0,2), 16); g = Integer.parseInt(text.substring(2,4), 16); b = Integer.parseInt(text.substring(4), 16); return new Color(r, g, b); } catch (Exception e) { return defaultValue; } } // a known Java colour string else if (text.equals("black")) { return Color.black; } else if (text.equals("blue")) { return Color.blue; } else if (text.equals("cyan")) { return Color.cyan; } else if (text.equals("darkgray")) { return Color.darkGray; } else if (text.equals("gray")) { return Color.gray; } else if (text.equals("green")) { return Color.green; } else if (text.equals("lightgray")) { return Color.lightGray; } else if (text.equals("magenta")) { return Color.magenta; } else if (text.equals("orange")) { return Color.orange; } else if (text.equals("pink")) { return Color.pink; } else if (text.equals("red")) { return Color.red; } else if (text.equals("white")) { return Color.white; } else if (text.equals("yellow")) { return Color.yellow; } return defaultValue; } // Control panel operations // Initialise the control panel void initialiseControlPanel() { if (showControlPanel) { Panel p1 = new Panel(); add("North", p1); p1.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); searchButton = new Button("Search"); searchButton.setFont(boldFont); p1.add(searchButton); Label tempLabel = new Label(" for"); tempLabel.setFont(boldFont); p1.add(tempLabel); wordField = new TextField(12); wordField.setFont(boldFont); p1.add(wordField); Label temp2 = new Label(" "); p1.add(temp2); prevButton = new Button("Previous"); prevButton.setFont(boldFont); prevButton.disable(); p1.add(prevButton); nextButton = new Button(" Next "); nextButton.setFont(boldFont); nextButton.disable(); p1.add(nextButton); } } // Button and field functionality // Enable and disable the word field void enableSearchButton() { if (showControlPanel) { searchButton.enable(); } } void disableSearchButton() { if (showControlPanel) { searchButton.disable(); } } // Get and set the search text in the wordField String getSearchTerm() { if (showControlPanel) { return wordField.getText(); } else { return initialSearch; } } void setSearchTerm(String word) { if (showControlPanel) { wordField.setText(word); } } // Enable or disable the "Previous" and "Next" buttons void enablePreviousAndNext() { if (showControlPanel) { Component c = firstDisplay.current; if (c.getClass().getName().endsWith("ResultBox")) { if (((ResultBox) c).prevBoxExists()) { prevButton.enable(); } else { prevButton.disable(); } } c = lastDisplay.current; if (c.getClass().getName().endsWith("ResultBox")) { if (((ResultBox) c).nextBoxExists()) { nextButton.enable(); } else { nextButton.disable(); } } } } // Shift to previous box // // If the user clicks "Previous" then scroll up. void shiftPrevious() { Component c = firstDisplay.current; if (c.getClass().getName().endsWith("ResultBox")) { ResultBox b = (ResultBox) c; if (b.prevBoxExists()) { b = b.prev; // empty all the displays firstDisplay.emptyContents(); // add as many result boxes as there are displays for (int i = 1 ; ((i <= depth) && (b != null)); i++) { lastDisplay.display(b); b.resize(b.display.size()); b.paintAll(b.getGraphics()); b = b.next; } } } enablePreviousAndNext(); } // Shift to next box // // If the user clicks "Next" then scroll down if possible void shiftNext() { Component c = lastDisplay.current; if (c.getClass().getName().endsWith("ResultBox")) { ResultBox b = (ResultBox) c; if (b.nextBoxExists()) { // find the new "first" displayed box c = firstDisplay.current; b = (ResultBox) c; b = b.next; // empty all the displays firstDisplay.emptyContents(); // add as many result boxes as there are displays for (int i = 1 ; ((i <= depth) && (b != null)); i++) { lastDisplay.display(b); b.resize(b.display.size()); b.paintAll(b.getGraphics()); b = b.next; } } } enablePreviousAndNext(); } public class SafeReader extends Thread { // Has the reader finished reading? private boolean complete; // The buffer in where the data read is stored private byte[] buffer; // The stream this reader reads from private DataInputStream in; // If the bytes_read hasn't changed in this time, then stop this thread // and timeout private int timeout; // The current buffer size private int buffer_current_size; // The maximum buffer size private int buffer_max_size; // The number of bytes read through this reader since the last poll private int bytes_read; /** Default constructor * @param in the DataInputStream to read from */ public SafeReader(DataInputStream in) { complete = false; buffer_current_size = 0; buffer_max_size = 16; buffer = new byte[buffer_max_size]; // Default size timeout = 60; // Sixty seconds bytes_read = 0; this.in = in; } /** Construct a new SafeReader * @param in the DataInputStream to read from * @param timeout how long this thread should wait when reading data */ public SafeReader(DataInputStream in, int timeout) { this(in); this.timeout = timeout; } /** Retrieve the current buffer * @return a byte[] buffer trimmed to the appropriate size */ public byte[] getBuffer() { byte[] final_buffer = new byte[buffer_current_size]; System.arraycopy(buffer, 0, final_buffer, 0, buffer_current_size); return final_buffer; } /** Retrieve the current timeout * @return the timeout as an int */ public int getTimeout() { return timeout; } /** See whether the reader has finished reading * @return true if the read is complete (-1 bytes read!) or false otherwise */ public synchronized boolean isComplete() { return complete; } /** Poll the reader to determine if it has read any bytes recently * @return the number of bytes read */ public long poll() { // Maybe should test if complete - but its only one second right? long result = bytes_read; bytes_read = 0; return result; } /** Try to read bytes from the input stream. Note the caller is responsible for closing the datainputstream */ public void run() { try { int i = 0; byte[] read_buffer = new byte[8]; while ((i = in.read(read_buffer)) != -1) { // Check our storage buffer is big enough if(buffer_max_size - (buffer_current_size + i) <= 0) { // Double the size buffer_max_size = 2 * buffer_max_size; byte[] new_buffer = new byte[buffer_max_size]; System.arraycopy(buffer, 0, new_buffer, 0, buffer_current_size); buffer = new_buffer; } // Then copy in the read buffer System.arraycopy(read_buffer, 0, buffer, buffer_current_size, i); buffer_current_size += i; bytes_read += i; } setComplete(true); } catch(IOException exception) { System.err.println("SafeReader.run(): Unexpected IOException. Fatal."); exception.printStackTrace(); } } /** Set the complete boolean to the desired state, synchronized to avoid race with isComplete() * @param value the desired state */ private synchronized void setComplete(boolean value) { complete = value; } } }