View Javadoc
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-2024 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 int; 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 OutputStream; the new output stream
65       * @return Dumper&lt;T&gt;; 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 boolean; if true; groups of three or more output lines with the significant content are compressed; if
76       *            false; no output is suppressed
77       * @return Dumper&lt;T&gt;; 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 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 int; the position where the Decoder must be added (inserted)
97       * @param decoder Decoder; the decoder to add or insert
98       * @return Dumper&lt;T&gt;; 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 String; 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 String; text to write.
120      * @param pattern String; 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 byte; the byte to append
154      * @return boolean; 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 byte[]; the bytes to append
175      * @return Dumper&lt;T&gt;; 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[]; byte array from which to take the bytes to append
186      * @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
187      *            the address that is printed before the dumped bytes)
188      * @param len int; number of bytes to append
189      * @return Dumper&lt;T&gt;; 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 InputStream; the input stream that is to be consumed
206      * @return Dumper&lt;T&gt;; 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 boolean; 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 int; 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 }