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