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.jpos.core.Configurable;
023import org.jpos.core.ConfigurationException;
024import org.jpos.core.XmlConfigurable;
025import org.jpos.q2.SimpleConfigurationFactory;
026import org.jpos.util.function.ByteArrayMapper;
027import org.jpos.util.function.LogEventMapper;
028
029import java.io.ByteArrayOutputStream;
030import java.io.IOException;
031import java.io.PrintStream;
032import java.util.ArrayList;
033import java.util.List;
034
035/**
036 * LogEventWriter that can be configured with event and output mappers to modify
037 * the events before writing to output stream and modify the output stream before writing
038 * to the final destination respectfully.
039 *
040 * Example configuration: <br>
041 *     <pre>{@code
042 *         <writer class="org.jpos.util.MappingLogEventWriter">
043 *             <event-mapper class="...">
044 *                 <property....  />
045 *             </event-mapper>
046 *             <event-mapper class="..."/>
047 *             <output-mapper class="...">
048 *                 <property.... />
049 *             </output-mapper>
050 *             <output-mapper class="..."/>
051 *         </writer>
052 *     }</pre>
053 *
054 * @author Alwyn Schoeman
055 * @since 2.1.4
056 */
057public class MappingLogEventWriter extends BaseLogEventWriter implements XmlConfigurable {
058    /** Creates a writer with no mappers configured; mappers are wired by {@link #setConfiguration(Element)}. */
059    public MappingLogEventWriter() {}
060    List<LogEventMapper> eventMappers;
061    List<ByteArrayMapper> outputMappers;
062    ByteArrayOutputStream captureOutputStream;
063    PrintStream capturePrintStream;
064
065    @Override
066    public void setPrintStream(PrintStream p) {
067        super.setPrintStream(p);
068        if (p != null && capturePrintStream == null) {
069            configureCaptureStreams();
070        }
071    }
072
073    @Override
074    public synchronized void close() {
075        if (capturePrintStream != null) {
076            capturePrintStream.close();
077            capturePrintStream = null;
078            captureOutputStream = null;
079        }
080        super.close();
081    }
082
083    @Override
084    public void write(LogEvent ev) {
085        ev = mapEvents(ev);
086        if (capturePrintStream != null) {
087            writeToCaptureStream(ev);
088            try {
089                byte[] output = mapOutput(captureOutputStream.toByteArray());
090                p.write(output);
091            } catch (IOException e) {
092                e.printStackTrace(p);
093            } finally {
094                p.flush();
095                captureOutputStream.reset();
096            }
097        } else {
098            delegateWriteToSuper(ev);
099        }
100    }
101
102    @Override
103    public void setConfiguration(Element e) throws ConfigurationException {
104        configureEventMappers(e);
105        configureOutputMappers(e);
106    }
107
108    /**
109     * Lazily allocates the capture stream pair when at least one output mapper is configured.
110     */
111    protected void configureCaptureStreams() {
112        if (outputMappers != null && !outputMappers.isEmpty()) {
113            captureOutputStream = new ByteArrayOutputStream();
114            capturePrintStream = new PrintStream(captureOutputStream);
115        }
116    }
117
118    /**
119     * Reads {@code <event-mapper>} children from {@code e} and instantiates each.
120     *
121     * @param e XML configuration element
122     * @throws ConfigurationException if any mapper class cannot be instantiated or configured
123     */
124    protected void configureEventMappers(Element e) throws ConfigurationException {
125        List<Element> eventMappers = e.getChildren("event-mapper");
126        LogEventMapper mapper;
127        for (Element em : eventMappers) {
128            String clazz = em.getAttributeValue("class");
129            if (clazz != null) {
130                try {
131                    mapper = (LogEventMapper) Class.forName(clazz).newInstance();
132                } catch (Exception ex) {
133                    throw new ConfigurationException(ex);
134                }
135                if (mapper != null) {
136                    if (mapper instanceof Configurable) {
137                        SimpleConfigurationFactory factory = new SimpleConfigurationFactory();
138                        ((Configurable) mapper).setConfiguration(factory.getConfiguration(em));
139                    }
140                    if (mapper instanceof XmlConfigurable) {
141                        ((XmlConfigurable) mapper).setConfiguration(em);
142                    }
143                    if (this.eventMappers == null) {
144                        this.eventMappers = new ArrayList<>();
145                    }
146                    this.eventMappers.add(mapper);
147                }
148            }
149        }
150    }
151
152    /**
153     * Reads {@code <output-mapper>} children from {@code e} and instantiates each.
154     *
155     * @param e XML configuration element
156     * @throws ConfigurationException if any mapper class cannot be instantiated or configured
157     */
158    protected void configureOutputMappers(Element e) throws ConfigurationException {
159        List<Element> outputMappers = e.getChildren("output-mapper");
160        ByteArrayMapper mapper;
161        for (Element em : outputMappers) {
162            String clazz = em.getAttributeValue("class");
163            if (clazz != null) {
164                try {
165                    mapper = (ByteArrayMapper) Class.forName(clazz).newInstance();
166                } catch (Exception ex) {
167                    throw new ConfigurationException(ex);
168                }
169                if (mapper != null) {
170                    if (mapper instanceof Configurable) {
171                        SimpleConfigurationFactory factory = new SimpleConfigurationFactory();
172                        ((Configurable) mapper).setConfiguration(factory.getConfiguration(em));
173                    }
174                    if (mapper instanceof XmlConfigurable) {
175                        ((XmlConfigurable) mapper).setConfiguration(em);
176                    }
177                    if (this.outputMappers == null) {
178                        this.outputMappers = new ArrayList<>();
179                    }
180                    this.outputMappers.add(mapper);
181                }
182            }
183        }
184    }
185
186    /**
187     * Applies every registered event mapper, in order, to {@code ev}.
188     *
189     * @param ev event to transform
190     * @return the transformed event
191     */
192    protected LogEvent mapEvents(LogEvent ev) {
193        if (eventMappers != null) {
194            for (LogEventMapper mapper : eventMappers) {
195                ev = mapper.apply(ev);
196            }
197        }
198        return ev;
199    }
200
201    /**
202     * Applies every registered output mapper, in order, to the captured output bytes.
203     *
204     * @param output bytes captured from {@link #writeToCaptureStream(LogEvent)}
205     * @return the transformed output bytes
206     */
207    protected byte[] mapOutput(byte[] output) {
208        if (outputMappers != null) {
209            for (ByteArrayMapper mapper : outputMappers) {
210                output = mapper.apply(output);
211            }
212        }
213        return output;
214    }
215
216    /**
217     * This method exists and is used so that we can verify the order of instructions
218     * during a call to write in unit tests.
219     *
220     * @param ev LogEvent to write.
221     */
222    protected void delegateWriteToSuper(LogEvent ev) {
223        super.write(ev);
224    }
225
226    /**
227     * Write to capture print stream when defined.
228     * @param ev LogEvent to write.
229     */
230    protected void writeToCaptureStream(LogEvent ev) {
231        if (capturePrintStream != null && ev != null) {
232            ev.dump(capturePrintStream, "");
233            capturePrintStream.flush();
234        }
235    }
236}