001    /*
002     * CRIMSON
003     * Copyright (c) 2006, Stephen Fisher, Susan Davidson, and Junhyong Kim, 
004     * University of Pennsylvania.
005     *
006     * This program is free software; you can redistribute it and/or
007     * modify it under the terms of the GNU General Public License as
008     * published by the Free Software Foundation; either version 2 of the
009     * License, or (at your option) any later version.
010     *
011     * This program is distributed in the hope that it will be useful, but
012     * WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     * General Public License for more details.
015     *
016     * You should have received a copy of the GNU General Public License
017     * along with this program; if not, write to the Free Software
018     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019     * 02110-1301 USA.
020     *
021     * @(#)Query.java
022     */
023    
024    package edu.upenn.crimson;
025    
026    import edu.upenn.crimson.io.*;
027    import java.util.HashSet;
028    import java.util.ArrayList;
029    import java.util.StringTokenizer;
030    import java.util.Scanner;
031    import java.util.Arrays;
032    import java.sql.*;
033    import java.io.*;
034    
035    /**
036     * Contains query info.
037     *
038     * @author  Stephen Fisher
039     * @version $Id: Query.java,v 1.44 2009/07/25 03:37:09 fisher Exp $
040     */
041    
042    public class Query implements Cloneable { 
043    
044            /** This is a unique name for the Query object. */
045            private String id = "";
046            
047            /** Notes specific to this Query object. */
048            private String notes = "";
049            
050            /** 
051             * This is the ID for the tree object that this query refers
052             * to. 
053             */
054            private String treeID = "";
055            
056            /**
057             * LEAF SELECTION: 0 = Select All, 1 = Random Selection, 2 =
058             * Select By Temporal Depth (distributed), 3 = Select By Temporal Depth (weighted), 
059             * 4 = Selection by Level (distributed), 5 = Selection by Level (weighted), 6 = Manual Selection
060             */
061            private int leafSelection = 0;
062            
063            /** 
064             * Number of leaves to include when choosen randomly. */
065            private int numLeaves = 0;
066            
067            /** Temporal depth of the leaves to be selected.*/
068            private double tempDepthThresh = 0.0;
069            
070            /** Level of the leaves to be selected.*/
071            private int levelThresh = 0;
072            
073            /** Set of IDs for the leaves to include. */
074            // TEST *****
075            //      private HashSet leaves = new HashSet();
076            private HashSet leaves = null;
077            
078            /** 
079             * SEQUENCE SELECTION: 0 = Select All, 1 = Random Codon Selection,
080             * 2 = Randon Position Selection, 3 = Manual Codon Selection, 4 =
081             * Manual Position Selection, 5 = Select None
082             */
083            private int sequenceSelection = 0;
084            
085            /** 
086             * Number of positions (base pairs or codons) to include when
087             * choosen randomly.
088             */
089            private int numPositions = 0;
090            
091            /** This set contains the IDs for the partitions to be queried. */
092            // TEST *****
093            //      private HashSet partitions = new HashSet();
094            private HashSet partitions = null;
095            
096            /** 
097             * When true, no data will be included in the output file (ie the
098             * partitions list will be ignored).
099             */
100            private boolean onlyStruct = false;
101            
102            /** 
103             * Base pair or codon positions to include. This is only valid
104             * when a single partition is selected.
105             */
106            // TEST *****
107            //      private HashSet positions = new HashSet();
108            private HashSet positions = null;
109            
110            /** 
111             * Seed for random number generator. If -1, then will be ignored
112             * when the query is run. 
113             */
114            private long seed = -1;
115            
116            /** True when the query object has been saved to the database. */
117            private boolean saved = false;
118            
119            /** Output sequence for internal node. */
120            //       private boolean incInternalSeq = false;
121            
122            /** Create a new Query object. */
123            public Query(String treeID) {
124                    this(treeID, "");
125            }
126            
127            /** Create a new Query object. */
128            public Query(String treeID, String id) throws InvalidIDException {
129                    if (! setTreeID(treeID)) throw new InvalidIDException("Invalid tree ID.");
130                    
131                    id = id.trim().toUpperCase();
132                    
133                    // if no ID, then create a random ID for this Query
134                    if (CrimsonUtils.isEmpty(id)) {
135                            id = ObjectHandles.randomQueryID("Q_");
136                    } else {
137                            if (ObjectHandles.containsQuery(id)) id = ObjectHandles.randomQueryID(id + "_");
138                    }
139                    
140                    this.id = id.toUpperCase();
141                    
142                    // add the query to the relevant lists
143                    ObjectHandles.addQuery(this);
144            }
145            
146        //--------------------------------------------------------------------------
147        // Setters and Getters
148            
149        /** 
150             * Set the ID.  If the new ID is the same as the current ID, then
151             * doesn't do anything.  If the new ID already exists in the
152             * queryPool, then throws an exception.  
153             */
154            /*
155              public void setID(String id) throws InvalidIDException { 
156              // don't do anything if new and old values are the same
157              if (this.id == id) return;
158              
159              // ObjectHandles.renameQuery() will do the actual changing of
160              // the Query's id.
161              try { ObjectHandles.renameQuery(this, id); }
162              catch (InvalidIDException e) { throw e; }
163              
164              this.saved = false;  // potentially out of sync with db
165              }
166            */
167            
168        public String getID() { return id.toUpperCase(); }
169            
170            public void setNotes(String notes) { 
171                    if (notes == null) notes = "";
172                    this.notes = notes; 
173                    
174                    this.saved = false;  // potentially out of sync with db
175            }
176            
177            public String getNotes() { return notes; }
178            
179            /** Returns true if treeID set, false otherwise. */
180            public boolean setTreeID(String treeID) { 
181                    if (CrimsonUtils.isEmpty(treeID)) {
182                            CrimsonUtils.printError("Trying to set query treeID to an empty value.");
183                            return false;
184                    }
185                    
186                    if (! Database.isOpen()) {
187                            CrimsonUtils.printError("Can't create queries when no active database.");
188                            return false;
189                    }
190                    
191                    // XXX WHY DID I COMPARE TO ONLINE DB WHEN I CAN JUST COMPARE TO TREEPOOL???
192                    //              if (Trees.dbContains(treeID)) {
193                    if (ObjectHandles.containsTree(treeID)) {
194                            this.treeID = treeID.toUpperCase(); 
195                            this.saved = false;  // potentially out of sync with db
196                            return true;
197                    } else {
198                            CrimsonUtils.printError("Invalid tree '" + treeID + "'. Tree ID not set for query: " + id);
199                            return false;
200                    }
201            }
202            
203            public String getTreeID() { return treeID.toUpperCase(); }
204            
205            public void setLeafSelection(int leafSelection) { 
206                    if ((leafSelection < 0) || (leafSelection > 6)) {
207                            CrimsonUtils.printError("Invalid leaf selection method, in Query.");
208                    } else {
209                            this.leafSelection = leafSelection; 
210                    }
211                    
212                    this.saved = false;  // potentially out of sync with db
213            }
214            
215            public int getLeafSelection() { return leafSelection; }
216            
217            public String getLeafSelectionString() {
218                    switch (leafSelection) {
219                    case 0: return "All";
220                    case 1: return "Random";
221                    case 2: return "Temp Depth [uniform, " + tempDepthThresh + "]";
222                    case 3: return "Temp Depth [weig, " + tempDepthThresh + "]";
223                    case 4: return "Level [uniform, " + levelThresh + "]";
224                    case 5: return "Level [weig, " + levelThresh + "]";
225                    case 6: return "Manual";
226                    default: return "";
227                    }
228            }
229            
230            public void setNumLeaves(int numLeaves) { 
231                    if (numLeaves < 0) {
232                            CrimsonUtils.printError("Invalid number of leaves, in Query.");
233                    } else {
234                            this.numLeaves = numLeaves; 
235                    }
236                    
237                    this.saved = false;  // potentially out of sync with db
238            }
239            
240            public int getNumLeaves() { return numLeaves; }
241            
242            public void setTempDepthThresh(double tempDepthThresh) { 
243                    if (tempDepthThresh < 0.0) {
244                            CrimsonUtils.printError("Invalid Temporal Depth, in Query.");
245                    } else {
246                            this.tempDepthThresh = tempDepthThresh;
247                    }
248                    
249                    this.saved = false;  // potentially out of sync with db
250            }
251            
252            public double getTempDepthThresh() { return tempDepthThresh; }
253            
254            public void setLevelThresh(int levelThresh) { 
255                    if (levelThresh < 0) {
256                            CrimsonUtils.printError("Invalid Level, in Query.");
257                    } else {
258                            this.levelThresh = levelThresh;
259                    }
260                    
261                    this.saved = false;  // potentially out of sync with db
262            }
263            
264            public int getLevelThresh() { return levelThresh; }
265            
266            public void setLeaves(HashSet leaves) { 
267                    // TEST *****
268                    //              if (leaves == null) leaves = new HashSet();
269                    this.leaves = leaves; 
270                    
271                    this.saved = false;  // potentially out of sync with db
272            }
273            
274            public HashSet getLeaves() { 
275                    // TEST *****
276                    if (leaves == null) leaves = new HashSet();
277                    return leaves; 
278            }
279            
280            public void setSequenceSelection(int sequenceSelection) { 
281                    if ((sequenceSelection < 0) || (sequenceSelection > 5)) {
282                            CrimsonUtils.printError("Invalid sequence selection method, in Query.");
283                    } else {
284                            this.sequenceSelection = sequenceSelection;
285                            
286                            // make sure onlyStruct is consistent
287                            if (sequenceSelection == 5) {
288                                    this.onlyStruct = true;
289                            } else {
290                                    this.onlyStruct = false;
291                            }
292                    }
293                    
294                    this.saved = false;  // potentially out of sync with db
295            }
296            
297            public int getSequenceSelection() { return sequenceSelection; }
298            
299            public String getSequenceSelectionString() {
300                    switch (sequenceSelection) {
301                    case 0: return "All";
302                    case 1: return "Random Codon (" + numPositions + ")";
303                    case 2: return "Random Base Pair (" + numPositions + ")";
304                    case 3: return "Manual Codon";
305                    case 4: return "Manual Base Pair";
306                    case 5: return "None";
307                    default: return "";
308                    }
309            }
310            
311            public void setNumPositions(int numPositions) { 
312                    if (numPositions < 0) {
313                            CrimsonUtils.printError("Invalid number of positions (bp/codon), in Query.");
314                    } else {
315                            this.numPositions = numPositions;
316                    }
317                    
318                    this.saved = false;  // potentially out of sync with db
319            }
320            
321            public int getNumPositions() { return numPositions; }
322            
323            public void setPartitions(HashSet partitions) { 
324                    // TEST *****
325                    //              if (partitions == null) partitions = new HashSet();
326                    this.partitions = partitions; 
327                    
328                    this.saved = false;  // potentially out of sync with db
329            }
330            
331            public HashSet getPartitions() { 
332                    // TEST *****
333                    if (partitions == null) partitions = new HashSet();
334                    return partitions; 
335            }
336            
337            public void setOnlyStruct(boolean onlyStruct) { 
338                    this.onlyStruct = onlyStruct; 
339                    
340                    // make sure sequence selection is consistent
341                    if (onlyStruct) {
342                            this.sequenceSelection = 5;
343                    }
344                    
345                    this.saved = false;  // potentially out of sync with db
346            }
347            
348            public boolean isOnlyStruct() { return onlyStruct; }
349            
350            public void setPositions(HashSet positions) { 
351                    // TEST *****
352                    //              if (positions == null) positions = new HashSet();
353                    this.positions = positions; 
354                    
355                    this.saved = false;  // potentially out of sync with db
356            }
357            
358            public HashSet getPositions() { 
359                    // TEST *****
360                    if (positions == null) positions = new HashSet();
361                    return positions; 
362            }
363            
364            public void setSeed(long seed) { 
365                    if (seed < -1) {
366                            CrimsonUtils.printError("Invalid random number seed, in Query.");
367                    } else {
368                            this.seed = seed;
369                    }
370                    
371                    this.saved = false;  // potentially out of sync with db
372            }
373            
374            public long getSeed() { return seed; }
375            
376            public void setSaved(boolean saved) { this.saved = saved; }
377            
378            public boolean isSaved() { return saved; }
379            
380        //--------------------------------------------------------------------------
381        // Miscellaneous Methods
382            
383            /** Adds a leaf to the leaf list. */
384            public void addLeaf(String item) { 
385                    // TEST *****
386                    if (leaves == null) leaves = new HashSet();
387                    leaves.add(item); 
388                    
389                    this.saved = false;  // potentially out of sync with db
390            }
391            
392            /** Adds a partitions to the partitions list. */
393            public void addPartition(String item) { 
394                    // TEST *****
395                    if (partitions == null) partitions = new HashSet();
396                    partitions.add(item.toUpperCase()); 
397                    
398                    this.saved = false;  // potentially out of sync with db
399            }
400            
401            /** Adds a positions to the positions list. */
402            public void addPosition(String item) { 
403                    // TEST *****
404                    if (positions == null) positions = new HashSet();
405                    positions.add(item); 
406                    
407                    this.saved = false;  // potentially out of sync with db
408            }
409            
410            /** Adds a ':' separated list of positions to the positions list. */
411            public void addPositions(String items) { 
412                    // TEST *****
413                    if (positions == null) positions = new HashSet();
414    
415                    StringTokenizer tokens = new StringTokenizer(items, ":");
416                    while (tokens.hasMoreTokens()) {
417                            String token = tokens.nextToken().trim();
418                            // this won't get the condition when 0 is the beginning of
419                            // a range
420                            if (token.compareTo("0") == 0) 
421                                    CrimsonUtils.printWarning("Position range begins at 1, not 0. This query will not run.");
422                            positions.add(token);
423                    }
424                    
425                    this.saved = false;  // potentially out of sync with db
426            }
427            
428            /**
429             * Create a shallow clone (just clone the structure, not the
430             * Objects) of the existing object. This will randomly assign the
431             * clone's ID, based on the ID for the parent object.
432             */
433            public Object clone() {
434                    return clone(ObjectHandles.randomQueryID(this.id + "_"));
435            }
436            
437            /**
438             * Create a shallow clone (just clone the structure, not the
439             * Objects) of the existing object.
440             * @XXX Note sure if this is correct, may need to use ".set()"
441             * methods to set the values of the clone.
442             */
443            public Object clone(String id) {
444                    Query queryItem = new Query(treeID, id);
445                    queryItem.leafSelection = this.leafSelection;
446                    queryItem.numLeaves = this.numLeaves;
447                    queryItem.tempDepthThresh = this.tempDepthThresh;
448                    queryItem.levelThresh = this.levelThresh;
449                    queryItem.leaves = this.leaves;
450                    queryItem.sequenceSelection = this.sequenceSelection;
451                    queryItem.numPositions = this.numPositions;
452                    queryItem.partitions = this.partitions;
453                    queryItem.positions = this.positions;
454                    queryItem.seed = this.seed;
455                    queryItem.notes = this.notes;
456                    queryItem.saved = false;
457                    return queryItem;
458            }
459            
460            /**
461             * This routine is only meant to be used by Queries when loading
462             * Queries from a database via a proxy. It may change, as
463             * needed to accommodate database loading. Queries is expected to
464             * contain the output from the following select statement: "SELECT
465             * id, tree_id, notes, leaf_select, num_leaves, temp_depth_thresh,
466             * level_thresh, seq_select, num_pos, seed, leaves, partitions,
467             * positions FROM QUERIES"
468             */
469            public static void dbLoadProxy(StringBuilder queries) {
470                    int i = 0;
471                    int iStart;
472                    int len = queries.length() - 1;
473                    Query query;
474                    String tmp;
475            while (i < len) {
476                            // get query ID
477                            iStart = i;
478                            while ((i < len) && (queries.charAt(i) != ';')) i++;
479                            tmp = queries.substring(iStart,i);
480                            i++;
481                            // get tree ID
482                            iStart = i;
483                            while ((i < len) && (queries.charAt(i) != ';')) i++;
484                            query = new Query(queries.substring(iStart,i), tmp);
485                            i++;
486                            // get notes
487                            iStart = i;
488                            while ((i < len) && (queries.charAt(i) != ';')) i++;
489                            query.notes = queries.substring(iStart,i);
490                            i++;
491                            // get leafSelection
492                            iStart = i;
493                            while ((i < len) && (queries.charAt(i) != ';')) i++;
494                            query.leafSelection = Integer.parseInt(queries.substring(iStart,i));
495                            i++;
496                            // get numLeaves
497                            iStart = i;
498                            while ((i < len) && (queries.charAt(i) != ';')) i++;
499                            query.numLeaves = Integer.parseInt(queries.substring(iStart,i));
500                            i++;
501                            // get tempDepthThresh
502                            iStart = i;
503                            while ((i < len) && (queries.charAt(i) != ';')) i++;
504                            query.tempDepthThresh = Double.parseDouble(queries.substring(iStart,i));
505                            i++;
506                            // get levelThresh
507                            iStart = i;
508                            while ((i < len) && (queries.charAt(i) != ';')) i++;
509                            query.levelThresh = Integer.parseInt(queries.substring(iStart,i));
510                            i++;
511                            // get sequenceSelection
512                            iStart = i;
513                            while ((i < len) && (queries.charAt(i) != ';')) i++;
514                            query.sequenceSelection = Integer.parseInt(queries.substring(iStart,i));
515                            i++;
516                            // get numPositions
517                            iStart = i;
518                            while ((i < len) && (queries.charAt(i) != ';')) i++;
519                            query.numPositions = Integer.parseInt(queries.substring(iStart,i));
520                            i++;
521                            // get seed
522                            iStart = i;
523                            while ((i < len) && (queries.charAt(i) != ';')) i++;
524                            query.seed = Long.parseLong(queries.substring(iStart,i));
525                            i++;
526    
527                            // get leaves (CLOB)
528                            iStart = i;
529                            while ((i < len) && (queries.charAt(i) != ';')) i++;
530                            // if not empty, then add leaves
531                            if (i > (iStart + 1)) query.leaves = new HashSet(Arrays.asList(queries.substring(iStart,i).split(",")));
532                            i++;
533    
534                            // get partitions (CLOB)
535                            iStart = i;
536                            while ((i < len) && (queries.charAt(i) != ';')) i++;
537                            // if not empty, then add leaves
538                            if (i > (iStart + 1)) query.partitions = new HashSet(Arrays.asList(queries.substring(iStart,i).split(",")));
539                            i++;
540    
541                            // get positions (CLOB)
542                            iStart = i;
543                            while ((i < len) && (queries.charAt(i) != ';')) i++;
544                            // if not empty, then add leaves
545                            if (i > (iStart + 1)) query.positions = new HashSet(Arrays.asList(queries.substring(iStart,i).split(",")));
546                            i++;
547    
548                            // since we just downloaded the query, we know it's saved
549                            query.saved = true;
550                    }
551            }
552            
553            /**
554             * This routine is only meant to be used by Queries when loading
555             * Queries from a database without using a proxy (ie CLOBs are not
556             * strings). It may change, as needed to accommodate database
557             * loading. qObj is expected to contain the output from the
558             * following select statement: "SELECT id, tree_id, notes,
559             * leaf_select, num_leaves, temp_depth_thresh, level_thresh,
560             * seq_select, num_pos, seed, leaves, partitions, positions FROM
561             * QUERIES"
562             */
563            public static Object dbLoad(ArrayList qObj) {
564                    Query query = new Query(String.valueOf(qObj.get(1)), String.valueOf(qObj.get(0)));
565                    query.notes = String.valueOf(qObj.get(2));
566                    query.leafSelection = Integer.parseInt(String.valueOf(qObj.get(3)));
567                    query.numLeaves = Integer.parseInt(String.valueOf(qObj.get(4)));
568                    query.tempDepthThresh = Double.parseDouble(String.valueOf(qObj.get(5)));
569                    query.levelThresh = Integer.parseInt(String.valueOf(qObj.get(6)));
570                    query.sequenceSelection = Integer.parseInt(String.valueOf(qObj.get(7)));
571                    query.numPositions = Integer.parseInt(String.valueOf(qObj.get(8)));
572                    query.seed = Long.parseLong(String.valueOf(qObj.get(9)));
573    
574                    try {
575                            query.setLeaves(Database.readClobSet(qObj.get(10)));
576                            query.setPartitions(Database.readClobSet(qObj.get(11)));
577                            query.setPositions(Database.readClobSet(qObj.get(12)));
578                            /*
579                            if (! CrimsonUtils.isEmpty(qObj.get(10).toString())) {
580                                    Clob clob = (Clob) qObj.get(10);
581                                    //if ((clob != null) && (clob.length() > 0)) {
582                                    // TEST *****
583                                    query.leaves = new HashSet();
584                                    StringBuffer sb = new StringBuffer();
585                                    Reader reader = clob.getCharacterStream();
586                                    BufferedReader bReader = new BufferedReader(reader);
587                                    String line;
588                                    while ((line = bReader.readLine()) != null) sb.append(line);
589                                    // query.leaves = new HashSet(Arrays.asList(sb.toString().split(",")));
590                                    Scanner scanner = new Scanner(sb.toString()).useDelimiter(",");
591                                    while (scanner.hasNext()) query.leaves.add(scanner.next().trim());
592                                    
593                                    // close the reader
594                                    bReader.close();
595                                    reader.close();
596                            }
597                            
598                            if (! CrimsonUtils.isEmpty(qObj.get(11).toString())) {
599                                    Clob clob = (Clob) qObj.get(11);
600                                    //if ((clob != null) && (clob.length() > 0)) {
601                                    // TEST *****
602                                    query.partitions = new HashSet();
603                                    StringBuffer sb = new StringBuffer();
604                                    Reader reader = clob.getCharacterStream();
605                                    BufferedReader bReader = new BufferedReader(reader);
606                                    String line;
607                                    while ((line = bReader.readLine()) != null) sb.append(line);
608                                    Scanner scanner = new Scanner(sb.toString()).useDelimiter(",");
609                                    while (scanner.hasNext()) query.partitions.add(scanner.next().trim());
610                                    
611                                    // close the reader
612                                    bReader.close();
613                                    reader.close();
614                            }
615                            
616                            if (! CrimsonUtils.isEmpty(qObj.get(12).toString())) {
617                                    Clob clob = (Clob) qObj.get(12);
618                                    //if ((clob != null) && (clob.length() > 0)) {
619                                    // TEST *****
620                                    query.positions = new HashSet();
621                                    StringBuffer sb = new StringBuffer();
622                                    Reader reader = clob.getCharacterStream();
623                                    BufferedReader bReader = new BufferedReader(reader);
624                                    String line;
625                                    while ((line = bReader.readLine()) != null) sb.append(line);
626                                    Scanner scanner = new Scanner(sb.toString()).useDelimiter(",");
627                                    while (scanner.hasNext()) query.positions.add(scanner.next().trim());
628                                    
629                                    // close the reader
630                                    bReader.close();
631                                    reader.close();
632                            }
633                            */
634                    } catch (Exception e) {
635                            CrimsonUtils.printError("Error loading Query CLOBs.");
636                            CrimsonUtils.printError(e.getMessage());
637                            return null;
638                    }
639                    
640                    // since we just downloaded the query, we know it's saved
641                    query.saved = true;
642                    
643                    return query;
644            }
645            
646            /**
647             * Creates an array with the fields in the following order: id,
648             * leafSelection, numLeaves, tempDepthThresh, levelThresh, leaves,
649             * sequenceSelection, numPositions, partitions, positions,
650             * seed, treeID, notes.
651             */
652            /*
653              public Object[] toArray() {
654              Object[] out = new Object[12];
655              out[0] = id;
656              out[1] = Integer.toString(leafSelection);
657              out[2] = Integer.toString(numLeaves);
658              out[3] = Double.toString(tempDepthThresh);
659              out[4] = Integer.toString(levelThresh);
660              out[5] = leaves;
661              out[6] = Integer.toString(sequenceSelection);
662              out[7] = Integer.toString(numPositions);
663              out[8] = partitions;
664              out[9] = positions;
665              out[10] = Double.toString(seed);
666              out[11] = treeID;
667              out[12] = notes;
668              return out;
669              }
670            */
671            
672            /** Returns Query information for debugging purposes. */
673            public String toString() {
674                    String out = "";
675                    out += " ID: " + id + "\n";
676                    out += " Tree ID: " + treeID + "\n";
677                    out += " Leaf Selection: " + getLeafSelectionString() + "\n";
678                    out += " Number Leaves: " + Integer.toString(numLeaves) + "\n";
679                    out += " Temporal Depth Thresh: " + Double.toString(tempDepthThresh) + "\n";
680                    out += " Level Thresh: " + Integer.toString(levelThresh) + "\n";
681                    out += " Leaves: " + leaves + "\n";
682                    out += " Sequence Selection: " + getSequenceSelectionString() + "\n";
683                    out += " Number Positions (bp/codon): " + Integer.toString(numPositions) + "\n";
684                    out += " Partitions: " + partitions + "\n";
685                    out += " Positions (bp/codon): " + positions + "\n";
686                    out += " Seed: " + Long.toString(seed) + "\n";
687                    out += " Notes: " + notes + "\n";
688                    out += " Saved: " + saved + "\n";
689                    return out;
690            }
691    } // Query.java