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