1 package org.djutils.decoderdumper;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.OutputStream;
6 import java.util.ArrayList;
7 import java.util.List;
8
9 /**
10 * Common code for all (decoder-) Dumpers.
11 * <p>
12 * Copyright (c) 2013-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
13 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
14 * </p>
15 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
16 * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
17 * @param <T> Type of dumper
18 */
19 public class Dumper<T>
20 {
21 /** The (currently active) decoders. */
22 private List<Decoder> decoders = new ArrayList<>();
23
24 /** The address of the next byte. */
25 private int address = 0;
26
27 /** Output stream for completed output lines. */
28 private OutputStream outputStream = System.out;
29
30 /** If true, 3 or more output lines containing the same 16 bytes are compressed. */
31 private boolean suppressMultipleIdenticalLines = false;
32
33 /** Number of identical lines in output. */
34 private int suppressedCount = 0;
35
36 /** Used in conjunction with <code>suppressMultipleIdenticalLines</code>. */
37 private String lastPattern = "";
38
39 /** Used in conjunction with <code>suppressMultipleIdenticalLines</code>. */
40 private String lastOutput = "";
41
42 /** Line that is output to indicate where one or more output lines were suppressed. */
43 private static final String SUPPRESSEDOUTPUTINDICATORLINE = "*\n";
44
45 /**
46 * Construct a new Dumper.
47 * @param addressOffset address for the first byte that will be appended
48 */
49 public Dumper(final int addressOffset)
50 {
51 this.address = addressOffset;
52 }
53
54 /**
55 * Construct a new Dumper with addressOffset 0.
56 */
57 public Dumper()
58 {
59 this(0);
60 }
61
62 /**
63 * Set or replace the active output stream. (The default output stream is <code>System.out</code>.)
64 * @param newOutputStream the new output stream
65 * @return this Dumper object (for method chaining)
66 */
67 public Dumper<T> setOutputStream(final OutputStream newOutputStream)
68 {
69 this.outputStream = newOutputStream;
70 return this;
71 }
72
73 /**
74 * Set the output compression mode.
75 * @param newState if true; groups of three or more output lines with the significant content are compressed; if
76 * false; no output is suppressed
77 * @return this Dumper object (for method chaining)
78 */
79 public Dumper<T> setSuppressMultipleIdenticalLines(final boolean newState)
80 {
81 this.suppressMultipleIdenticalLines = newState;
82 return this;
83 }
84
85 /**
86 * Add a Decoder at the end of the current list of decoders.
87 * @param decoder the decoder to add or insert
88 */
89 public void addDecoder(final Decoder decoder)
90 {
91 this.decoders.add(decoder);
92 }
93
94 /**
95 * Add a Decoder at a specified index.
96 * @param index the position where the Decoder must be added (inserted)
97 * @param decoder the decoder to add or insert
98 * @return this Dumper object (for method chaining)
99 * @throws IndexOutOfBoundsException when the provided index is invalid
100 */
101 public Dumper<T> addDecoder(final int index, final Decoder decoder) throws IndexOutOfBoundsException
102 {
103 this.decoders.add(index, decoder);
104 return this;
105 }
106
107 /**
108 * Write some output.
109 * @param outputText text to write.
110 * @throws IOException when an outputStream has been set and it throws an IOException
111 */
112 private void writeOutput(final String outputText) throws IOException
113 {
114 this.outputStream.write(outputText.getBytes("UTF-8"));
115 }
116
117 /**
118 * Write some output, applying suppression of multiple lines with the same dumped bytes (if that option is active).
119 * @param outputText text to write.
120 * @param pattern pattern that should be used to check for multiple identical output lines
121 * @throws IOException when an outputStream has been set and it throws an IOException
122 */
123 private void writeFilteringOutput(final String outputText, final String pattern) throws IOException
124 {
125 if (this.suppressedCount > 0 && ((!this.suppressMultipleIdenticalLines) || (!pattern.equals(this.lastPattern))))
126 {
127 // We have suppressed output lines AND (suppressing is now OFF OR pattern != lastPattern)
128 if (!outputText.equals(this.lastPattern))
129 {
130 writeOutput(this.lastOutput);
131 }
132 this.suppressedCount = 0;
133 }
134 this.lastOutput = outputText;
135 if ((!this.suppressMultipleIdenticalLines) || (!pattern.equals(this.lastPattern)))
136 {
137 writeOutput(outputText);
138 }
139 else
140 {
141 // Suppress this output
142 if (1 == this.suppressedCount++)
143 {
144 // Write the suppressed output indicator line
145 writeOutput(SUPPRESSEDOUTPUTINDICATORLINE);
146 }
147 }
148 this.lastPattern = pattern;
149 }
150
151 /**
152 * Append one byte to this dump.
153 * @param theByte the byte to append
154 * @return true if output was generated; false if the byte was accumulated, but did not result in immediate output
155 * @throws IOException when output was generated and writing to the output stream generated an IOException
156 */
157 public boolean append(final byte theByte) throws IOException
158 {
159 boolean needFlush = false;
160 for (Decoder decoder : this.decoders)
161 {
162 needFlush |= decoder.append(this.address, theByte);
163 }
164 this.address++;
165 if (needFlush)
166 {
167 return flush();
168 }
169 return false;
170 }
171
172 /**
173 * Append an array of bytes.
174 * @param bytes the bytes to append
175 * @return this Dumper object (for method chaining)
176 * @throws IOException when an outputStream has been set and it throws an IOException
177 */
178 public Dumper<T> append(final byte[] bytes) throws IOException
179 {
180 return append(bytes, 0, bytes.length);
181 }
182
183 /**
184 * Append a slice of an array of bytes.
185 * @param bytes byte array from which to take the bytes to append
186 * @param start index of first byte in <code>bytes</code> to append (NB. using non-zero does <b>not</b> cause a jump in
187 * the address that is printed before the dumped bytes)
188 * @param len number of bytes to append
189 * @return this Dumper object (for method chaining)
190 * @throws IOException when an outputStream has been set and it throws an IOException
191 */
192 public Dumper<T> append(final byte[] bytes, final int start, final int len) throws IOException
193 {
194 for (int pos = start; pos < start + len; pos++)
195 {
196 append(bytes[pos]);
197 }
198 return this;
199 }
200
201 /**
202 * Consume an entire input stream and append what it produces to this Dumpmer. The input stream is <b>not</b> closed by this
203 * <code>append</code> method. This method does not return until the <code>inputStream</code> returns end of file, or throws
204 * an IOException (which is - actually - not a return to the caller, but a jump to the closest handler for that exception).
205 * @param inputStream the input stream that is to be consumed
206 * @return this Dumper object (for method chaining)
207 * @throws IOException when the <code>inputStream</code> throws that exception, or when an output stream has been set and
208 * that throws an IOException
209 */
210 public Dumper<T> append(final InputStream inputStream) throws IOException
211 {
212 byte[] buffer = new byte[8192];
213 int read;
214 while ((read = inputStream.read(buffer)) >= 0)
215 {
216 append(buffer, 0, read);
217 }
218 return this;
219 }
220
221 /**
222 * Force the currently assembled output to be written (write partial result if the output line currently being assembled is
223 * not full).
224 * @return true if output was generated; false if no output was generated
225 * @throws IOException when output was generated and writing to the output stream generated an IOException
226 */
227 public boolean flush() throws IOException
228 {
229 StringBuilder result = new StringBuilder();
230 StringBuilder pattern = new StringBuilder();
231 int totalReturnedWidth = 0;
232 for (Decoder decoder : this.decoders)
233 {
234 String part = decoder.getResult();
235 totalReturnedWidth += part.length();
236 if (part.length() < decoder.getMaximumWidth())
237 {
238 String format = String.format("%%-%ds", decoder.getMaximumWidth());
239 part = String.format(format, part);
240 }
241 result.append(part);
242 if (!decoder.ignoreForIdenticalOutputCheck())
243 {
244 pattern.append(part);
245 }
246 }
247 writeFilteringOutput(totalReturnedWidth == 0 ? "" : result.toString(), pattern.toString());
248 return totalReturnedWidth > 0;
249 }
250
251 /**
252 * Return the maximum width of an output line.
253 * @return the maximum width of an output line
254 */
255 public int getMaximumWidth()
256 {
257 int result = 0;
258 for (Decoder decoder : this.decoders)
259 {
260 result += decoder.getMaximumWidth();
261 }
262 return result;
263 }
264
265 @Override
266 public String toString()
267 {
268 return "Dumper [decoders=" + this.decoders + ", address=" + this.address + ", outputStream=" + this.outputStream
269 + ", suppressMultipleIdenticalLines=" + this.suppressMultipleIdenticalLines + ", suppressedCount="
270 + this.suppressedCount + ", lastPattern=" + this.lastPattern + "]";
271 }
272
273 }