// AVQuery.java // // Sample solution to CMP 318 99-1 Assignment #2 Q2 // // import java.applet.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; import java.util.*; /** * AVQuery is an applet that queries the AltaVista search engine and outputs * the results in a simple list. * *

Operation Notes

* * The applet allows the user to enter a query term in Altavista's standard * (i.e. not the "advanced" setting) query format and shows the resulting * result set consisting of hits to other web pages. The user can control * the number of hits to load from the server via a simple setting.

* * Note that the server requesting is entirely synchronous, that is, * the user enters a new search term and the program does not come back * to be controlled until the search results are received. * *

Architecture Notes

* * The applet is divided according to the MVC architecture. It looks like * this: *

* * The ResultsModel is completely separated from UI, View, or Controller * code. The Views and Controllers all make use of UI components by * object composition rather than inheritance. That way the GUI * appearances and behaviour can be modified without changing the * interfaces of these components.

* * Finally, note that the AVQuery object is in a priveleged position * compared to the MVC components: it does the component construction, * understands the MVC connections, and knows about the overall UI and * how to place sub-components of the interface on them. For that reason, * it knows about the TextArea used to display the results. * * @author Andrew Walenstein */ public class AVQuery extends Applet { // Note that I've violated normal naming convention in order to strongly // encode the major application objects present in the applet. The suffix // reflects the role of the object. // // results_MOD - the model is the results set // search_CONT - search is parameterized by a search setting // maxHits_CONT - search is parameterized by a maximum hits setting // listing_VCONT - result set is viewed by listing; this controls display // fakeDispI_VIEW - display is an interface-level view independent of model // private final ResultsModel results_MOD = new ResultsModel(); private final SearchControl search_CONT = new SearchControl(); private final MaxHitControl maxHits_CONT = new MaxHitControl(); private final ListView listing_VCONT = new ListView(); private final TextArea fakeDispI_VIEW = new TextArea(4,40); // There are 4 observers, each corresponding to a connection between // an applet component. // // resultsModelQuery - initiate search when new search term event occurs // resultsModelNewMaxHits - changes number of hits for further searches // listViewRefresh - updates view completely from model // fakeDisplayBrowseURL - fakes a browser follow to a given URL. // private final Observer resultsModelQuery = new Observer() { public void update ( Observable o, Object newQueryText ) { results_MOD.setQuery ( (String)newQueryText ); } }; private final Observer resultsModelNewMaxHits = new Observer() { public void update ( Observable o, Object oo ) { results_MOD.setMaxHits ( maxHits_CONT.getMaxHits() ); } }; private final Observer listViewRefresh = new Observer() { public void update ( Observable o, Object hitsObj ) { if ( hitsObj != null ) listing_VCONT.updateFrom ( (Hit[]) hitsObj ); } }; private final Observer fakeDisplayBrowseURL = new Observer() { public void update ( Observable o, Object selectedNum ) { final int selIndex = ((Integer)selectedNum).intValue(); final Hit hSelected = results_MOD.getHit ( selIndex ); fakeDispI_VIEW.setText ( "Selected #" + selIndex + " " + hSelected.theURL + "\n\n" + hSelected.body ); } }; /** * Initializes the browser applet. */ public void init() { // initialize model from controller defaults // results_MOD.setMaxHits ( maxHits_CONT.getMaxHits() ); // Connect up main application objects using observer objects // results_MOD.addObserver ( listViewRefresh ); search_CONT.addObserver ( resultsModelQuery ); maxHits_CONT.addObserver ( resultsModelNewMaxHits ); listing_VCONT.addObserver ( fakeDisplayBrowseURL ); // UI part: add Components to our Container // final Label appTitle = new Label( "AltaVista Query Applet", Label.CENTER ); appTitle.setFont ( new Font ( "Times", Font.BOLD, 16 ) ); appTitle.setForeground ( new Color ( 255, 250, 250 ) ); appTitle.setBackground ( new Color ( 120, 160, 170 ) ); final Panel headPanel = new Panel ( new BorderLayout() ); headPanel.add ( appTitle, BorderLayout.CENTER ); headPanel.add ( maxHits_CONT.getComponent(), BorderLayout.EAST ); final Panel topPanel = new Panel ( new BorderLayout() ); topPanel.add ( headPanel, BorderLayout.NORTH ); topPanel.add ( search_CONT.getComponent(), BorderLayout.SOUTH ); setLayout ( new BorderLayout() ); add ( topPanel, BorderLayout.NORTH ); add ( listing_VCONT.getComponent(), BorderLayout.CENTER ); add ( fakeDispI_VIEW, BorderLayout.SOUTH ); } } /** * The applet is divided in UI (V & C) and non-UI (M) classes. * All UI classes need to be connected to a java.awt.Container in * order to receive or send UI messages. Thus they all need to * be composed of some java.awt.Components, but the client classes * do not want to know which. Instead of extending a particular * component, the UI classes conform to this interface, allowing * the non-UI classes to subclass other classes (such as Observable). */ interface AVQueryUIComponent { /** * All UIComponents "own" one part of the display, allowing clients * to add this Component to the display as they see fit. */ public Component getComponent(); } /** * SearchControl is a Controller for the search term. Whenever a new * search term is entered, it tells all observers. * * Note that SearchControl encapsulates completely the UI sub-components * since no client classes have to deal with java.awt.event.* events. * Note also that it is completely reusable since (a) it does not make any * assumptions about listeners, and (b) it observes AVQueryUIComponent to * allow its client to ask for its displayable part. * * The actual search controller is composed of a text field * and search button. Actions are fired either when the button is * depressed, or when the text field has a entered into it. */ class SearchControl extends Observable implements AVQueryUIComponent { public Component getComponent() { return myPanel; } // myPanel - AWT component this search panel is known by // searchTextField - UI component holds value of text string to send // searchActionAdapter - adapts UI events to component communication events // private final Panel myPanel; private final TextField searchTextField; private final ActionListener searchActionAdapter = new ActionListener() { public void actionPerformed ( ActionEvent e ) { final String searchText = searchTextField.getText(); if ( searchText.length() > 0 ) { setChanged(); notifyObservers ( searchText ); } } }; private static final int DEFAULT_SEARCH_WIDTH = 50; /** * @param width width of the text field in characters */ SearchControl() { searchTextField = new TextField ( DEFAULT_SEARCH_WIDTH ); final Button sButt = new Button ( "Search" ); // Redirect events from the button or text field to the observers // sButt.addActionListener ( searchActionAdapter ); searchTextField.addActionListener ( searchActionAdapter ); // Bundle both sub-subcomponents into a collection // myPanel = new Panel ( new BorderLayout() ); myPanel.add ( sButt, BorderLayout.WEST ); myPanel.add ( searchTextField, BorderLayout.CENTER ); } } /** * MaxHitControl control modifies the model's maximum hit amount. */ class MaxHitControl extends Observable implements AVQueryUIComponent { public Component getComponent() { return checksPanel; } // checksPanel - container holding the controller objects // selectionGroup - ensures checkboxes are mutually exclusive // checkActionAdapter - adapts UI events to component communication events // private final Panel checksPanel; private final CheckboxGroup selectionGroup = new CheckboxGroup(); private final ItemListener checkActionAdapter = new ItemListener() { public void itemStateChanged ( ItemEvent e ) { setChanged(); notifyObservers(); // observers query controller through getMaxHits() } }; /** * @return the maximum number of hits, -1 on error */ public int getMaxHits() { final String selectedL = selectionGroup.getSelectedCheckbox().getLabel(); try { return Integer.parseInt ( selectedL ); } catch ( NumberFormatException e ) { } return -1; } MaxHitControl() { final String[] sizes = { "20", "50", "100", "200" }; final Panel marksPanel = new Panel ( new GridLayout ( 1, sizes.length ) ); final Font smallFont = new Font ( "Times", Font.PLAIN, 9 ); for ( int i = 0 ; i < sizes.length ; ++i ) { final Checkbox box = new Checkbox ( sizes[i], selectionGroup, i==0 ); box.setFont ( smallFont ); box.addItemListener ( checkActionAdapter ); marksPanel.add ( box ); } final Label hitsLabel = new Label ( "Max Hits: " ); hitsLabel.setFont ( smallFont ); checksPanel = new Panel ( new FlowLayout ( FlowLayout.RIGHT ) ); checksPanel.add ( hitsLabel ); checksPanel.add ( marksPanel ); } } /** * ListView is a View that displays the model result values in a List. * It changes the list as the model changes. Note that the granularity * of change for the list is on the order of an entire new list rather * than a single item change. * * ListView also is a UI-level controller. When a selection is made, it * notifies UI-level objects that are observing it. */ class ListView extends Observable implements AVQueryUIComponent { public Component getComponent() { return hitList; } // hitList - AWT component used to display/control // selectionActionAdapter - adapts UI events to UI component communication // final private List hitList; private final ActionListener selectActionAdapter = new ActionListener() { public void actionPerformed ( ActionEvent e ) { setChanged(); notifyObservers ( new Integer ( hitList.getSelectedIndex() ) ); } }; private static final int DEFAULT_LIST_LENGTH = 40; /** * add a listing view of an Altavista Results model. */ ListView() { hitList = new List ( DEFAULT_LIST_LENGTH ); hitList.addActionListener ( selectActionAdapter ); } /** * the list is re-constructed from a set of search results. * @param hits the list of hits to update from */ public void updateFrom ( Hit[] hits ) { hitList.removeAll(); for ( int i = 0 ; i < hits.length ; ++i ) { hitList.add ( hits[i].title ); } } } /** * A ResultsModel holds a query result set from an AltaVista query. * This is an Observable. Its internal results hits changes every * time the search term changes (if query state is valid). * * Note that it knows nothing about UIs, and could easily have been in a * separate file. */ class ResultsModel extends Observable { // nullResult - an empty results list // private static final Hit[] nullResult = new Hit[0]; // STATES: two changeable [C] internal states, one dependent [D] state // // hits - [D] result set matching queryText // maxHits - [C] max number of hits to return from query // queryText - [C] text with which to query search engine // private Hit[] hits = nullResult; private int maxHits = 0; private String queryText = ""; /** * Used to query model about results values. * @param resultNumber index of query hit to return * @return Hit number "resultNumber" */ public Hit getHit ( int resultNumber ) { return hits[resultNumber]; } /** * Sets the number of hits that are requested from AltaVista when the * query text is set. * * @param maxHits maximum number of hits to find, should be > 0. */ public void setMaxHits ( int maxHits ) { this.maxHits = maxHits; } /** * Sets the query text and corresponding results list. * * @param queryText the search string in AltaVista search key format */ synchronized public void setQuery ( String queryText ) { if ( queryText.length() <= 0 || maxHits <= 0 ) return; // Note that we change states only if the querying was successful // try { hits = AltavistaReader.getQuery ( queryText, maxHits ); if ( hits == null ) hits = nullResult; this.queryText = queryText; setChanged(); notifyObservers ( hits ); } catch ( IOException e ) { } } }