991f60dbc535636273af256a8692d4f9bb6f6ef5
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLGenerator.java
1 // $Id: CQLGenerator.java,v 1.3 2002-11-03 16:49:38 mike Exp $
2
3 package org.z3950.zing.cql;
4 import java.util.Properties;
5 import java.util.Random;
6 import java.io.InputStream;
7 import java.io.FileInputStream;
8 import java.io.FileNotFoundException;
9
10
11 /**
12  * A generator that produces random CQL queries.
13  * <P>
14  * Why is that useful?  Mainly to produce test-cases for CQL parsers
15  * (including the <TT>CQLParser</TT> class in this package): you can
16  * generate a random search tree, render it to XCQL and remember the
17  * result.  Then decompile the tree to CQL, feed the generated CQL to
18  * the parser of your choice, and check that the XCQL it comes up with
19  * is the same what you got from your initial rendering.
20  * <P>
21  * This code is based on the grammar in the <TT>Grammar</TT> file of
22  * this distribution - there is a <TT>generate_<I>x</I>()</TT> method
23  * for each grammar element <I>X</I>.
24  *
25  * @version     $Id: CQLGenerator.java,v 1.3 2002-11-03 16:49:38 mike Exp $
26  * @see         <A href="http://zing.z3950.org/cql/index.html"
27  *                      >http://zing.z3950.org/cql/index.html</A>
28  */
29 public class CQLGenerator {
30     Properties params;
31     Random rnd;
32     static private boolean DEBUG = false;
33
34     /**
35      * Creates a new CQL generator with the specified parameters.
36      * <P>
37      * @param params
38      *  A <TT>Properties</TT> table containing configuration
39      *  parameters for the queries to be generated by this generator.
40      *  Recognised parameters are:
41      *  <P>
42      *  <DL>
43      *   <DT><TT>seed</TT></DT>
44      *   <DD>
45      *    If specified, this is a <TT>long</TT> used to seed the
46      *    random number generator, so that the CQL generator can be
47      *    run repeatably, giving the same results each time.  If it's
48      *    omitted, then no seed is explicitly specified, and the
49      *    results of each run will be different (so long as you don't
50      *    run it more that 2^32 times :-)
51      *    <P>
52      *   </DD>
53      *   <DT><TT>complexQuery</TT></DT>
54      *   <DD>
55      *    [mandatory] A floating-point number between 0.0 and 1.0,
56      *    indicating the probability for each <TT>cql-query</TT> node
57      *    that it will be expanded into a ``complex query''
58      *    (<TT>cql-query&nbsp;boolean&nbsp;search-clause</TT>) rather
59      *    than a <TT>search-clause</TT>.
60      *    <P>
61      *   </DD>
62      *   <DT><TT>complexClause</TT></DT>
63      *   <DD>
64      *    [mandatory] A floating-point number between 0.0 and 1.0,
65      *    indicating the probability for each <TT>search-clause</TT>
66      *    node that it will be expanded into a full sub-query rather
67      *    than a <TT>[ qualifier relation ] term</TT> triplet.
68      *    <P>
69      *   </DD>
70      *   <DT><TT>proxOp</TT></DT>
71      *   <DD>
72      *    [mandatory] A floating-point number between 0.0 and 1.0,
73      *    indicating the probability that each boolean operator will
74      *    be chosen to be proximity operation; otherwise, the three
75      *    simpler boolean operations (<TT>and</TT>, <TT>or</TT> and
76      *    <TT>not</TT>) are chosen with equal probability.
77      *    <P>
78      *   </DD>
79      *   <DT><TT>equalsRelation</TT></DT>
80      *   <DD>
81      *    [mandatory] A floating-point number between 0.0 and 1.0,
82      *    indicating the probability that each relation will be chosen
83      *    to be <TT>=</TT> - this is treated as a special case, since
84      *    it's likely to be by far the most common relation in
85      *    ``real life'' searches.
86      *    <P>
87      *   </DD>
88      *   <DT><TT>numericRelation</TT></DT>
89      *   <DD>
90      *    [mandatory] A floating-point number between 0.0 and 1.0,
91      *    indicating the probability that a relation, having chosen
92      *    not to be <TT>=</TT>, is instead chosen to be one of the six
93      *    numeric relations (<TT>&lt;</TT>, <TT>&gt;</TT>,
94      *    <TT>&lt;=</TT>, <TT>&gt;=</TT>, <TT>&lt;&gt;</TT> and
95      *    <TT>=</TT>).
96      *    <P>
97      *   </DD>
98      *  </DL>
99      */
100     public CQLGenerator(Properties params) {
101         this.params = params;
102         String seed = params.getProperty("seed");
103         if (seed != null)
104             rnd = new Random(new Long(seed).longValue());
105         else
106             rnd = new Random();
107     }
108
109     private static void debug(String str) {
110         if (DEBUG)
111             System.err.println("DEBUG: " + str);
112     }
113
114     /** 
115      * Generates a single random CQL query.
116      * <P>
117      * Uses the parameters that were associated with the generator
118      * when it was created.  You are free to create as many random
119      * queries as you wish from a single generator; each of them will
120      * use the same parameters.
121      * <P>
122      * @return
123      *  A <TT>CQLNode</TT> that is the root of the generated tree.
124      *  That tree may be rendered in XCQL using its <TT>toXCQL()</TT>
125      *  method, or decompiled into CQL using its <TT>toCQL</TT>
126      *  method.
127      */
128     public CQLNode generate() throws ParameterMissingException {
129         return generate_cql_query();
130     }
131
132     private CQLNode generate_cql_query() throws ParameterMissingException {
133         if (!maybe("complexQuery")) {
134             return generate_search_clause();
135         }
136
137         CQLNode node1 = generate_cql_query();
138         CQLNode node2 = generate_search_clause();
139         if (maybe("proxOp")) {
140             // ### generate proximity nodes
141         } else {
142             switch (rnd.nextInt(3)) {
143             case 0: return new CQLAndNode(node1, node2);
144             case 1: return new CQLOrNode(node1, node2);
145             case 2: return new CQLNotNode(node1, node2);
146             }
147         }
148
149         return generate_search_clause();
150     }
151
152     private CQLNode generate_search_clause() throws ParameterMissingException {
153         if (maybe("complexClause")) {
154             return generate_cql_query();
155         }
156
157         // ### Should sometimes generate qualifier/relation-free terms
158         String qualifier = generate_qualifier();
159         CQLRelation relation = generate_relation();
160         String term = generate_term();
161
162         return new CQLTermNode(qualifier, relation, term);
163     }
164
165     // ### Should probably be more configurable
166     private String generate_qualifier() {
167         String qualifier = "";  // shut up compiler warning
168         if (rnd.nextInt(2) == 0) {
169             switch (rnd.nextInt(3)) {
170             case 0: qualifier = "dc.author"; break;
171             case 1: qualifier = "dc.title"; break;
172             case 2: qualifier = "dc.subject"; break;
173             }
174         } else {
175             switch (rnd.nextInt(4)) {
176             case 0: qualifier = "bath.author"; break;
177             case 1: qualifier = "bath.title"; break;
178             case 2: qualifier = "bath.subject"; break;
179             case 3: qualifier = "foo>bar"; break;
180             }
181         }
182
183         return qualifier;
184     }
185
186     private CQLRelation generate_relation() throws ParameterMissingException {
187         String base = generate_base_relation();
188         CQLRelation rel = new CQLRelation(base);
189         // ### should generate modifiers too
190         return rel;
191     }
192
193     private String generate_base_relation() throws ParameterMissingException {
194         if (maybe("equalsRelation")) {
195             return "=";
196         } else if (maybe("numericRelation")) {
197             return generate_numeric_relation();
198         } else {
199             switch (rnd.nextInt(3)) {
200             case 0: return "exact";
201             case 1: return "all";
202             case 2: return "any";
203             }
204         }
205
206         // NOTREACHED
207         return "";              // shut up compiler warning
208     }
209
210     // ### could read candidate terms from /usr/dict/words
211     // ### should introduce wildcard characters
212     // ### should generate multi-word terms
213     private String generate_term() {
214         switch (rnd.nextInt(10)) {
215         case 0: return "cat";
216         case 1: return "\"cat\"";
217         case 2: return "comp.os.linux";
218         case 3: return "xml:element";
219         case 4: return "<xml.element>";
220         case 5: return "prox/word/>=/5";
221         case 6: return "";
222         case 7: return "frog fish";
223         case 8: return "the complete dinosaur";
224         case 9: return "foo*bar";
225         }
226
227         // NOTREACHED
228         return "";              // shut up compiler warning
229     }
230
231     private String generate_numeric_relation() {
232         switch (rnd.nextInt(6)) {
233         case 0: return "<";
234         case 1: return ">";
235         case 2: return "<=";
236         case 3: return ">=";
237         case 4: return "<>";
238         case 5: return "=";
239         }
240
241         // NOTREACHED
242         return "";              // shut up compiler warning
243     }
244
245     boolean maybe(String param) throws ParameterMissingException {
246         String probability = params.getProperty(param);
247         if (probability == null)
248             throw new ParameterMissingException(param);
249
250         double dice = rnd.nextDouble();
251         double threshhold = new Double(probability).doubleValue();
252         boolean res = dice < threshhold;
253         debug("dice=" + String.valueOf(dice).substring(0, 8) +
254               " vs. " + threshhold + "='" + param + "': " + res);
255         return res;
256     }   
257
258
259     /**
260      * A simple test-harness for the generator.
261      * <P>
262      * It generates a single random query using the parameters
263      * specified in a nominated properties file, plus any additional
264      * <I>name value</I> pairs provided on the command-line, and
265      * decompiles it into CQL which is written to standard output.
266      * <P>
267      * For example,
268      * <TT>java org.z3950.zing.cql.CQLGenerator
269      *  etc/generate.properties seed 18398</TT>,
270      * where the file <TT>generate.properties</TT> contains:<PRE>
271      *  complexQuery=0.4
272      *  complexClause=0.4
273      *  equalsRelation=0.5
274      *  numericRelation=0.7
275      *  proxOp=0.0
276      * </PRE>
277      * yields:<PRE>
278      *  ((dc.author = "&lt;xml.element&gt;") or (bath.title = cat)) and
279      *          (dc.subject &gt;= "the complete dinosaur")
280      * </PRE>
281      * <P>
282      * @param configFile
283      *  The name of a properties file from which to read the
284      *  configuration parameters (see above).
285      * @param name
286      *  The name of a configuration parameter.
287      * @param value
288      *  The value to assign to the configuration parameter named in
289      *  the immediately preceding command-line argument.
290      * @return
291      *  A CQL query expressed in a form that should be comprehensible
292      *  to all conformant CQL compilers.
293      */
294     public static void main (String[] args) throws Exception {
295         if (args.length % 2 != 1) {
296             System.err.println("Usage: CQLGenerator <props-file> "+
297                                "[<name> <value>]...");
298             System.exit(1);
299         }
300
301         String configFile = args[0];
302         InputStream f = new FileInputStream(configFile);
303         if (f == null)
304             throw new FileNotFoundException("getResourceAsStream(" +
305                                             configFile + ")");
306
307         Properties params = new Properties();
308         params.load(f);
309         f.close();
310         for (int i = 1; i < args.length; i += 2)
311             params.setProperty(args[i], args[i+1]);
312
313         CQLGenerator generator = new CQLGenerator(params);
314         CQLNode tree = generator.generate();
315         System.out.println(tree.toCQL());
316     }
317 }