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.util;
020
021import org.jdom2.Element;
022import org.jdom2.output.Format;
023import org.jdom2.output.XMLOutputter;
024import org.jpos.jfr.LogEventDump;
025import org.jpos.log.LogRenderer;
026import org.jpos.log.LogRendererRegistry;
027import org.jpos.log.evt.SysInfo;
028import org.jpos.log.render.txt.SysInfoTxtLogRenderer;
029
030import java.io.ByteArrayOutputStream;
031import java.io.IOException;
032import java.io.PrintStream;
033import java.sql.SQLException;
034import java.time.Duration;
035import java.time.Instant;
036import java.time.LocalDateTime;
037import java.time.ZoneId;
038import java.util.*;
039
040/**
041 * @author @apr
042 */
043public class LogEvent {
044    private LogSource source;
045    private String tag;
046    private final List<Object> payLoad;
047    private Instant createdAt;
048    private Instant dumpedAt;
049    private boolean honorSourceLogger;
050    private boolean noArmor;
051    private boolean hasException;
052    private String traceId;
053
054    public LogEvent (String tag) {
055        super();
056        this.tag = tag;
057        createdAt = Instant.now();
058        this.payLoad = Collections.synchronizedList (new ArrayList<>());
059    }
060
061    public LogEvent () {
062        this("info");
063    }
064    public LogEvent (String tag, Object msg) {
065        this (tag);
066        addMessage(msg);
067    }
068    public LogEvent (LogSource source, String tag) {
069        this (tag);
070        this.source  = source;
071        honorSourceLogger = true;
072    }
073    public LogEvent (LogSource source, String tag, Object msg) {
074        this (tag);
075        this.source  = source;
076        honorSourceLogger = true;
077        addMessage(msg);
078    }
079    public String getTag() {
080        return tag;
081    }
082    public void setTag (String tag) {
083        this.tag = tag;
084    }
085    public void addMessage (Object msg) {
086        payLoad.add (msg);
087        if (msg instanceof Throwable)
088            hasException = true;
089    }
090    public void addMessage (String tagname, String message) {
091        payLoad.add ("<"+tagname+">"+message+"</"+tagname+">");
092    }
093    public LogSource getSource() {
094        return source;
095    }
096    public void setSource(LogSource source) {
097        this.source = source;
098    }
099    public void setNoArmor (boolean noArmor) {
100        this.noArmor = noArmor;
101    }
102    protected String dumpHeader (PrintStream p, String indent) {
103        if (noArmor) {
104            p.println("");
105        } else {
106            dumpedAt = getDumpedAt();
107            StringBuilder sb = new StringBuilder(indent);
108            sb.append ("<log realm=\"");
109            sb.append (getRealm());
110            sb.append("\" at=\"");
111            sb.append(LocalDateTime.ofInstant(dumpedAt, ZoneId.systemDefault()));
112            sb.append ('"');
113            long elapsed = Duration.between(createdAt, dumpedAt).toMillis();
114            if (elapsed > 0) {
115                sb.append (" lifespan=\"");
116                sb.append (elapsed);
117                sb.append ("ms\"");
118            }
119            if (traceId != null) {
120                sb.append (String.format (" trace-id=\"%s\"", traceId));
121            }
122            sb.append ('>');
123            p.println (sb);
124        }
125        return indent + "  ";
126    }
127    protected void dumpTrailer (PrintStream p, String indent) {
128        if (!noArmor)
129            p.println (indent + "</log>");
130    }
131    public void dump (PrintStream p, String outer) {
132        var jfr = new LogEventDump();
133        jfr.begin();
134        try {
135            String indent = dumpHeader (p, outer);
136            if (payLoad.isEmpty()) {
137                if (tag != null)
138                    p.println (indent + "<" + tag + "/>");
139            }
140            else {
141                String newIndent;
142                if (tag != null) {
143                    if (!tag.isEmpty())
144                        p.println (indent + "<" + tag + ">");
145                    newIndent = indent + "  ";
146                }
147                else
148                    newIndent = "";
149                synchronized (payLoad) {
150                    for (Object o : payLoad) {
151                        if (o instanceof Loggeable)
152                            ((Loggeable) o).dump(p, newIndent);
153                        else if (o instanceof SQLException) {
154                            SQLException e = (SQLException) o;
155                            p.println(newIndent + "<SQLException>"
156                              + e.getMessage() + "</SQLException>");
157                            p.println(newIndent + "<SQLState>"
158                              + e.getSQLState() + "</SQLState>");
159                            p.println(newIndent + "<VendorError>"
160                              + e.getErrorCode() + "</VendorError>");
161                            ((Throwable) o).printStackTrace(p);
162                        } else if (o instanceof Throwable) {
163                            p.println(newIndent + "<exception name=\""
164                              + ((Throwable) o).getMessage() + "\">");
165                            p.print(newIndent);
166                            ((Throwable) o).printStackTrace(p);
167                            p.println(newIndent + "</exception>");
168                        } else if (o instanceof Object[]) {
169                            Object[] oa = (Object[]) o;
170                            p.print(newIndent + "[");
171                            for (int j = 0; j < oa.length; j++) {
172                                if (j > 0)
173                                    p.print(",");
174                                p.print(oa[j].toString());
175                            }
176                            p.println("]");
177                        } else if (o instanceof Element) {
178                            p.println("");
179                            XMLOutputter out = new XMLOutputter(Format.getPrettyFormat());
180                            out.getFormat().setLineSeparator("\n");
181                            try {
182                                out.output((Element) o, p);
183                            } catch (IOException ex) {
184                                ex.printStackTrace(p);
185                            }
186                            p.println("");
187                        } else if (o != null) {
188                            LogRenderer<Object> renderer = LogRendererRegistry.getRenderer(o.getClass(), LogRenderer.Type.TXT);
189                            if (renderer != null)
190                                renderer.render(o, p, newIndent);
191                            else
192                                p.println(newIndent + o);
193                        } else {
194                            p.println(newIndent + "null");
195                        }
196                    }
197                }
198                if (tag != null && !tag.isEmpty())
199                    p.println (indent + "</" + tag + ">");
200            }
201        } catch (Throwable t) {
202            t.printStackTrace(p);
203
204        } finally {
205            dumpTrailer (p, outer);
206            jfr.commit();
207        }
208    }
209    public String getRealm() {
210        return source != null ? source.getRealm() : "";
211    }
212    public LogEvent withTraceId (String traceId) {
213        this.traceId = traceId;
214        return this;
215    }
216    public LogEvent withTraceId (UUID uuid) {
217        this.traceId = uuid.toString().replace("-", "");
218        return this;
219    }
220    public LogEvent withSource (LogSource source) {
221        setSource(source);
222        return this;
223    }
224    public LogEvent add (Object o) {
225        addMessage(o);
226        return this;
227    }
228    public LogEvent withTraceId () {
229        getTraceId();
230        return this;
231    }
232    public String getTraceId() {
233        synchronized(getPayLoad()) {
234            if (traceId == null)
235                traceId = UUID.randomUUID().toString().replace("-","");
236            return traceId;
237        }
238    }
239
240    /**
241     * WARNING: payLoad is a SynchronizedList. If you intend to get a reference
242     * to it in order to iterate over the list, you need to synchronize on the
243     * returned object.
244     *
245     * <pre>
246     *     synchronized (evt.getPayLoad()) {
247     *        Iterator iter = evt.getPayLoad().iterator();
248     *        while (iter.hasNext()) {
249     *            ...
250     *            ...
251     *
252     *        }
253     *     }
254     * </pre>
255     * @return payLoad, which is a SynchronizedList
256     */
257    public List<Object> getPayLoad() {
258        return payLoad;
259    }
260    public String toString(String indent) {
261        ByteArrayOutputStream baos = new ByteArrayOutputStream();
262        PrintStream p = new PrintStream (baos);
263        synchronized (getPayLoad()) {
264            dump(p, indent);
265        }
266        return baos.toString();
267    }
268    public String toString() {
269        return toString("");
270    }
271
272    public boolean hasException() {
273        return hasException;
274    }
275    /**
276     * This is a hack for backward compatibility after accepting PR67
277     * @see <a href="https://github.com/jpos/jPOS/pull/67">PR67</a>
278     * @return true if ISOSource has been set
279     */
280    public boolean isHonorSourceLogger() {
281        return honorSourceLogger;
282    }
283
284    public synchronized Instant getDumpedAt() {
285        if (dumpedAt == null)
286            dumpedAt = Instant.now();
287        return dumpedAt;
288    }
289    public Instant getCreatedAt() {
290        return createdAt;
291    }
292}