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.rc;
020
021import org.jpos.transaction.TransactionConstants;
022import org.jpos.util.Loggeable;
023
024import java.io.PrintStream;
025import java.util.*;
026import java.util.stream.Collectors;
027
028/**
029 * Represents the Result of a transaction
030 */
031public class Result implements Loggeable {
032    private final List<Entry> entries = Collections.synchronizedList(new ArrayList<>());
033
034    /** Default constructor. */
035    public Result() {
036        super();
037    }
038
039    /**
040     * Adds an informational entry to this result.
041     *
042     * @param source the source identifier of the entry
043     * @param format printf-style format string
044     * @param args   format arguments
045     * @return this Result instance
046     */
047    public Result info (String source, String format, Object ... args) {
048        return add(Type.INFO, null, source, format, args);
049    }
050
051    /**
052     * Adds a warning entry to this result.
053     *
054     * @param source the source identifier of the entry
055     * @param format printf-style format string
056     * @param args   format arguments
057     * @return this Result instance
058     */
059    public Result warn (String source, String format, Object ... args) {
060        return add(Type.WARN, null, source, format, args);
061    }
062
063    /**
064     * Adds a success entry to this result.
065     *
066     * @param irc    the success IRC code (must satisfy {@link IRC#success()})
067     * @param source the source identifier of the entry
068     * @param format printf-style format string
069     * @param args   format arguments
070     * @return this Result instance
071     * @throws IllegalArgumentException if the IRC is not a success code
072     */
073    public Result success (IRC irc, String source, String format, Object ... args) {
074        if (!irc.success())
075            throw new IllegalArgumentException("Invalid success IRC " + irc);
076        return add(Type.SUCCESS, irc, source, ""+format, args);
077    }
078
079    /**
080     * Adds a failure entry to this result.
081     *
082     * @param irc    the failure IRC code
083     * @param source the source identifier of the entry
084     * @param format printf-style format string
085     * @param args   format arguments
086     * @return this Result instance
087     */
088    public Result fail (IRC irc, String source, String format, Object ... args) {
089        synchronized (entries) {
090            if (isSuccess()) {
091                format = "" + format + " (inhibits " + success() + ")";
092            }
093            return add(Type.FAIL, irc, source, ""+format, args);
094        }
095    }
096
097    /**
098     * Helper method used to avoid adding an extra 'return' line in failing transaction participants
099     * @return TransactionConstants.FAIL which is basically ABORT | READONLY | NO_JOIN;
100     */
101    public int FAIL() {
102        return TransactionConstants.FAIL;
103    }
104
105    /**
106     * Returns {@code true} if this result contains at least one informational entry.
107     *
108     * @return {@code true} if an INFO entry is present
109     */
110    public boolean hasInfo() {
111        synchronized (entries) {
112            return entries.stream().anyMatch(e -> e.type == Type.INFO);
113        }
114    }
115
116    /**
117     * Returns {@code true} if this result contains at least one warning entry.
118     *
119     * @return {@code true} if a WARN entry is present
120     */
121    public boolean hasWarnings() {
122        synchronized (entries) {
123            return entries.stream().anyMatch(e -> e.type == Type.WARN);
124        }
125    }
126
127    /**
128     * Returns {@code true} if this result contains at least one failure entry.
129     *
130     * @return {@code true} if a FAIL entry is present
131     */
132    public boolean hasFailures() {
133        synchronized (entries) {
134            return entries.stream().anyMatch(e -> e.type == Type.FAIL);
135        }
136    }
137
138    /**
139     * Returns {@code true} if any entry has an IRC that is marked as inhibit.
140     *
141     * @return {@code true} if an inhibit IRC is present
142     */
143    public boolean hasInhibit() {
144        synchronized (entries) {
145            return entries.stream().anyMatch(e -> e.irc != null && e.irc.inhibit());
146        }
147    }
148
149    /**
150     * Returns {@code true} if any entry carries the given IRC code.
151     *
152     * @param irc the IRC to look for
153     * @return {@code true} if the IRC is found in any entry
154     */
155    public boolean hasIRC (IRC irc) {
156        synchronized (entries) {
157            return entries.stream().anyMatch(entry -> entry.irc == irc);
158        }
159    }
160
161    /**
162     * Returns {@code true} if there is a failure entry with the given IRC code.
163     *
164     * @param irc the failure IRC to look for
165     * @return {@code true} if a failure entry with the given IRC is present
166     */
167    public boolean hasFailure(IRC irc) {
168        synchronized (entries) {
169            return failureList().stream().anyMatch(entry -> entry.irc == irc);
170        }
171    }
172
173    /**
174     * Returns {@code true} if there is a warning entry with the given IRC code.
175     *
176     * @param irc the warning IRC to look for
177     * @return {@code true} if a warning entry with the given IRC is present
178     */
179    public boolean hasWarning(IRC irc) {
180        synchronized (entries) {
181            return warningList().stream().anyMatch(entry -> entry.irc == irc);
182        }
183    }
184
185    /**
186     * Returns {@code true} if there is an informational entry with the given IRC code.
187     *
188     * @param irc the info IRC to look for
189     * @return {@code true} if an info entry with the given IRC is present
190     */
191    public boolean hasInfo(IRC irc) {
192        synchronized (entries) {
193            return infoList().stream().anyMatch(entry -> entry.irc == irc);
194        }
195    }
196
197    /**
198     * Returns {@code true} if this result has a success entry and no failure entries.
199     *
200     * @return {@code true} if the overall result is a success
201     */
202    public boolean isSuccess() {
203        synchronized (entries) {
204            return isSuccess0() && !hasFailures();
205        }
206    }
207
208    /**
209     * Returns the first failure entry, or {@code null} if none exists.
210     *
211     * @return the first {@link Entry} of type FAIL, or {@code null}
212     */
213    public Entry failure() {
214        synchronized (entries) {
215            Optional<Entry> entry = entries.stream().filter(e -> e.type == Type.FAIL).findFirst();
216            return entry.isPresent() ? entry.get() : null;
217        }
218    }
219
220    /**
221     * Returns the first success entry if the result is successful, or {@code null} otherwise.
222     *
223     * @return the first {@link Entry} of type SUCCESS if successful, or {@code null}
224     */
225    public Entry success() {
226        synchronized (entries) {
227            Optional<Entry> entry = entries.stream().filter(e -> e.type == Type.SUCCESS).findFirst();
228            return entry.isPresent() && !hasFailures() ? entry.get() : null;
229        }
230    }
231
232    /**
233     * Returns the full list of all entries in this result.
234     *
235     * @return list of all {@link Entry} objects
236     */
237    public List<Entry> entries() {
238        return entries;
239    }
240
241    /**
242     * Returns a list of all informational entries.
243     *
244     * @return list of INFO {@link Entry} objects
245     */
246    public List<Entry> infoList() {
247        return entries
248          .stream()
249          .filter(s -> s.type == Type.INFO)
250          .collect(Collectors.toList());
251    }
252
253    /**
254     * Returns a list of all success entries.
255     *
256     * @return list of SUCCESS {@link Entry} objects
257     */
258    public List<Entry> successList() {
259        return entries
260          .stream()
261          .filter(s -> s.type == Type.SUCCESS)
262          .collect(Collectors.toList());
263    }
264
265    /**
266     * Returns a list of all warning entries.
267     *
268     * @return list of WARN {@link Entry} objects
269     */
270    public List<Entry> warningList() {
271        return entries
272          .stream()
273          .filter(s -> s.type == Type.WARN)
274          .collect(Collectors.toList());
275    }
276
277    /**
278     * Returns a list of all failure entries.
279     *
280     * @return list of FAIL {@link Entry} objects
281     */
282    public List<Entry> failureList() {
283        return entries
284          .stream()
285          .filter(s -> s.type == Type.FAIL)
286          .collect(Collectors.toList());
287    }
288
289    private Result add (Type type, IRC irc, String source, String format, Object ... args) {
290        entries.add (
291          new Entry(type, irc, source, String.format(format, args))
292        );
293        return this;
294    }
295
296    @Override
297    public void dump(final PrintStream ps, final String indent) {
298        if (entries.size() == 0) {
299            ps.printf ("%s<result/>%n", indent);
300            return;
301        }
302        final String inner = indent + "  ";
303        ps.printf("%s<result>%n", indent);
304        synchronized (entries) {
305            if (isSuccess0()) {
306                String inhibited = hasFailures() ? " inhibited='true'" : "";
307                ps.printf("%s<success%s>%n", inner, inhibited);
308                entries
309                  .stream()
310                  .filter(s -> s.type == Type.SUCCESS)
311                  .forEach(e -> ps.printf("%s  [%s] %s %s%n", inner, e.irc, e.source, e.message));
312                ps.printf("%s</success>%n", inner);
313            }
314            if (hasFailures()) {
315                ps.printf("%s<fail>%n", inner);
316                entries
317                  .stream()
318                  .filter(s -> s.type == Type.FAIL)
319                  .forEach(e -> ps.printf("%s  [%s] %s %s%n", inner, e.irc, e.source, e.message));
320                ps.printf("%s</fail>%n", inner);
321            }
322            if (hasWarnings()) {
323                ps.printf("%s<warn>%n", inner);
324                entries
325                  .stream()
326                  .filter(s -> s.type == Type.WARN)
327                  .forEach(e -> ps.printf("%s  [%s] %s%n", inner, e.source, e.message));
328                ps.printf("%s</warn>%n", inner);
329            }
330            if (hasInfo()) {
331                ps.printf("%s<info>%n", inner);
332                entries
333                  .stream()
334                  .filter(s -> s.type == Type.INFO)
335                  .forEach(e -> ps.printf("%s  [%s] %s%n", inner, e.source, e.message));
336                ps.printf("%s</info>%n", inner);
337            }
338        }
339        ps.printf("%s</result>%n", indent);
340    }
341
342    @Override
343    public String toString() {
344        return "Result{" +
345          "entries=" + entries +
346          '}';
347    }
348
349    private boolean isSuccess0() {
350        synchronized (entries) {
351            return entries.stream().anyMatch(e -> e.type == Type.SUCCESS);
352        }
353    }
354
355    private enum Type {
356        /** Informational entry type. */
357        INFO,
358        /** Warning entry type. */
359        WARN,
360        /** Success entry type. */
361        SUCCESS,
362        /** Failure entry type. */
363        FAIL
364    }
365
366    /** Represents a single entry in a transaction result. */
367    public static class Entry {
368        /** The type of this entry (INFO, WARN, SUCCESS, or FAIL). */
369        Type type;
370        /** The IRC code associated with this entry, or {@code null} for INFO/WARN. */
371        IRC irc;
372        /** The source identifier that generated this entry. */
373        String source;
374        /** The human-readable message for this entry. */
375        String message;
376
377        /**
378         * Constructs an Entry with the given type, IRC, source, and message.
379         *
380         * @param type    the entry type
381         * @param irc     the IRC code (may be {@code null})
382         * @param source  the source identifier
383         * @param message the entry message
384         */
385        public Entry(Type type, IRC irc, String source, String message) {
386            this.type = type;
387            this.irc = irc;
388            this.source = source;
389            this.message = message;
390        }
391
392        /**
393         * Returns the type of this entry.
394         *
395         * @return entry type
396         */
397        public Type getType() {
398            return type;
399        }
400
401        /**
402         * Returns the IRC code of this entry.
403         *
404         * @return IRC code, or {@code null} if not applicable
405         */
406        public IRC getIrc() {
407            return irc;
408        }
409
410        /**
411         * Returns the source identifier of this entry.
412         *
413         * @return source identifier string
414         */
415        public String getSource() {
416            return source;
417        }
418
419        /**
420         * Returns the message text of this entry.
421         *
422         * @return message string
423         */
424        public String getMessage() {
425            return message;
426        }
427
428        @Override
429        public String toString() {
430            return "Entry{" +
431              "type=" + type +
432              ", irc=" + irc +
433              ", source='" + source + '\'' +
434              ", message='" + message + '\'' +
435              '}';
436        }
437    }
438}