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<T>; 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<T>; 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<T>; 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<T>; 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<T>; 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<T>; 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 }