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