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-2021 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&lt;T&gt;; 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&lt;T&gt;; 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&lt;T&gt;; 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&lt;T&gt;; 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&lt;T&gt;; 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&lt;T&gt;; 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 }