import java.io.*; import java.util.*; import java.lang.reflect.*; /** * Provides a simple text-based user interface for searching a gene / protein * data repository that implements the SequenceSearch interface, and returns * data in SearchResults. */ public class SimpleSearchTool { // Class variables /** This system's newline string. */ private static final String newline = System.getProperty( "line.separator" ); // Class methods /** * Use this to signal bugs, not user or system errors. * @param message to provide in exception */ private static void panic( String message ) { throw new RuntimeException( "Bug: " + message ); } // Instance variables /** Our SequenceSearch, which we call to do all the real work. */ private SequenceSearch searcher; /** A reader for System.in. */ private BufferedReader in; /** How many search results to show on a "page". */ private int nPerPage = 10; // If this were a FancySearchTool, we'd set this // based on the available lines per page. // Constructor /** * Make a SimpleSearchTool. This mainly consists of storing the supplied * SequenceSearch reference and getting a reader for System.in. * * @param a SequenceSearch instance */ public SimpleSearchTool( SequenceSearch searcher ) { if ( searcher == null ) throw new NullPointerException( "Must provide a SequenceSearch" ); this.searcher = searcher; // Open our input. in = new BufferedReader( new InputStreamReader(System.in) ); } // Instance methods /** * Wrap our System.in reader so we only have to handle its exceptions * in one place. Quit if the reader gets an error. Also prompt and * verify that answer is chosen from acceptable set. * * @param prompt - text to be typed before reading user's response; may * be null in which case the prompt is ?. */ private String getUserInput( String prompt ) { String response = null; if ( prompt == null ) prompt = "? "; System.out.print( prompt ); try { response = in.readLine(); } catch ( IOException e ) { System.out.println( "Problem reading input: " + e.getMessage() ); System.exit(1); } return response; } /** * Ask user to choose one of several options represented by single * characters. Retry until they choose one of them. Options are * assumed not to be case sensitive. * *

(Note: It is recommended that caller include a cancel or quit or * "none of the above" option... This method would take no special * action in that case, but it gives the user something to answer if * they don't like the other choices.) * *

* @param prompt - Text to be typed before reading user's response -- may * be null. List of options in brackets is appended to the prompt. * @param options - String of unique initial characters of acceptable * responses. If null, any response is taken to be acceptable. * @return initial character of the response, converted to uppercase. */ private char getUserChoice( String prompt, String options ) { char choice = '\0'; // the ASCII null char String response = null; // Simplify our tests. if ( options != null && options.length() == 0 ) { options = null; } else { options = options.toUpperCase(); } // Add the options, if any, onto the prompt, if any. if ( prompt == null ) prompt = ""; if ( options != null ) { prompt += " [" + options + "] "; } // Keep asking til we get a known option. while ( true ) { // Get the answer: response = getUserInput( prompt ); // Check if it's acceptable. if ( response.length() > 0 ) { choice = response.toUpperCase().charAt(0); // Done if caller did not specify options, or if their choice // is in the options. if ( options == null || options.indexOf( choice ) >= 0 ) return choice; } // If we get here, the choice wasn't acceptable, and we'll ask again. System.out.println( "That wasn't one of the choices -- please try again." ); } } /** * Get an integer response from the user. * @prompt a prompt string -- if null, "? " is used * @return the user's integer */ private int getUserInteger( String prompt ) { String response = null; if ( prompt == null ) prompt = "? "; // Retry if they don't type a valid integer. while ( true ) { response = getUserInput( prompt ); try { return Integer.parseInt( response ); } catch ( Exception e ) { System.out.println( "Could not interpret that as a number -- please try again." ); } } } /** * Ask whether user wants to use Hamming or edit distance. * @return H for Hamming, E for edit */ private char whichDistance() { return getUserChoice( "Do you want to use:" + newline + " Hamming distance (H)" + newline + " Edit distance (E)" + newline + "Choice", "HE" ); } /** * Start the user interface. Asks the user what they want to do, calls * our SequenceSearch to do it, reports the results. */ public void run() { // Continue til the user tells us to quit. while ( true ) { // Get the overall type of operation: search the database, search // one sequence, compute distance. char task = getUserChoice( newline + "Would you like to:" + newline + " (F) Find sequences with matches?" + newline + " (S) Search for matches in one sequence?" + newline + " (D) Compute the distance between two strings?" + newline + " (T) See which protein translation is used?" + newline + " (X) Translate a sequence?" + newline + " (E) Exit?" + newline + "Choice", "FSDTXE" ); // Service the choice. switch ( task ) { case 'F': findSequences(); break; case 'S': searchOneSequence(); break; case 'D': computeDistance(); break; case 'T': reportTranslationVersion(); break; case 'X': translateSequence(); break; case 'E': return; default: // Do switch cases match options? panic( "got unknown task choice" ); } } } /** * User wants to find sequences with matches. */ private void findSequences() { Collection results = null; // Will be a Collection of SearchResult. // Ask whether to search genes or proteins. char sequenceType = getUserChoice( "Search genes (G) or proteins (P)?", "GP" ); // Ask which distance. char distanceType = whichDistance(); // Get the pattern. String pattern = getUserInput( "Pattern to match: " ); // And get the maximum distance. int maxDistance = getUserInteger( "Maximum distance? " ); switch ( sequenceType ) { case 'G': switch ( distanceType ) { case 'H': results = searcher.searchGenesByHammingDistance( pattern, maxDistance ); break; case 'E': results = searcher.searchGenesByEditDistance( pattern, maxDistance ); break; default: panic( "got unknown distance choice" ); } break; case 'P': switch ( distanceType ) { case 'H': results = searcher.searchProteinsByHammingDistance( pattern, maxDistance ); break; case 'E': results = searcher.searchProteinsByEditDistance( pattern, maxDistance ); break; default: panic( "got unknown distance choice" ); } break; default: panic( "got unknown sequence type choice" ); } // Found anything? if ( results == null || results.isEmpty() ) { System.out.println( "No matches found." ); return; } // Here, we have matches to report. In case there are a lot, show them // a few at a time. Iterator it = results.iterator(); SearchResult result = null; int nShown = 0; // Count how many we've shown on this page. System.out.println( newline + "Results:" ); while ( it.hasNext() ) { if ( nShown == nPerPage ) { // Ask if they want to continue. char more = getUserChoice( "Continue?", "YN" ); if ( more == 'N' ) break; nShown = 0; // They want to continue -- show another batch. } else { nShown++; } try { result = (SearchResult) it.next(); } catch ( ClassCastException e ) { panic( "Search for sequences with matches did not " + "return results in a known form." ); } // Show the result using the SearchResult's own toString. System.out.println( result ); System.out.println(); } } /** * User wants to search a sequence. */ private void searchOneSequence() { List results = null; // Will be a List of Integer. // Get the sequence and pattern. String sequence = getUserInput( "Sequence to search in:" + newline ); String pattern = getUserInput( "Pattern to match:" + newline ); // Ask which kind of distance. char distanceType = whichDistance(); // And get the maximum distance. int maxDistance = getUserInteger( "Maximum distance? " ); switch ( distanceType ) { case 'H': results = searcher.searchOneSequenceByHammingDistance( sequence, pattern, maxDistance ); break; case 'E': results = searcher.searchOneSequenceByEditDistance( sequence, pattern, maxDistance ); break; default: panic( "got unknown distance choice" ); } // Found anything? if ( results == null || results.isEmpty() ) { System.out.println( "No matches found." ); return; } // Show results but don't get fancy. If we wanted fancy, we'd package // the results in a SearchResult, and have its toString provide a nicely // formatted result. Note if the searcher didn't put the results in an // ordered collection, in order, they'll come out any old how... Iterator it = results.iterator(); Integer num = null; System.out.println( newline + "There were matches at these locations:" ); while ( it.hasNext() ) { try { num = (Integer) it.next(); } catch ( ClassCastException e ) { panic( "Search for matches in sequence did not return " + "results in a known form." ); } System.out.print( " " + num ); } System.out.println(); } /** * User wants to compute a distance. */ private void computeDistance() { int distance = 0; // Get the two strings to compare. String a = getUserInput( "First string: " ); String b = getUserInput( "Second string: " ); // Ask which kind of distance. char distanceType = whichDistance(); switch ( distanceType ) { case 'H': distance = searcher.hammingDistance( a, b ); break; case 'E': distance = searcher.editDistance( a, b ); break; default: panic( "got unknown distance choice" ); } // Report the result. System.out.println( "Distance is " + distance ); } /** * User wants to know which of the protein translation procedures has * been implemented. */ private void reportTranslationVersion() { String which = null; switch ( searcher.translationVersion() ) { case 1: which = "the original"; break; case 2: which = "the updated"; break; default: which = "an unknown"; break; } System.out.println( "Protein translation uses " + which + " procedure." ); } /** * User would like to translate a sequence. */ private void translateSequence() { // Get the sequence. String sequence = getUserInput( "Sequence: " ); // Make sure this searcher has a translate( String ) method. Class[] paramTypes = { String.class }; Method method = null; try { method = ((GeneDataSearch) searcher).getClass().getMethod( "translate", paramTypes ); // Ask the searcher to translate it. Object[] params = { sequence }; String protein = null; try { protein = (String) method.invoke( ((GeneDataSearch) searcher), params ); } catch ( IllegalAccessException e ) { System.out.println( "Not allowed to call the translate method." ); return; } catch ( InvocationTargetException e ) { System.out.println( "The translate method threw an exception: " + e.getMessage() ); return; } // Show the translation. System.out.println( "Translation is: " + protein ); } catch ( NoSuchMethodException e ) { System.out.println( "GeneDataSearch doesn't appear to have a translate(String) method." ); return; } } // Class methods /** * Starts up a SimpleSearchTool. */ public static void main( String[] args ) { // Make a GeneDataSearch, which implements SequenceSearch, for // SimpleSearchTool to use. SequenceSearch searcher = new GeneDataSearch(); // Make a SimpleSearchTool and run it. Quit when it exits. SimpleSearchTool tool = new SimpleSearchTool( searcher ); tool.run(); // If we fall out of run, user wants to exit. System.exit(0); } }