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