bc732e27859065cd77ccdd9dfb8e3d76f3ceb1b7
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLGenerator.java
1 // $Id: CQLGenerator.java,v 1.1 2002-10-30 09:19:26 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.1 2002-10-30 09:19:26 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         String qualifier = generate_qualifier();
158         String relation = generate_relation();
159         String term = generate_term();
160
161         return new CQLTermNode(qualifier, relation, term);
162     }
163
164     // ### Should probably be more configurable
165     private String generate_qualifier() {
166         String qualifier = "";  // shut up compiler warning
167         if (rnd.nextInt(2) == 0) {
168             switch (rnd.nextInt(3)) {
169             case 0: qualifier = "dc.author"; break;
170             case 1: qualifier = "dc.title"; break;
171             case 2: qualifier = "dc.subject"; break;
172             }
173         } else {
174             switch (rnd.nextInt(4)) {
175             case 0: qualifier = "bath.author"; break;
176             case 1: qualifier = "bath.title"; break;
177             case 2: qualifier = "bath.subject"; break;
178             case 3: qualifier = "foo>bar"; break;
179             }
180         }
181
182         return qualifier;
183     }
184
185     // ### Representation of relations will change when we handle modifiers
186     private String generate_relation() throws ParameterMissingException {
187         return generate_base_relation();
188         // ### should generate modifiers too
189     }
190
191     private String generate_base_relation() throws ParameterMissingException {
192         if (maybe("equalsRelation")) {
193             return "=";
194         } else if (maybe("numericRelation")) {
195             return generate_numeric_relation();
196         } else {
197             switch (rnd.nextInt(3)) {
198             case 0: return "exact";
199             case 1: return "all";
200             case 2: return "any";
201             }
202         }
203
204         // NOTREACHED
205         return "";              // shut up compiler warning
206     }
207
208     // ### could read candidate terms from /usr/dict/words
209     // ### should introduce wildcard characters
210     // ### should generate multi-word terms
211     private String generate_term() {
212         switch (rnd.nextInt(10)) {
213         case 0: return "cat";
214         case 1: return "\"cat\"";
215         case 2: return "comp.os.linux";
216         case 3: return "xml:element";
217         case 4: return "<xml.element>";
218         case 5: return "prox/word/>=/5";
219         case 6: return "";
220         case 7: return "frog fish";
221         case 8: return "the complete dinosaur";
222         case 9: return "foo*bar";
223         }
224
225         // NOTREACHED
226         return "";              // shut up compiler warning
227     }
228
229     private String generate_numeric_relation() {
230         switch (rnd.nextInt(6)) {
231         case 0: return "<";
232         case 1: return ">";
233         case 2: return "<=";
234         case 3: return ">=";
235         case 4: return "<>";
236         case 5: return "=";
237         }
238
239         // NOTREACHED
240         return "";              // shut up compiler warning
241     }
242
243     boolean maybe(String param) throws ParameterMissingException {
244         String probability = params.getProperty(param);
245         if (probability == null)
246             throw new ParameterMissingException(param);
247
248         double dice = rnd.nextDouble();
249         double threshhold = new Double(probability).doubleValue();
250         boolean res = dice < threshhold;
251         debug("dice=" + String.valueOf(dice).substring(0, 8) +
252               " vs. " + threshhold + "='" + param + "': " + res);
253         return res;
254     }   
255
256
257     /**
258      * A simple test-harness for the generator.
259      * <P>
260      * It generates a single random query using the parameters
261      * specified in a nominated properties file, and decompiles it
262      * into CQL which is written to standard output.
263      * <P>
264      * For example,
265      * <TT>java org.z3950.zing.cql.CQLGenerator etc/generate.properties</TT>
266      * where the file <TT>generate.properties</TT> contains:<PRE>
267      *  seed=18398
268      *  complexQuery=0.4
269      *  complexClause=0.4
270      *  equalsRelation=0.5
271      *  numericRelation=0.7
272      *  proxOp=0.0
273      * </PRE>
274      * yields:<PRE>
275      *  ((dc.author = "&lt;xml.element&gt;") or (bath.title = cat)) and
276      *          (dc.subject &gt;= "the complete dinosaur")
277      * </PRE>
278      * <P>
279      * @param configFile
280      *  The name of a properties file from which to read the
281      *  configuration parameters (see above).
282      * @return
283      *  A CQL query expressed in a form that should be comprehensible
284      *  to all conformant CQL compilers.
285      */
286     public static void main (String[] args) throws Exception {
287         if (args.length != 1) {
288             System.err.println("Usage: CQLGenerator <props-file>");
289             System.exit(1);
290         }
291
292         String configFile = args[0];
293         InputStream f = new FileInputStream(configFile);
294         if (f == null)
295             throw new FileNotFoundException("getResourceAsStream(" +
296                                             configFile + ")");
297
298         Properties params = new Properties();
299         params.load(f);
300         f.close();
301
302         CQLGenerator generator = new CQLGenerator(params);
303         CQLNode tree = generator.generate();
304         System.out.println(tree.toCQL());
305     }
306 }