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}