001package org.cpsolver.ifs.util; 002 003import java.io.Serializable; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.List; 007 008/** 009 * A representation of a boolean query. Besides of AND, OR, and NOT, the query 010 * may also contain terms (such as major:M1) that are evaluated by the {@link TermMatcher}. 011 * 012 * @version IFS 1.3 (Iterative Forward Search)<br> 013 * Copyright (C) 2024 Tomáš Müller<br> 014 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 015 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 016 * <br> 017 * This library is free software; you can redistribute it and/or modify 018 * it under the terms of the GNU Lesser General Public License as 019 * published by the Free Software Foundation; either version 3 of the 020 * License, or (at your option) any later version. <br> 021 * <br> 022 * This library is distributed in the hope that it will be useful, but 023 * WITHOUT ANY WARRANTY; without even the implied warranty of 024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 025 * Lesser General Public License for more details. <br> 026 * <br> 027 * You should have received a copy of the GNU Lesser General Public 028 * License along with this library; if not see 029 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 030 */ 031public class Query implements Serializable { 032 private static final long serialVersionUID = 1L; 033 034 private Term iQuery = null; 035 036 public Query(String query) { 037 iQuery = parse(query == null ? "" : query.trim()); 038 } 039 040 public Query(Term query) { 041 iQuery = query; 042 } 043 044 public Term getQuery() { return iQuery; } 045 046 /** 047 * Evaluate if the query using the provided term matcher. 048 */ 049 public boolean match(TermMatcher m) { 050 return iQuery.match(m); 051 } 052 053 /** 054 * Evaluate if the query using the provided term matcher. 055 */ 056 057 public boolean match(AmbigousTermMatcher m) { 058 Boolean ret = iQuery.match(m); 059 if (ret == null) return true; 060 return ret; 061 } 062 063 @Override 064 public String toString() { 065 return iQuery.toString(); 066 } 067 068 public String toString(QueryFormatter f) { 069 return iQuery.toString(f); 070 } 071 072 public boolean hasAttribute(String... attr) { 073 for (String a: attr) 074 if (iQuery.hasAttribute(a)) return true; 075 return false; 076 } 077 078 public boolean hasAttribute(Collection<String> attr) { 079 for (String a: attr) 080 if (iQuery.hasAttribute(a)) return true; 081 return false; 082 } 083 084 private static List<String> split(String query, String... splits) { 085 List<String> ret = new ArrayList<String>(); 086 int bracket = 0; 087 boolean quot = false; 088 int last = 0; 089 boolean white = false; 090 loop: for (int i = 0; i < query.length(); i++) { 091 if (query.charAt(i) == '"') { 092 quot = !quot; 093 white = !quot; 094 continue; 095 } 096 if (!quot && query.charAt(i) == '(') { bracket ++; white = false; continue; } 097 if (!quot && query.charAt(i) == ')') { bracket --; white = true; continue; } 098 if (quot || bracket > 0 || (!white && query.charAt(i) != ' ')) { 099 white = (query.charAt(i) == ' '); 100 continue; 101 } 102 white = (query.charAt(i) == ' '); 103 String q = query.substring(i).toLowerCase(); 104 for (String split: splits) { 105 if (split.isEmpty() || q.startsWith(split + " ") || q.startsWith(split + "\"") || q.startsWith(split + "(")) { 106 String x = query.substring(last, i).trim(); 107 if (split.isEmpty() && x.endsWith(":")) continue; 108 if (!x.isEmpty()) ret.add(x); 109 last = i + split.length(); 110 if (!split.isEmpty()) 111 i += split.length() - 1; 112 continue loop; 113 } 114 } 115 } 116 String x = query.substring(last).trim(); 117 if (!x.isEmpty()) ret.add(x); 118 return ret; 119 } 120 121 private static Term parse(String query) { 122 List<String> splits; 123 splits = split(query, "and", "&&", "&"); 124 if (splits.size() > 1) { 125 CompositeTerm t = new AndTerm(); 126 for (String q: splits) 127 t.add(parse(q)); 128 return t; 129 } 130 splits = split(query, "or", "||", "|"); 131 if (splits.size() > 1) { 132 CompositeTerm t = new OrTerm(); 133 for (String q: splits) 134 t.add(parse(q)); 135 return t; 136 } 137 splits = split(query, ""); 138 if (splits.size() > 1) { 139 CompositeTerm and = new AndTerm(); 140 boolean not = false; 141 splits: for (String q: splits) { 142 if (q.equalsIgnoreCase("not") || q.equals("!")) { not = true; continue; } 143 if (q.startsWith("!(")) { 144 q = q.substring(1); not = true; 145 } else if (q.toLowerCase().startsWith("not(")) { 146 q = q.substring(3); not = true; 147 } 148 if (not) { 149 and.add(new NotTerm(parse(q))); 150 not = false; 151 } else { 152 Term t = parse(q); 153 if (t instanceof AtomTerm) { 154 AtomTerm a = (AtomTerm)t; 155 for (Term x: and.terms()) { 156 if (x instanceof AtomTerm && ((AtomTerm)x).sameAttribute(a)) { 157 and.remove(x); 158 OrTerm or = new OrTerm(); 159 or.add(x); or.add(a); 160 and.add(or); 161 continue splits; 162 } else if (x instanceof OrTerm && ((OrTerm)x).terms().get(0) instanceof AtomTerm && ((AtomTerm)((OrTerm)x).terms().get(0)).sameAttribute(a)) { 163 ((OrTerm)x).terms().add(a); 164 continue splits; 165 } 166 } 167 } 168 and.add(t); 169 } 170 } 171 return and; 172 } 173 if (query.startsWith("(") && query.endsWith(")")) return parse(query.substring(1, query.length() - 1).trim()); 174 if (query.startsWith("\"") && query.endsWith("\"") && query.length() >= 2) return new AtomTerm(null, query.substring(1, query.length() - 1).trim()); 175 int idx = query.indexOf(':'); 176 if (idx >= 0) { 177 return new AtomTerm(query.substring(0, idx).trim().toLowerCase(), query.substring(idx + 1).trim()); 178 } else { 179 return new AtomTerm(null, query); 180 } 181 } 182 183 /** 184 * Representation of a term in the query, that is attribute:value. Or a sub-query. 185 */ 186 public static interface Term extends Serializable { 187 /** 188 * Does the term matches the provided object 189 */ 190 public boolean match(TermMatcher m); 191 /** 192 * Print the term/sub-query 193 */ 194 public String toString(QueryFormatter f); 195 /** 196 * Check if this term (or its sub-terms) contain a given attribute 197 */ 198 public boolean hasAttribute(String attribute); 199 /** 200 * Does the term matches the provided object, returning null if does not apply (e.g., the object does not have the provided attribute) 201 */ 202 public Boolean match(AmbigousTermMatcher m); 203 } 204 205 /** 206 * Composition of one or more terms in a query (e.g., A and B and C) 207 */ 208 public static abstract class CompositeTerm implements Term { 209 private static final long serialVersionUID = 1L; 210 private List<Term> iTerms = new ArrayList<Term>(); 211 212 public CompositeTerm() {} 213 214 public CompositeTerm(Term... terms) { 215 for (Term t: terms) add(t); 216 } 217 218 public CompositeTerm(Collection<Term> terms) { 219 for (Term t: terms) add(t); 220 } 221 222 /** 223 * Add a term 224 */ 225 public void add(Term t) { iTerms.add(t); } 226 227 /** 228 * Remove a term 229 */ 230 public void remove(Term t) { iTerms.remove(t); } 231 232 /** List terms */ 233 protected List<Term> terms() { return iTerms; } 234 235 /** Operation over the terms, such as AND or OR */ 236 public abstract String getOp(); 237 238 @Override 239 public boolean hasAttribute(String attribute) { 240 for (Term t: terms()) 241 if (t.hasAttribute(attribute)) return true; 242 return false; 243 } 244 245 @Override 246 public String toString() { 247 String ret = ""; 248 for (Term t: terms()) { 249 if (!ret.isEmpty()) ret += " " + getOp() + " "; 250 ret += t; 251 } 252 return (terms().size() > 1 ? "(" + ret + ")" : ret); 253 } 254 255 @Override 256 public String toString(QueryFormatter f) { 257 String ret = ""; 258 for (Term t: terms()) { 259 if (!ret.isEmpty()) ret += " " + getOp() + " "; 260 ret += t.toString(f); 261 } 262 return (terms().size() > 1 ? "(" + ret + ")" : ret); 263 } 264 } 265 266 /** 267 * Representation of an OR between two or more terms. 268 */ 269 public static class OrTerm extends CompositeTerm { 270 private static final long serialVersionUID = 1L; 271 public OrTerm() { super(); } 272 public OrTerm(Term... terms) { super(terms); } 273 public OrTerm(Collection<Term> terms) { super(terms); } 274 275 @Override 276 public String getOp() { return "OR"; } 277 278 /** A term matches, when at least one sub-term matches */ 279 @Override 280 public boolean match(TermMatcher m) { 281 if (terms().isEmpty()) return true; 282 for (Term t: terms()) 283 if (t.match(m)) return true; 284 return false; 285 } 286 287 @Override 288 public Boolean match(AmbigousTermMatcher m) { 289 if (terms().isEmpty()) return true; 290 for (Term t: terms()) { 291 Boolean r = t.match(m); 292 if (r == null) return null; 293 if (r) return true; 294 } 295 return false; 296 } 297 } 298 299 /** 300 * Representation of an AND between two or more terms. 301 */ 302 public static class AndTerm extends CompositeTerm { 303 private static final long serialVersionUID = 1L; 304 public AndTerm() { super(); } 305 public AndTerm(Term... terms) { super(terms); } 306 public AndTerm(Collection<Term> terms) { super(terms); } 307 308 @Override 309 public String getOp() { return "AND"; } 310 311 /** A term matches, when all sub-term match */ 312 @Override 313 public boolean match(TermMatcher m) { 314 for (Term t: terms()) 315 if (!t.match(m)) return false; 316 return true; 317 } 318 319 @Override 320 public Boolean match(AmbigousTermMatcher m) { 321 for (Term t: terms()) { 322 Boolean r = t.match(m); 323 if (r == null) return null; 324 if (!r) return false; 325 } 326 return true; 327 } 328 } 329 330 /** 331 * Representation of a NOT term 332 */ 333 public static class NotTerm implements Term { 334 private static final long serialVersionUID = 1L; 335 private Term iTerm; 336 337 public NotTerm(Term t) { 338 iTerm = t; 339 } 340 341 /** A term matches, when sub-term does not match */ 342 @Override 343 public boolean match(TermMatcher m) { 344 return !iTerm.match(m); 345 } 346 347 @Override 348 public boolean hasAttribute(String attribute) { 349 return iTerm.hasAttribute(attribute); 350 } 351 352 @Override 353 public Boolean match(AmbigousTermMatcher m) { 354 Boolean r = iTerm.match(m); 355 if (r == null) return r; 356 return !r; 357 } 358 359 @Override 360 public String toString() { return "NOT " + iTerm.toString(); } 361 362 @Override 363 public String toString(QueryFormatter f) { return "NOT " + iTerm.toString(f); } 364 } 365 366 /** 367 * Representation of one attribute:value term 368 */ 369 public static class AtomTerm implements Term { 370 private static final long serialVersionUID = 1L; 371 private String iAttr, iBody; 372 373 public AtomTerm(String attr, String body) { 374 if (body.startsWith("\"") && body.endsWith("\"") && body.length()>1) 375 body = body.substring(1, body.length() - 1); 376 iAttr = attr; iBody = body; 377 } 378 379 /** A term matches, when {@link TermMatcher#match(String, String)} matches */ 380 @Override 381 public boolean match(TermMatcher m) { 382 return m.match(iAttr, iBody); 383 } 384 385 @Override 386 public boolean hasAttribute(String attribute) { 387 return attribute != null && attribute.equals(iAttr); 388 } 389 390 public boolean sameAttribute(AtomTerm t) { 391 return t != null && hasAttribute(t.iAttr); 392 } 393 394 @Override 395 public String toString() { return (iAttr == null ? "" : iAttr + ":") + (iBody.indexOf(' ') >= 0 ? "\"" + iBody + "\"" : iBody); } 396 397 @Override 398 public String toString(QueryFormatter f) { return f.format(iAttr, iBody); } 399 400 @Override 401 public Boolean match(AmbigousTermMatcher m) { 402 return m.match(iAttr, iBody); 403 } 404 } 405 406 /** 407 * Term matcher interface. Check representing an object to be matched with the query. 408 * 409 */ 410 public static interface TermMatcher { 411 /** 412 * Does the object has the given attribute matching the given term? For example, for a student, major:M1 413 * would match a student that has major M1 (attribute is major, term is M1). 414 */ 415 public boolean match(String attr, String term); 416 } 417 418 /** 419 * Ambigous term matcher. Returning yes, no, or does not apply (null). 420 */ 421 public static interface AmbigousTermMatcher { 422 /** 423 * Does the object has the given attribute matching the given term? 424 * Returns null if does not apply. 425 **/ 426 public Boolean match(String attr, String term); 427 } 428 429 /** 430 * Query formatter class 431 */ 432 public static interface QueryFormatter { 433 String format(String attr, String term); 434 } 435 436 public static void main(String[] args) { 437 System.out.println(parse("(dept:1124 or dept:1125) and area:bio")); 438 System.out.println(parse("a \"b c\" or ddd f \"x:x\" x: s !(band or org) (a)or(b)")); 439 System.out.println(parse("! f (a)or(b) d !d not x s")); 440 System.out.println(parse("")); 441 System.out.println(split("(a \"b c\") ddd f", "")); 442 System.out.println(split("a \"b c\" OR not ddd f", "or")); 443 System.out.println(split("a or((\"b c\" or dddor) f) q", "or")); 444 System.out.println(parse("false")); 445 } 446 447 448}