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}