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}