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-2020 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 final 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&lt;T&gt;; 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&lt;T&gt;; 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&lt;T&gt;; 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("UTF-8"));
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&lt;T&gt;; 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&lt;T&gt;; 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&lt;T&gt;; 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 }