cc8ac148843810fd018ef04f2cb5201b4ac30935
[cql-java-moved-to-github.git] / src / main / java / org / z3950 / zing / cql / CQLGenerator.java
1 // $Id: CQLGenerator.java,v 1.9 2007-07-03 15:41:35 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 same grammar as the <TT>CQLParser</TT> class in
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.9 2007-07-03 15:41:35 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 an <TT>[ index 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 MissingParameterException {
129         return generate_cql_query();
130     }
131
132     private CQLNode generate_cql_query() throws MissingParameterException {
133         if (!maybe("complexQuery")) {
134             return generate_search_clause();
135         }
136
137         CQLNode node1 = generate_cql_query();
138         CQLNode node2 = generate_search_clause();
139         // ### should generate prefix-mapping nodes
140         if (maybe("proxOp")) {
141             // ### generate proximity nodes
142         } else {
143             switch (rnd.nextInt(3)) {
144             case 0: return new CQLAndNode(node1, node2, new ModifierSet("and"));
145             case 1: return new CQLOrNode (node1, node2, new ModifierSet("or"));
146             case 2: return new CQLNotNode(node1, node2, new ModifierSet("not"));
147             }
148         }
149
150         return generate_search_clause();
151     }
152
153     private CQLNode generate_search_clause() throws MissingParameterException {
154         if (maybe("complexClause")) {
155             return generate_cql_query();
156         }
157
158         // ### Should sometimes generate index/relation-free terms
159         String index = generate_index();
160         CQLRelation relation = generate_relation();
161         String term = generate_term();
162
163         return new CQLTermNode(index, relation, term);
164     }
165
166     // ### Should probably be more configurable
167     private String generate_index() {
168         String index = "";      // shut up compiler warning
169         if (rnd.nextInt(2) == 0) {
170             switch (rnd.nextInt(3)) {
171             case 0: index = "dc.author"; break;
172             case 1: index = "dc.title"; break;
173             case 2: index = "dc.subject"; break;
174             }
175         } else {
176             switch (rnd.nextInt(4)) {
177             case 0: index = "bath.author"; break;
178             case 1: index = "bath.title"; break;
179             case 2: index = "bath.subject"; break;
180             case 3: index = "foo>bar"; break;
181             }
182         }
183
184         return index;
185     }
186
187     private CQLRelation generate_relation() throws MissingParameterException {
188         String base = generate_base_relation();
189         CQLRelation rel = new CQLRelation(base);
190         // ### should generate modifiers too
191         return rel;
192     }
193
194     private String generate_base_relation() throws MissingParameterException {
195         if (maybe("equalsRelation")) {
196             return "=";
197         } else if (maybe("numericRelation")) {
198             return generate_numeric_relation();
199         } else {
200             switch (rnd.nextInt(3)) {
201             case 0: return "within";
202             case 1: return "all";
203             case 2: return "any";
204             }
205         }
206
207         // NOTREACHED
208         return "";              // shut up compiler warning
209     }
210
211     // ### could read candidate terms from /usr/dict/words
212     // ### should introduce wildcard characters
213     // ### should generate multi-word terms
214     private String generate_term() {
215         switch (rnd.nextInt(10)) {
216         case 0: return "cat";
217         case 1: return "\"cat\"";
218         case 2: return "comp.os.linux";
219         case 3: return "xml:element";
220         case 4: return "<xml.element>";
221         case 5: return "prox/word/>=/5";
222         case 6: return "";
223         case 7: return "frog fish";
224         case 8: return "the complete dinosaur";
225         case 9: return "foo*bar";
226         }
227
228         // NOTREACHED
229         return "";              // shut up compiler warning
230     }
231
232     private String generate_numeric_relation() {
233         switch (rnd.nextInt(6)) {
234         case 0: return "<";
235         case 1: return ">";
236         case 2: return "<=";
237         case 3: return ">=";
238         case 4: return "<>";
239         case 5: return "=";
240         }
241
242         // NOTREACHED
243         return "";              // shut up compiler warning
244     }
245
246     boolean maybe(String param) throws MissingParameterException {
247         String probability = params.getProperty(param);
248         if (probability == null)
249             throw new MissingParameterException(param);
250
251         double dice = rnd.nextDouble();
252         double threshhold = new Double(probability).doubleValue();
253         boolean res = dice < threshhold;
254         debug("dice=" + String.valueOf(dice).substring(0, 8) +
255               " vs. " + threshhold + "='" + param + "': " + res);
256         return res;
257     }   
258
259
260     /**
261      * A simple test-harness for the generator.
262      * <P>
263      * It generates a single random query using the parameters
264      * specified in a nominated properties file, plus any additional
265      * <I>name value</I> pairs provided on the command-line, and
266      * decompiles it into CQL which is written to standard output.
267      * <P>
268      * For example,
269      * <TT>java org.z3950.zing.cql.CQLGenerator
270      *  etc/generate.properties seed 18398</TT>,
271      * where the file <TT>generate.properties</TT> contains:<PRE>
272      *  complexQuery=0.4
273      *  complexClause=0.4
274      *  equalsRelation=0.5
275      *  numericRelation=0.7
276      *  proxOp=0.0
277      * </PRE>
278      * yields:<PRE>
279      *  ((dc.author = "&lt;xml.element&gt;") or (bath.title = cat)) and
280      *          (dc.subject &gt;= "the complete dinosaur")
281      * </PRE>
282      * <P>
283      * @param configFile
284      *  The name of a properties file from which to read the
285      *  configuration parameters (see above).
286      * @param name
287      *  The name of a configuration parameter.
288      * @param value
289      *  The value to assign to the configuration parameter named in
290      *  the immediately preceding command-line argument.
291      * @return
292      *  A CQL query expressed in a form that should be comprehensible
293      *  to all conformant CQL compilers.
294      */
295     public static void main (String[] args) throws Exception {
296         if (args.length % 2 != 1) {
297             System.err.println("Usage: CQLGenerator <props-file> "+
298                                "[<name> <value>]...");
299             System.exit(1);
300         }
301
302         String configFile = args[0];
303         InputStream f = new FileInputStream(configFile);
304         if (f == null)
305             throw new FileNotFoundException(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 }