001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.transaction; 020 021import com.fasterxml.jackson.annotation.JsonIgnore; 022import org.jdom2.Element; 023import org.jdom2.output.Format; 024import org.jdom2.output.XMLOutputter; 025import org.jpos.iso.ISOUtil; 026import org.jpos.util.*; 027import org.jpos.rc.Result; 028 029import java.io.*; 030import java.util.*; 031import java.util.concurrent.CompletableFuture; 032import java.util.concurrent.Future; 033import java.util.concurrent.locks.Lock; 034import java.util.concurrent.locks.ReentrantLock; 035import java.util.stream.Stream; 036 037import static org.jpos.transaction.ContextConstants.*; 038 039public class Context implements Externalizable, Loggeable, Cloneable, Pausable { 040 @Serial 041 private static final long serialVersionUID = 2604524947983441462L; 042 private transient Map<Object,Object> map; // transient map 043 private Map<Object,Object> pmap; // persistent (serializable) map 044 private transient boolean trace = false; 045 private CompletableFuture<Integer> pausedFuture; 046 private long timeout; 047 private final Lock lock = new ReentrantLock(); 048 public Context () { 049 super (); 050 } 051 052 /** 053 * puts an Object in the transient Map 054 */ 055 public void put (Object key, Object value) { 056 if (trace) { 057 getProfiler().checkPoint( 058 String.format("%s='%s' [%s]", getKeyName(key), value, Caller.info(1)) 059 ); 060 } 061 getMap().put (key, value); 062 synchronized (this) { 063 notifyAll(); 064 } 065 } 066 /** 067 * puts an Object in the transient Map 068 */ 069 public void put (Object key, Object value, boolean persist) { 070 if (trace) { 071 getProfiler().checkPoint( 072 String.format("%s(P)='%s' [%s]", getKeyName(key), value, Caller.info(1)) 073 ); 074 } 075 if (persist && value instanceof Serializable) 076 getPMap().put (key, value); 077 getMap().put(key, value); 078 } 079 080 /** 081 * Persists a transient entry 082 * @param key the key 083 */ 084 public void persist (Object key) { 085 Object value = get(key); 086 if (value instanceof Serializable) 087 getPMap().put (key, value); 088 } 089 090 /** 091 * Evicts a persistent entry 092 * @param key the key 093 */ 094 public void evict (Object key) { 095 getPMap().remove (key); 096 } 097 098 /** 099 * Get object instance from transaction context. 100 * 101 * @param <T> desired type of object instance 102 * @param key the key of object instance 103 * @return object instance if exist in context or {@code null} otherwise 104 */ 105 public <T> T get(Object key) { 106 @SuppressWarnings("unchecked") 107 T obj = (T) getMap().get(key); 108 return obj; 109 } 110 111 /** 112 * Check if key present 113 * @param key the key 114 * @return true if present 115 */ 116 public boolean hasKey(Object key) { 117 return getMap().containsKey(key); 118 } 119 120 /** 121 * Determines whether the specified keys are all present in the map. 122 * This method accepts a variable number of key arguments and supports 123 * both Object[] and String keys. When the key is a String, it can contain 124 * multiple keys separated by a '|' character, and the method will return 125 * true if any of those keys is present in the map. The method does not 126 * support nested arrays of keys. 127 * 128 * @param keys A variable-length array of keys to check for in the map. 129 * These keys can be of any Object type or String containing 130 * multiple keys separated by '|'. 131 * @return true if all specified keys (or any of the '|' separated keys 132 * within a String key) are present in the map, false otherwise. 133 */ 134 public boolean hasKeys(Object... keys) { 135 Map<Object,Object> m = getMap(); 136 return Arrays.stream(keys) 137 .flatMap(obj -> obj instanceof Object[] ? Arrays.stream((Object[]) obj) : Stream.of(obj)) 138 .allMatch(key -> { 139 if (key == null) { 140 return m.containsKey(null); // Explicit null check 141 } 142 String s = (key instanceof String a ? a : key.toString()).strip(); 143 if (s.contains("|")) { 144 return Arrays.stream(s.split("\\|")) 145 .map(String::strip) 146 .anyMatch(m::containsKey); 147 } 148 return m.containsKey(s); 149 }); 150 } 151 152 /** 153 * Returns a comma-separated string of keys that are not present in the map. 154 * This method accepts a variable number of key arguments and supports 155 * both Object[] and String keys. When the key is a String, it can contain 156 * multiple keys separated by a '|' character, and the method will return 157 * the keys not present in the map. The method does not support nested arrays of keys. 158 * 159 * @param keys A variable-length array of keys to check for their absence in the map. 160 * These keys can be of any Object type or String containing 161 * multiple keys separated by '|'. 162 * @return A comma-separated string of keys that are not present in the map. 163 * If all the specified keys (or any of the '|' separated keys within 164 * a String key) are present in the map, an empty string is returned. 165 */ 166 public String keysNotPresent (Object... keys) { 167 Map<Object, Object> m = getMap(); 168 StringJoiner notFoundKeys = new StringJoiner(","); 169 170 Arrays.stream(keys) 171 .flatMap(obj -> obj instanceof Object[] ? Arrays.stream((Object[]) obj) : Stream.of(obj)) 172 .forEach(key -> { 173 boolean keyPresent; 174 175 if (key instanceof String s) { 176 s = s.strip(); 177 if (s.contains("|")) { 178 keyPresent = Arrays.stream(s.split("\\|")) 179 .map(String::strip) 180 .anyMatch(m::containsKey); 181 } else { 182 keyPresent = m.containsKey(s); 183 } 184 } else { 185 keyPresent = m.containsKey(key); 186 } 187 188 if (!keyPresent) { 189 notFoundKeys.add(key.toString().strip()); 190 } 191 }); 192 193 return notFoundKeys.toString(); 194 } 195 196 197 /** 198 * Check key exists present persisted map 199 * @param key the key 200 * @return true if present 201 */ 202 public boolean hasPersistedKey(Object key) { 203 return getPMap().containsKey(key); 204 } 205 206 /** 207 * Move entry to new key name 208 * @param from key 209 * @param to key 210 * @return the entry's value (could be null if 'from' key not present) 211 */ 212 public synchronized <T> T move(Object from, Object to) { 213 T obj = get(from); 214 if (obj != null) { 215 put(to, obj, hasPersistedKey(from)); 216 remove(from); 217 } 218 return obj; 219 } 220 221 /** 222 * Get object instance from transaction context. 223 * 224 * @param <T> desired type of object instance 225 * @param key the key of object instance 226 * @param defValue default value returned if there is no value in context 227 * @return object instance if exist in context or {@code defValue} otherwise 228 */ 229 public <T> T get(Object key, T defValue) { 230 @SuppressWarnings("unchecked") 231 T obj = (T) getMap().get(key); 232 return obj != null ? obj : defValue; 233 } 234 235 /** 236 * Transient remove 237 */ 238 public synchronized <T> T remove(Object key) { 239 getPMap().remove(key); 240 @SuppressWarnings("unchecked") 241 T obj = (T) getMap().remove(key); 242 return obj; 243 } 244 245 public String getString (Object key) { 246 Object obj = getMap().get (key); 247 if (obj instanceof String) 248 return (String) obj; 249 else if (obj != null) 250 return obj.toString(); 251 return null; 252 } 253 public String getString (Object key, String defValue) { 254 Object obj = getMap().get (key); 255 if (obj instanceof String) 256 return (String) obj; 257 else if (obj != null) 258 return obj.toString(); 259 return defValue; 260 } 261 public void dump (PrintStream p, String indent) { 262 String inner = indent + " "; 263 p.println (indent + "<context>"); 264 dumpMap (p, inner); 265 p.println (indent + "</context>"); 266 } 267 /** 268 * persistent get with timeout 269 * @param key the key 270 * @param timeout timeout 271 * @return object (null on timeout) 272 */ 273 @SuppressWarnings("unchecked") 274 public synchronized <T> T get (Object key, long timeout) { 275 T obj; 276 long now = System.currentTimeMillis(); 277 long end = now + timeout; 278 while ((obj = (T) map.get (key)) == null && 279 (now = System.currentTimeMillis()) < end) 280 { 281 try { 282 if (end > now) 283 this.wait (end - now); 284 } catch (InterruptedException ignored) { } 285 } 286 return obj; 287 } 288 public void writeExternal (ObjectOutput out) throws IOException { 289 out.writeByte (0); // reserved for future expansion (version id) 290 Set s = getPMap().entrySet(); 291 out.writeInt (s.size()); 292 Iterator iter = s.iterator(); 293 while (iter.hasNext()) { 294 Map.Entry entry = (Map.Entry) iter.next(); 295 out.writeObject(entry.getKey()); 296 out.writeObject(entry.getValue()); 297 } 298 } 299 public void readExternal (ObjectInput in) 300 throws IOException, ClassNotFoundException 301 { 302 in.readByte(); // ignore version for now 303 getMap(); // force creation of map 304 getPMap(); // and pmap 305 int size = in.readInt(); 306 for (int i=0; i<size; i++) { 307 String k = (String) in.readObject(); 308 Object v = in.readObject(); 309 map.put (k, v); 310 pmap.put (k, v); 311 } 312 } 313 314 /** 315 * Creates a copy of the current Context object. 316 * <p> 317 * This method clones the Context object, creating new synchronized map containers 318 * that are independent of the original. However, the keys and values themselves 319 * are <b>not cloned</b> - both Context instances will share references to the same 320 * key/value objects. Structural changes (add/remove operations) to one Context's 321 * maps will not affect the other, but modifications to mutable key/value objects 322 * will be visible in both Contexts. 323 * </p> 324 * <p> 325 * The cloned Context preserves the thread-safety characteristics through 326 * {@code Collections.synchronizedMap} wrappers. 327 * </p> 328 * 329 * @return a copy of the current Context object with independent map containers 330 * @throws AssertionError if cloning is not supported, which should not happen 331 */ 332 @Override 333 public Context clone() { 334 try { 335 Context context = (Context) super.clone(); 336 if (map != null) { 337 context.map = Collections.synchronizedMap (new LinkedHashMap<>()); 338 context.map.putAll(map); 339 } 340 if (pmap != null) { 341 context.pmap = Collections.synchronizedMap (new LinkedHashMap<>()); 342 context.pmap.putAll(pmap); 343 } 344 return context; 345 } catch (CloneNotSupportedException e) { 346 throw new AssertionError(); // Should not happen 347 } 348 } 349 350 /** 351 * Creates a clone of the current Context instance, including only the specified keys. 352 * This method accepts a variable number of key arguments and supports both 353 * Object[] and String keys. When the key is a String, it can contain multiple 354 * keys separated by a '|' character. The method does not support nested arrays of keys. 355 * 356 * @param keys A variable-length array of keys to include in the cloned context. 357 * These keys can be of any Object type or String containing multiple 358 * keys separated by '|'. 359 * @return A cloned Context instance containing only the specified keys and 360 * their associated values from the original context. If none of the 361 * specified keys are present in the original context, an empty Context 362 * instance is returned. 363 */ 364 public Context clone(Object... keys) { 365 Context clonedContext = new Context(); 366 Map<Object, Object> m = getMap(); 367 Map<Object, Object> pm = getPMap(); 368 Arrays.stream(keys) 369 .flatMap(obj -> obj instanceof Object[] ? Arrays.stream((Object[]) obj) : Stream.of(obj)) 370 .flatMap(obj -> { 371 if (obj == null) { 372 return Stream.of((Object) null); // Handle null as a key 373 } else if (obj instanceof String s) { 374 s = s.strip(); 375 return Arrays.stream(s.split("\\|")) 376 .map(String::strip) 377 .map(str -> (Object) str); // Split strings into keys 378 } else { 379 String s = obj.toString().strip(); // Convert non-String to String 380 return Arrays.stream(s.split("\\|")) 381 .map(String::strip) 382 .map(str -> (Object) str); 383 } 384 }) 385 .filter(m::containsKey) // Check if the key exists in the map 386 .forEachOrdered(key -> clonedContext.put(key, m.get(key), pm.containsKey(key))); 387 return clonedContext; 388 } 389 390 /** 391 * Merges the entries from the provided Context object into the current Context. 392 * <p> 393 * This method iterates over the entries in the given Context object 'c' and adds or updates 394 * the entries in the current Context. If an entry already exists in the current Context, 395 * its value will be updated. If an entry is marked as persisted in the given Context object, 396 * it will also be marked as persisted in the current Context. 397 * </p> 398 * 399 * @param c the Context object whose entries should be merged into the current Context 400 */ 401 public void merge(Context c) { 402 if (c != null) { 403 c.getMap().forEach((key, value) -> put(key, value, c.hasPersistedKey(key))); 404 } 405 } 406 @Override 407 public boolean equals(Object o) { 408 if (this == o) return true; 409 if (o == null || getClass() != o.getClass()) return false; 410 Context context = (Context) o; 411 return trace == context.trace && 412 Objects.equals(map, context.map) && 413 Objects.equals(pmap, context.pmap); 414 } 415 416 @Override 417 public int hashCode() { 418 return Objects.hash(map, pmap, trace); 419 } 420 421 /** 422 * @return persistent map 423 */ 424 private synchronized Map<Object,Object> getPMap() { 425 if (pmap == null) 426 pmap = Collections.synchronizedMap (new LinkedHashMap<> ()); 427 return pmap; 428 } 429 /** 430 * @return transient map 431 */ 432 public synchronized Map<Object,Object> getMap() { 433 if (map == null) 434 map = Collections.synchronizedMap (new LinkedHashMap<>()); 435 return map; 436 } 437 438 @JsonIgnore 439 public Map<Object,Object> getMapClone() { 440 Map<Object,Object> cloned = Collections.synchronizedMap (new LinkedHashMap<>()); 441 synchronized(getMap()) { 442 cloned.putAll(map); 443 } 444 return cloned; 445 } 446 447 protected void dumpMap (PrintStream p, String indent) { 448 if (map != null) { 449 getMapClone().entrySet().forEach(e -> dumpEntry(p, indent, e)); 450 } 451 } 452 453 protected void dumpEntry (PrintStream p, String indent, Map.Entry<Object,Object> entry) { 454 String key = getKeyName(entry.getKey()); 455 if (key.startsWith(".") || key.startsWith("*")) 456 return; // see jPOS-63 457 458 p.printf("%s%s%s: ", indent, key, pmap != null && pmap.containsKey(key) ? "(P)" : ""); 459 Object value = entry.getValue(); 460 if (value instanceof Loggeable) { 461 p.println(); 462 try { 463 ((Loggeable) value).dump(p, indent + " "); 464 } catch (Exception ex) { 465 ex.printStackTrace(p); 466 } 467 p.print(indent); 468 } else if (value instanceof Element) { 469 p.println(); 470 XMLOutputter out = new XMLOutputter(Format.getPrettyFormat()); 471 out.getFormat().setLineSeparator(System.lineSeparator()); 472 try { 473 out.output((Element) value, p); 474 } catch (IOException ex) { 475 ex.printStackTrace(p); 476 } 477 p.println(); 478 } else if (value instanceof byte[] b) { 479 p.println(); 480 p.println(ISOUtil.hexdump(b)); 481 p.print(indent); 482 } 483 else if (value instanceof short[]) { 484 p.print (Arrays.toString((short[]) value)); 485 } else if (value instanceof int[]) { 486 p.print(Arrays.toString((int[]) value)); 487 } else if (value instanceof long[]) { 488 p.print(Arrays.toString((long[]) value)); 489 } else if (value instanceof Object[]) { 490 p.print (ISOUtil.normalize(Arrays.toString((Object[]) value), true)); 491 } 492 else if (value instanceof LogEvent) { 493 ((LogEvent) value).dump(p, indent); 494 p.print(indent); 495 } else if (value != null) { 496 try { 497 LogUtil.dump(p, indent, value.toString()); 498 } catch (Exception ex) { 499 ex.printStackTrace(p); 500 } 501 } 502 p.println(); 503 } 504 505 /** 506 * return a LogEvent used to store trace information 507 * about this transaction. 508 * If there's no LogEvent there, it creates one. 509 * @return LogEvent 510 */ 511 synchronized public LogEvent getLogEvent () { 512 LogEvent evt = get (LOGEVT.toString()); 513 if (evt == null) { 514 evt = new LogEvent (); 515 evt.setNoArmor(true); 516 put (LOGEVT.toString(), evt); 517 } 518 return evt; 519 } 520 /** 521 * return (or creates) a Profiler object 522 * @return Profiler object 523 */ 524 synchronized public Profiler getProfiler () { 525 Profiler prof = get (PROFILER.toString()); 526 if (prof == null) { 527 prof = new Profiler(); 528 put (PROFILER.toString(), prof); 529 } 530 return prof; 531 } 532 533 /** 534 * return (or creates) a Resultr object 535 * @return Profiler object 536 */ 537 synchronized public Result getResult () { 538 Result result = get (RESULT.toString()); 539 if (result == null) { 540 result = new Result(); 541 put (RESULT.toString(), result); 542 } 543 return result; 544 } 545 546 /** 547 * adds a trace message 548 * @param msg trace information 549 */ 550 public void log (Object msg) { 551 if (msg != getMap()) // prevent recursive call to dump (and StackOverflow) 552 getLogEvent().addMessage (msg); 553 } 554 /** 555 * add a checkpoint to the profiler 556 */ 557 public void checkPoint (String detail) { 558 getProfiler().checkPoint (detail); 559 } 560 561 public boolean isTrace() { 562 return trace; 563 } 564 public void setTrace(boolean trace) { 565 if (trace) 566 getProfiler(); 567 this.trace = trace; 568 } 569 570 @Override 571 public Future<Integer> pause() { 572 try { 573 lock.lock(); 574 if (pausedFuture == null) 575 pausedFuture = new CompletableFuture<>(); 576 else if (!pausedFuture.isDone()) 577 throw new IllegalStateException("already paused"); 578 } finally { 579 lock.unlock(); 580 } 581 return pausedFuture; 582 } 583 584 @Override 585 public void resume(int result) { 586 try { 587 lock.lock(); 588 if (pausedFuture == null) 589 pausedFuture = new CompletableFuture<>(); 590 pausedFuture.complete(result); 591 } finally { 592 lock.unlock(); 593 } 594 } 595 @Override 596 public void reset () { 597 try { 598 lock.lock(); 599 pausedFuture = null; 600 } finally { 601 lock.unlock(); 602 } 603 } 604 605 @Override 606 public long getTimeout() { 607 return timeout; 608 } 609 610 @Override 611 public void setTimeout(long timeout) { 612 this.timeout = timeout; 613 } 614 615 private String getKeyName(Object keyObject) { 616 return keyObject instanceof String ? (String) keyObject : 617 Caller.shortClassName(keyObject.getClass().getName())+"."+ keyObject; 618 } 619}