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