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-2022 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 }