1 package com.softmotions.ejdb2; 2 3 import java.io.ByteArrayOutputStream; 4 import java.io.OutputStream; 5 import java.io.UnsupportedEncodingException; 6 import java.lang.ref.ReferenceQueue; 7 import java.lang.ref.WeakReference; 8 import java.util.ArrayList; 9 import java.util.List; 10 import java.util.Map; 11 import java.util.StringJoiner; 12 import java.util.concurrent.ConcurrentHashMap; 13 14 /** 15 * EJDB2 Query specification. 16 * <p> 17 * Query can be reused multiple times with various placeholder parameters. See 18 * JQL specification: 19 * https://github.com/Softmotions/ejdb/blob/master/README.md#jql 20 * <p> 21 * Memory resources used by JQL instance must be released explicitly by 22 * {@link JQL#close()}. 23 * <p> 24 * <strong>Note:</strong> If user did not close instance explicitly it will be 25 * freed anyway once jql object will be garbage collected. 26 * <p> 27 * Typical usage: 28 * 29 * <pre> 30 * {@code 31 * try (JQL q = db.createQuery("/[foo=:val]", "mycoll") 32 * .setString("val", "bar")) { 33 * q.execute((docId, doc) -> { 34 * System.out.println(String.format("Found %d %s", docId, doc)); 35 * return 1; 36 * }); 37 * } 38 * } 39 * </pre> 40 */ 41 public final class JQL implements AutoCloseable { 42 43 private static final ReferenceQueue<JQL> refQueue = new ReferenceQueue<>(); 44 45 @SuppressWarnings("StaticCollection") 46 private static final Map<Long, Reference> refs = new ConcurrentHashMap<Long, Reference>(); 47 48 private static final Thread cleanupThread = new Thread(() -> { 49 while (true) { 50 try { 51 ((Reference) refQueue.remove()).cleanup(); 52 } catch (InterruptedException ignored) { 53 } 54 } 55 }); 56 57 static { 58 cleanupThread.setDaemon(true); cleanupThread.start()59 cleanupThread.start(); 60 } 61 62 private final EJDB2 db; 63 64 private final String query; 65 66 private String collection; 67 68 private long skip; 69 70 private long limit; 71 72 private long _handle; 73 74 private ByteArrayOutputStream explain; 75 76 /** 77 * Owner database instance 78 */ getDb()79 public EJDB2 getDb() { 80 return db; 81 } 82 83 /** 84 * Query specification used to construct this query object. 85 */ getQuery()86 public String getQuery() { 87 return query; 88 } 89 90 /** 91 * Collection name used for this query 92 */ getCollection()93 public String getCollection() { 94 return collection; 95 } 96 setCollection(String collection)97 public JQL setCollection(String collection) { 98 this.collection = collection; 99 return this; 100 } 101 102 /** 103 * Turn on collecting of query execution log 104 * 105 * @see #getExplainLog() 106 */ withExplain()107 public JQL withExplain() { 108 explain = new ByteArrayOutputStream(); 109 return this; 110 } 111 112 /** 113 * Turn off collecting of query execution log 114 * 115 * @see #getExplainLog() 116 */ withNoExplain()117 public JQL withNoExplain() { 118 explain = null; 119 return this; 120 } 121 getExplainLog()122 public String getExplainLog() { 123 try { 124 return explain != null ? explain.toString("UTF-8") : null; 125 } catch (UnsupportedEncodingException ignored) { 126 return null; 127 } 128 } 129 130 /** 131 * Number of records to skip. This parameter takes precedence over {@code skip} 132 * encoded in query spec. 133 * 134 * @return 135 */ setSkip(long skip)136 public JQL setSkip(long skip) { 137 this.skip = skip; 138 return this; 139 } 140 getSkip()141 public long getSkip() { 142 return skip > 0 ? skip : _get_skip(); 143 } 144 145 /** 146 * Maximum number of records to retrive. This parameter takes precedence over 147 * {@code limit} encoded in query spec. 148 */ setLimit(long limit)149 public JQL setLimit(long limit) { 150 this.limit = limit; 151 return this; 152 } 153 getLimit()154 public long getLimit() { 155 return limit > 0 ? limit : _get_limit(); 156 } 157 158 /** 159 * Set positional string parameter starting for {@code 0} index. 160 * <p> 161 * Example: 162 * 163 * <pre> 164 * {@code 165 * db.createQuery("/[foo=:?]", "mycoll").setString(0, "zaz") 166 * } 167 * </pre> 168 * 169 * @param pos Zero based positional index 170 * @param val Value to set 171 * @return 172 * @throws EJDB2Exception 173 */ setString(int pos, String val)174 public JQL setString(int pos, String val) throws EJDB2Exception { 175 _set_string(pos, null, val, 0); 176 return this; 177 } 178 179 /** 180 * Set string parameter placeholder in query spec. 181 * <p> 182 * Example: 183 * 184 * <pre> 185 * {@code 186 * db.createQuery("/[foo=:val]", "mycoll").setString("val", "zaz"); 187 * } 188 * </pre> 189 * 190 * @param placeholder Placeholder name 191 * @param val Value to set 192 * @return 193 * @throws EJDB2Exception 194 */ setString(String placeholder, String val)195 public JQL setString(String placeholder, String val) throws EJDB2Exception { 196 _set_string(0, placeholder, val, 0); 197 return this; 198 } 199 setLong(int pos, long val)200 public JQL setLong(int pos, long val) throws EJDB2Exception { 201 _set_long(pos, null, val); 202 return this; 203 } 204 setLong(String placeholder, long val)205 public JQL setLong(String placeholder, long val) throws EJDB2Exception { 206 _set_long(0, placeholder, val); 207 return this; 208 } 209 setJSON(int pos, String json)210 public JQL setJSON(int pos, String json) throws EJDB2Exception { 211 _set_string(pos, null, json, 1); 212 return this; 213 } 214 setJSON(int pos, JSON json)215 public JQL setJSON(int pos, JSON json) throws EJDB2Exception { 216 _set_string(pos, null, json.toString(), 1); 217 return this; 218 } 219 setJSON(String placeholder, String json)220 public JQL setJSON(String placeholder, String json) throws EJDB2Exception { 221 _set_string(0, placeholder, json, 1); 222 return this; 223 } 224 setJSON(String placeholder, JSON json)225 public JQL setJSON(String placeholder, JSON json) throws EJDB2Exception { 226 _set_string(0, placeholder, json.toString(), 1); 227 return this; 228 } 229 setRegexp(int pos, String regexp)230 public JQL setRegexp(int pos, String regexp) throws EJDB2Exception { 231 _set_string(pos, null, regexp, 2); 232 return this; 233 } 234 setRegexp(String placeholder, String regexp)235 public JQL setRegexp(String placeholder, String regexp) throws EJDB2Exception { 236 _set_string(0, placeholder, regexp, 2); 237 return this; 238 } 239 setDouble(int pos, double val)240 public JQL setDouble(int pos, double val) throws EJDB2Exception { 241 _set_double(pos, null, val); 242 return this; 243 } 244 setDouble(String placeholder, double val)245 public JQL setDouble(String placeholder, double val) throws EJDB2Exception { 246 _set_double(0, placeholder, val); 247 return this; 248 } 249 setBoolean(int pos, boolean val)250 public JQL setBoolean(int pos, boolean val) throws EJDB2Exception { 251 _set_boolean(pos, null, val); 252 return this; 253 } 254 setBoolean(String placeholder, boolean val)255 public JQL setBoolean(String placeholder, boolean val) throws EJDB2Exception { 256 _set_boolean(0, placeholder, val); 257 return this; 258 } 259 setNull(int pos)260 public JQL setNull(int pos) throws EJDB2Exception { 261 _set_null(pos, null); 262 return this; 263 } 264 setNull(String placeholder)265 public JQL setNull(String placeholder) throws EJDB2Exception { 266 _set_null(0, placeholder); 267 return this; 268 } 269 270 /** 271 * Execute query and handle record {@link EJDB2Document} values by provided 272 * {@code cb} 273 * 274 * @param cb Optional callback 275 * @throws EJDB2Exception 276 */ execute(EJDB2DocumentCallback cb)277 public void execute(EJDB2DocumentCallback cb) throws EJDB2Exception { 278 if (explain != null) { 279 explain.reset(); 280 } 281 if (cb != null) { 282 _execute(db, (id, sv) -> cb.onDocument(new EJDB2Document(id, sv)), explain); 283 } else { 284 _execute(db, null, explain); 285 } 286 } 287 288 /** 289 * Execute query without result set callback. 290 * 291 * @throws EJDB2Exception 292 */ execute()293 public void execute() throws EJDB2Exception { 294 execute(null); 295 } 296 executeRaw(JQLCallback cb)297 public void executeRaw(JQLCallback cb) throws EJDB2Exception { 298 if (explain != null) { 299 explain.reset(); 300 } 301 if (cb != null) { 302 _execute(db, (id, sv) -> cb.onRecord(id, sv), explain); 303 } else { 304 _execute(db, null, explain); 305 } 306 } 307 list()308 public List<EJDB2Document> list() throws EJDB2Exception { 309 List<EJDB2Document> list = new ArrayList<>(); 310 execute((doc) -> { 311 list.add(doc); 312 return 1; 313 }); 314 return list; 315 } 316 first()317 public EJDB2Document first() { 318 final EJDB2Document[] v = { null }; 319 if (explain != null) { 320 explain.reset(); 321 } 322 _execute(db, (id, json) -> { 323 v[0] = new EJDB2Document(id, json); 324 return 0; 325 }, explain); 326 return v[0]; 327 } 328 329 /** 330 * Get first document body as JSON string or null. 331 */ firstValue()332 public String firstValue() { 333 final String[] v = { null }; 334 if (explain != null) { 335 explain.reset(); 336 } 337 _execute(db, (id, json) -> { 338 v[0] = json; 339 return 0; 340 }, explain); 341 return v[0]; 342 } 343 344 /** 345 * Get first document id ot null 346 */ firstId()347 public Long firstId() { 348 final Long[] v = { null }; 349 if (explain != null) { 350 explain.reset(); 351 } 352 _execute(db, (id, json) -> { 353 v[0] = id; 354 return 0; 355 }, explain); 356 return v[0]; 357 } 358 359 /** 360 * Execute scalar query. 361 * <p> 362 * Example: 363 * 364 * <pre> 365 * long count = db.createQuery("@mycoll/* | count").executeScalarInt(); 366 * </pre> 367 */ executeScalarInt()368 public long executeScalarInt() { 369 if (explain != null) { 370 explain.reset(); 371 } 372 return _execute_scalar_long(db, explain); 373 } 374 375 /** 376 * Reset data stored in positional placeholderss 377 */ reset()378 public void reset() { 379 if (explain != null) { 380 explain.reset(); 381 } 382 _reset(); 383 } 384 385 /** 386 * Close query instance releasing memory resources 387 */ 388 @Override close()389 public void close() throws Exception { 390 Reference ref = refs.get(_handle); 391 if (ref != null) { 392 ref.enqueue(); 393 } else { 394 long h = _handle; 395 if (h != 0) { 396 _destroy(h); 397 } 398 } 399 } 400 JQL(EJDB2 db, String query, String collection)401 JQL(EJDB2 db, String query, String collection) throws EJDB2Exception { 402 this.db = db; 403 this.query = query; 404 this.collection = collection; 405 _init(db, query, collection); 406 // noinspection InstanceVariableUsedBeforeInitialized 407 refs.put(_handle, new Reference(this, refQueue)); 408 } 409 410 @Override toString()411 public String toString() { 412 return new StringJoiner(", ", JQL.class.getSimpleName() + "[", "]") 413 .add("query=" + query) 414 .add("collection=" + collection) 415 .toString(); 416 } 417 418 private static class Reference extends WeakReference<JQL> { 419 private long handle; 420 Reference(JQL jql, ReferenceQueue<JQL> rq)421 Reference(JQL jql, ReferenceQueue<JQL> rq) { 422 super(jql, rq); 423 handle = jql._handle; 424 } 425 cleanup()426 void cleanup() { 427 long h = handle; 428 handle = 0L; 429 if (h != 0) { 430 refs.remove(h); 431 _destroy(h); 432 } 433 } 434 } 435 _destroy(long handle)436 private static native void _destroy(long handle); 437 _init(EJDB2 db, String query, String collection)438 private native void _init(EJDB2 db, String query, String collection); 439 _execute(EJDB2 db, JQLCallback cb, OutputStream explainLog)440 private native void _execute(EJDB2 db, JQLCallback cb, OutputStream explainLog); 441 _execute_scalar_long(EJDB2 db, OutputStream explainLog)442 private native long _execute_scalar_long(EJDB2 db, OutputStream explainLog); 443 _reset()444 private native void _reset(); 445 _get_limit()446 private native long _get_limit(); 447 _get_skip()448 private native long _get_skip(); 449 _set_string(int pos, String placeholder, String val, int type)450 private native void _set_string(int pos, String placeholder, String val, int type); 451 _set_long(int pos, String placeholder, long val)452 private native void _set_long(int pos, String placeholder, long val); 453 _set_double(int pos, String placeholder, double val)454 private native void _set_double(int pos, String placeholder, double val); 455 _set_boolean(int pos, String placeholder, boolean val)456 private native void _set_boolean(int pos, String placeholder, boolean val); 457 _set_null(int pos, String placeholder)458 private native void _set_null(int pos, String placeholder); 459 }