├── schemas ├── classes │ └── classes │ │ ├── copy-class-files-here │ │ └── protobuf.properties ├── README.md └── build-custom-docker-with-schemas.md ├── lib ├── sparkplug-b.jar ├── solace-dt-protobufs.jar ├── jcsmp-topic-dispatch-0.0.1.jar └── aaron-solace-useful-utils-0.1.2.jar ├── src ├── browse-msgs.png ├── prettydump3.jpg ├── prettydump3.png ├── prettydump4.png ├── prettydump5.png ├── test │ └── java │ │ └── com │ │ └── solace │ │ └── labs │ │ └── aaron │ │ ├── AaronCharsetDecoder.java │ │ ├── RegexTests.java │ │ ├── ConsoleEcho.java │ │ ├── ConsoleSize.java │ │ ├── JsonTests.java │ │ ├── CodePointTests.java │ │ ├── LotsOfSubTopics.java │ │ ├── TestStuff.java │ │ └── ReportingCharsetDecoder.java └── main │ ├── java │ └── com │ │ └── solace │ │ └── labs │ │ └── aaron │ │ ├── SaxParserException.java │ │ ├── Col.java │ │ ├── utils │ │ └── DecoderUtils.java │ │ ├── SystemOutHelper.java │ │ ├── KeyboardHandler.java │ │ ├── ThinkingAnsiHelper.java │ │ ├── SaxHandler.java │ │ ├── SaxParser.java │ │ ├── GsonUtils.java │ │ ├── Banner.java │ │ ├── Elem.java │ │ ├── ConfigState.java │ │ ├── SdtUtils.java │ │ ├── HelperText.java │ │ ├── PrettyWrap.java │ │ ├── ProtoBufUtils.java │ │ └── PayloadSection.java │ └── resources │ └── log4j2.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitattributes ├── .gitignore ├── settings.gradle.kts ├── scripts ├── show-last-nth-msg-spool-id.sh ├── show-last-nth-rgmid.sh └── copy-last-n-msgs.sh ├── Dockerfile ├── gradlew.bat ├── gradlew └── LICENSE /schemas/classes/classes/copy-class-files-here: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/sparkplug-b.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/lib/sparkplug-b.jar -------------------------------------------------------------------------------- /src/browse-msgs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/src/browse-msgs.png -------------------------------------------------------------------------------- /src/prettydump3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/src/prettydump3.jpg -------------------------------------------------------------------------------- /src/prettydump3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/src/prettydump3.png -------------------------------------------------------------------------------- /src/prettydump4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/src/prettydump4.png -------------------------------------------------------------------------------- /src/prettydump5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/src/prettydump5.png -------------------------------------------------------------------------------- /lib/solace-dt-protobufs.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/lib/solace-dt-protobufs.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /lib/jcsmp-topic-dispatch-0.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/lib/jcsmp-topic-dispatch-0.0.1.jar -------------------------------------------------------------------------------- /lib/aaron-solace-useful-utils-0.1.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolaceLabs/solace-pretty-dump/HEAD/lib/aaron-solace-useful-utils-0.1.2.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | 4 | *.java text eol=lf 5 | 6 | 7 | 8 | 9 | # These are explicitly windows files and should use crlf 10 | *.bat text eol=crlf 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | libs/ 4 | .DS_Store 5 | _site/ 6 | .sass-cache/ 7 | .classpath 8 | .project 9 | .settings/ 10 | /bin/ 11 | *.class 12 | hs_err_pid* 13 | _site 14 | .DS_Store 15 | docs/_site 16 | .idea 17 | log/ 18 | *.ignore 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.8.3/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = "prettydump" 11 | //include("app") 12 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/AaronCharsetDecoder.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.CharBuffer; 5 | import java.nio.charset.Charset; 6 | import java.nio.charset.CharsetDecoder; 7 | import java.nio.charset.CoderResult; 8 | 9 | public class AaronCharsetDecoder extends CharsetDecoder { 10 | 11 | public AaronCharsetDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) { 12 | super(cs, averageCharsPerByte, maxCharsPerByte); 13 | // TODO Auto-generated constructor stub 14 | } 15 | 16 | @Override 17 | protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { 18 | // TODO Auto-generated method stub 19 | return null; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/RegexTests.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public class RegexTests { 7 | 8 | 9 | 10 | public static void main(String... args) { 11 | 12 | String 13 | key = "blahMs"; 14 | 15 | String regex = "M[sS]"; 16 | 17 | System.out.println(key.matches(regex)); 18 | System.out.println(key.contains(regex)); 19 | 20 | Pattern p = Pattern.compile(regex); 21 | Matcher m = p.matcher(key); 22 | System.out.println(m.find()); 23 | 24 | key = "msDuration"; 25 | // regex = "[^\\{Alpha}]ms"; 26 | regex = "(?:^|[^a-zA-Z])ms(?:$|ec|[^a-zA-Z])"; // non-capturing group(either start or non-alpha) + ms + non-cg(either end or non-alpha) 27 | 28 | p = Pattern.compile(regex); 29 | m = p.matcher(key); 30 | System.out.println(m.find()); 31 | 32 | String ansi = "\u001b[2;30m] color "; 33 | Pattern p2 = Pattern.compile("\u001b\\[[0-9; ]*m"); 34 | // String ansi = "\u001b[2;30m] color "; 35 | // Pattern p2 = Pattern.compile("\u001b"); 36 | System.out.println(p2); 37 | System.out.println(p2.matcher(ansi).find()); 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/SaxParserException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | public class SaxParserException extends Exception { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | public SaxParserException(String message) { 24 | super(message); 25 | } 26 | 27 | public SaxParserException(Throwable cause) { 28 | super(cause); 29 | } 30 | 31 | public SaxParserException(String message,Throwable cause) { 32 | super(message,cause); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/ConsoleEcho.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | 7 | public class ConsoleEcho { 8 | 9 | 10 | public static void main(String... args) throws IOException, InterruptedException { 11 | 12 | 13 | // System.out.println("starting..."); 14 | //// BufferedInputStream bim = new BufferedInputStream(System.in); 15 | // InputStreamReader reader = new InputStreamReader(System.in); 16 | // BufferedReader in = new BufferedReader(reader); 17 | // 18 | // while (true) { 19 | // 20 | // String input = in.readLine(); 21 | // if (input == null) { // nothing to do 22 | // Thread.sleep(50); 23 | // } else { 24 | // System.out.println("con: " + input); 25 | // } 26 | // } 27 | 28 | InputStreamReader reader = new InputStreamReader(System.in); 29 | 30 | 31 | while (true) { 32 | 33 | int a = reader.read(); 34 | System.out.println(a); 35 | 36 | // if (System.in.available() > 0) { 37 | // int c = System.in.read(); 38 | // System.out.println(c); 39 | // } 40 | 41 | 42 | 43 | } 44 | 45 | 46 | 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/ConsoleSize.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import org.fusesource.jansi.Ansi; 4 | import org.fusesource.jansi.AnsiConsole; 5 | 6 | public class ConsoleSize { 7 | 8 | 9 | 10 | public static void main(String... args) throws InterruptedException { 11 | 12 | System.out.println(Ansi.isDetected()); 13 | System.out.println(Ansi.isEnabled()); 14 | 15 | AnsiConsole.systemInstall(); 16 | 17 | 18 | Ansi ansi = new Ansi(); 19 | 20 | System.out.println(Ansi.isDetected()); 21 | System.out.println(Ansi.isEnabled()); 22 | // cursor stuff doesn't work! either linux or Cmd prompt 23 | ansi.saveCursorPosition(); 24 | Thread.sleep(1000); 25 | ansi.cursorRight(10); 26 | AnsiConsole.out().print("1"); 27 | Thread.sleep(1000); 28 | ansi.cursorLeft(2); 29 | AnsiConsole.out().print("2"); 30 | Thread.sleep(1000); 31 | ansi.cursorLeft(2); 32 | AnsiConsole.out().print("3"); 33 | Thread.sleep(1000); 34 | ansi.cursorLeft(2); 35 | AnsiConsole.out().print("4"); 36 | Thread.sleep(1000); 37 | ansi.cursorLeft(2); 38 | AnsiConsole.out().print("5"); 39 | Thread.sleep(1000); 40 | 41 | 42 | ansi.restoreCursorPosition(); 43 | 44 | ansi.a("back at the beginning"); 45 | 46 | System.out.println(ansi.toString()); 47 | 48 | 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/Col.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | public class Col { 20 | 21 | 22 | final int value; 23 | final boolean faint; 24 | final boolean italics; 25 | 26 | public Col(int value) { 27 | this.value = value; 28 | faint = false; 29 | italics = false; 30 | } 31 | 32 | public Col(int value, boolean faint) { 33 | this.value = value; 34 | this.faint = faint; 35 | italics = false; 36 | } 37 | 38 | public Col(int value, boolean faint, boolean italics) { 39 | this.value = value; 40 | this.faint = faint; 41 | this.italics = italics; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return (faint ? "Faint " : "") + value; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/JsonTests.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import java.util.Iterator; 4 | 5 | import org.json.JSONObject; 6 | import org.junit.Test; 7 | 8 | import com.google.gson.JsonObject; 9 | 10 | public class JsonTests { 11 | 12 | 13 | 14 | 15 | @Test 16 | public void test1() { 17 | JsonObject object = new JsonObject(); 18 | 19 | for (int i=50;i>0;i--) { 20 | object.addProperty(Integer.toString(i), i); 21 | } 22 | // object.addProperty("1", 1); 23 | // object.addProperty("2", 2); 24 | // object.addProperty("3", 3); 25 | // object.addProperty("4", 4); 26 | // object.addProperty("5", 5); 27 | // object.addProperty("6", 6); 28 | // object.addProperty("7", 7); 29 | // object.addProperty("8", 8); 30 | // object.addProperty("9", 9); 31 | System.out.println("With com.google.gson.JsonObject"); 32 | System.out.println(object.toString()); 33 | } 34 | 35 | @Test 36 | public void test2() { 37 | JSONObject object = new JSONObject(); 38 | object.put("1", 1); 39 | object.put("2", 2); 40 | object.put("3", 3); 41 | object.put("4", 4); 42 | object.put("5", 5); 43 | object.put("6", 6); 44 | object.put("7", 7); 45 | object.put("8", 8); 46 | object.put("9", 9); 47 | System.out.println(object.toString()); 48 | // verify order 49 | Iterator i = object.keys(); 50 | int index = 1; 51 | while (i.hasNext()) { 52 | int val = object.getInt(i.next()); 53 | // if (val != index) 54 | } 55 | } 56 | 57 | 58 | 59 | 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/utils/DecoderUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron.utils; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.nio.CharBuffer; 21 | import java.nio.charset.CharacterCodingException; 22 | import java.nio.charset.CharsetDecoder; 23 | 24 | public class DecoderUtils { 25 | 26 | public static String decodeToString(CharsetDecoder decoder, byte[] bytes) { 27 | CharBuffer cb; 28 | try { 29 | // won't throw since the decoder is "REPLACE" and not "REPORT" 30 | cb = decoder.decode(ByteBuffer.wrap(bytes)); 31 | return cb.toString(); 32 | } catch (CharacterCodingException e) { 33 | // can't get here now 34 | throw new AssertionError("Could not decode bytes to charset",e); 35 | } 36 | } 37 | 38 | public static String decodeToString(CharsetDecoder decoder, ByteBuffer buffer) { 39 | try { 40 | // won't throw since the decoder is "REPLACE" and not "REPORT" 41 | CharBuffer cb = decoder.decode(buffer); 42 | return cb.toString(); 43 | } catch (CharacterCodingException e) { 44 | // can't get here now 45 | throw new AssertionError("Could not decode bytes to charset",e); 46 | } 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/SystemOutHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | public class SystemOutHelper { 23 | 24 | StringBuilder col = new StringBuilder(); 25 | StringBuilder bw = new StringBuilder(); 26 | 27 | public SystemOutHelper print(String s) { 28 | col.append(s); 29 | bw.append(s); 30 | return this; 31 | } 32 | 33 | public SystemOutHelper print(AaAnsi ansi) { 34 | col.append(ansi.toString()); 35 | bw.append(ansi.toRawString()); 36 | return this; 37 | } 38 | 39 | public SystemOutHelper println(String s) { 40 | col.append(s).append('\n'); 41 | bw.append(s).append('\n'); 42 | return this; 43 | } 44 | 45 | public SystemOutHelper println() { 46 | col.append('\n'); 47 | bw.append('\n'); 48 | return this; 49 | } 50 | 51 | public SystemOutHelper println(AaAnsi ansi) { 52 | col.append(ansi.toString()).append('\n'); 53 | bw.append(ansi.toRawString()).append('\n'); 54 | return this; 55 | } 56 | 57 | public boolean containsRegex(Pattern p) { 58 | Matcher m = p.matcher(bw.toString()); 59 | return m.find(); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return col.toString(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/CodePointTests.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | public class CodePointTests { 6 | 7 | 8 | 9 | public static void main(String... args) { 10 | 11 | 12 | for (int i=0; i< 128; i++) { 13 | // System.out.println(i + ":" 14 | // + ", control=" + Character.isISOControl(i) 15 | // + ", defined=" + Character.isDefined(i) 16 | // + ", ignoreIdent=" + Character.isIdentifierIgnorable(i) 17 | // + ", whitespace=" + Character.isWhitespace(i) 18 | // ); 19 | 20 | 21 | String s = new String(new byte[] { (byte)(i) }, StandardCharsets.UTF_8); 22 | char c = s.charAt(0); 23 | // System.out.println(c); 24 | if (c == '') { 25 | System.out.print("* "); 26 | } 27 | System.out.println((int)c + ": '" + c + "':" 28 | + ", control=" + Character.isISOControl(i) 29 | + ", defined=" + Character.isDefined(i) 30 | + ", ignoreIdent=" + Character.isIdentifierIgnorable(i) 31 | + ", whitespace=" + Character.isWhitespace(i) 32 | ); 33 | } 34 | for (int i=0; i< 128; i++) { 35 | // System.out.println(i + ":" 36 | // + ", control=" + Character.isISOControl(i) 37 | // + ", defined=" + Character.isDefined(i) 38 | // + ", ignoreIdent=" + Character.isIdentifierIgnorable(i) 39 | // + ", whitespace=" + Character.isWhitespace(i) 40 | // ); 41 | 42 | 43 | String s = new String(new byte[] { (byte)(i-128) }, StandardCharsets.ISO_8859_1); 44 | char c = s.charAt(0); 45 | if (c == '�') { 46 | System.out.print("* "); 47 | } 48 | // System.out.println(c); 49 | // System.out.println((i+128) + ": " + c + ":" 50 | System.out.println((int)c + ": " + c + ":" 51 | + ", control=" + Character.isISOControl((int)c) 52 | + ", defined=" + Character.isDefined((int)c) 53 | + ", ignoreIdent=" + Character.isIdentifierIgnorable((int)c) 54 | + ", whitespace=" + Character.isWhitespace((int)c) 55 | ); 56 | } 57 | 58 | 59 | 60 | 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %d %p %c [%t] %m%n 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /scripts/show-last-nth-msg-spool-id.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # leave these values hardcode here, or make empty to have prompts 4 | HOST="http://localhost:8080" 5 | USER="admin" 6 | PW="admin" 7 | VPN="default" 8 | QUEUE="" 9 | NUM=0 10 | 11 | echo "┌ This utility returns the Message Spool ID of a message from the back of ┐" 12 | echo "│ a queue, the nth-newest message. This ID can then be used with a queue │" 13 | echo "│ browser to skip all the messages until then. Requires curl and xmllint. │" 14 | echo "└─────────────────────────────────────────────────────────────────────────┘" 15 | 16 | # read in some vars, or hardcode them above 17 | if [[ $HOST == "" ]]; then 18 | echo -n "SEMP URL (e.g. https://aaron.messaging.solace.cloud:943): " 19 | read HOST 20 | fi 21 | if [[ $USER == "" ]]; then 22 | echo -n "SEMP username: " 23 | read USER 24 | fi 25 | if [[ $PW == "" ]]; then 26 | echo -n "SEMP password: " 27 | stty -echo 28 | read PW 29 | stty echo 30 | echo 31 | fi 32 | if [[ $VPN == "" ]]; then 33 | echo -n "Message VPN: " 34 | read VPN 35 | fi 36 | if [[ $QUEUE == "" ]]; then 37 | echo -n "Queue: " 38 | read QUEUE 39 | fi 40 | if [[ $NUM == 0 ]]; then 41 | echo -n "Get Message Spool ID for nth-last message: n=" 42 | read NUM 43 | fi 44 | 45 | # here we go, try to get message details... 46 | 47 | RGMIDs=`curl -s --compressed -u $USER:$PW $HOST/SEMP -d "$QUEUE$VPN$NUM" | xmllint --xpath '//message-id/text()' - ` 48 | 49 | # uncomment this to dump them all to the console 50 | #echo $RGMIDs 51 | 52 | if [[ $RGMIDs == "" ]]; then 53 | echo "## Problem with SEMP request, probably too many messages! Try less than 28000." 54 | echo "## (or modify this script to support paging via )" 55 | exit 1 56 | fi 57 | 58 | IFS=$'\n' 59 | names=($RGMIDs) 60 | 61 | echo 62 | echo "The MsgSpoolID of the ${#names[@]}th-last message on queue '$QUEUE' is: ${names[${#names[@]}-1]} " 63 | -------------------------------------------------------------------------------- /scripts/show-last-nth-rgmid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # leave these values hardcode here, or make empty to have prompts 4 | HOST="http://localhost:8080" 5 | USER="admin" 6 | PW="admin" 7 | VPN="default" 8 | QUEUE="" 9 | NUM=0 10 | 11 | echo "┌ This utility returns the Replication Group Message ID of a message from the ──┐" 12 | echo "│ back of a queue, the nth-newest message. This ID can then be used with a │" 13 | echo "│ queue browser to skip all the messages until then. Requires curl and xmllint. │" 14 | echo "└───────────────────────────────────────────────────────────────────────────────┘" 15 | 16 | # read in some vars, or hardcode them above 17 | if [[ $HOST == "" ]]; then 18 | echo -n "SEMP URL (e.g. https://aaron.messaging.solace.cloud:943): " 19 | read HOST 20 | fi 21 | if [[ $USER == "" ]]; then 22 | echo -n "SEMP username: " 23 | read USER 24 | fi 25 | if [[ $PW == "" ]]; then 26 | echo -n "SEMP password: " 27 | stty -echo 28 | read PW 29 | stty echo 30 | echo 31 | fi 32 | if [[ $VPN == "" ]]; then 33 | echo -n "Message VPN: " 34 | read VPN 35 | fi 36 | if [[ $QUEUE == "" ]]; then 37 | echo -n "Queue: " 38 | read QUEUE 39 | fi 40 | if [[ $NUM == 0 ]]; then 41 | echo -n "Get Replication Group Message ID for nth-last message: n=" 42 | read NUM 43 | fi 44 | 45 | # here we go, try to get message details... 46 | 47 | RGMIDs=`curl -s --compressed -u $USER:$PW $HOST/SEMP -d "$QUEUE$VPN$NUM" | xmllint --xpath '//replication-group-msg-id/text()' - ` 48 | 49 | # uncomment this to dump them all to console 50 | #echo $RGMIDs 51 | 52 | if [[ $RGMIDs == "" ]]; then 53 | echo "## Problem with SEMP request, probably too many messages! Try less than 4900." 54 | echo "## (or modify this script to support paging via )" 55 | exit 1 56 | fi 57 | 58 | IFS=$'\n' 59 | names=($RGMIDs) 60 | 61 | echo 62 | echo "The RGMID of the ${#names[@]}th-last message on queue '$QUEUE' is: ${names[${#names[@]}-1]} " 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Download a Docker tarball from https://github.com/SolaceLabs/solace-pretty-dump/releases 2 | # Load into Docker Images with: docker load -i solace-pretty-dump-docker-1.1.1.tar.gz 3 | # 4 | # To RUN: 5 | # 6 | # docker run -it --rm solace-pretty-dump:latest broker.messaging.solace.cloud vpn-name user pw ">" -1 7 | # 8 | # or make an alias: pretty='docker run -it --rm solace-pretty-dump:latest' 9 | # 10 | # 11 | # To RUN in "wrap" mode around SdkPerf: (can't use -t pseudo-TTY mode since pipe | doesn't work 12 | # 13 | # ./sdkperf_java.sh -cip=0 -sql=q1 -md | docker run -i --rm solace-pretty-dump:latest wrap 14 | # 15 | # (maybe one day something like this could work: https://stackoverflow.com/questions/1401002/how-to-trick-an-application-into-thinking-its-stdout-is-a-terminal-not-a-pipe ) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | # ignore the rest, this is for Aaron 24 | 25 | # To BUILD: 26 | # 27 | # 1) ./gradlew clean assemble (or download a pre-built release from GitHub) 28 | # 2) docker build -t solace-pretty-dump:latest -t solace-pretty-dump:1.1.1 --file Dockerfile . 29 | # 30 | # Optional, for distribution: 31 | # 32 | # 3) docker save solace-pretty-dump:latest | gzip > solace-pretty-dump-docker-x.y.z.tar.gz 33 | # 34 | 35 | 36 | # This is where the actual Dockerfile starts 37 | 38 | FROM amazoncorretto:22-alpine as corretto-jdk 39 | #FROM amazoncorretto:17-alpine as corretto-jdk 40 | 41 | # required for strip-debug to work 42 | RUN apk add --no-cache binutils 43 | 44 | # Build small JRE image 45 | RUN jlink \ 46 | --add-modules ALL-MODULE-PATH \ 47 | --strip-debug \ 48 | --no-man-pages \ 49 | --no-header-files \ 50 | --compress=2 \ 51 | --output /jre 52 | 53 | FROM alpine:latest 54 | ENV JAVA_HOME=/jre 55 | ENV PATH="${JAVA_HOME}/bin:${PATH}" 56 | 57 | COPY --from=corretto-jdk /jre $JAVA_HOME 58 | 59 | RUN mkdir -p /opt/pretty 60 | WORKDIR /opt/pretty 61 | 62 | # after doing ./gradlew assemble, unzip a distribution, and then this will copy all the build/staged into the Docker image 63 | COPY build/staged/ ./ 64 | 65 | ENTRYPOINT ["./bin/prettydump"] 66 | 67 | ENV TERM="xterm-256color" 68 | 69 | LABEL version=1.1.1 70 | LABEL author="Aaron @ Solace" 71 | LABEL repo="https://github.com/SolaceLabs/solace-pretty-dump" 72 | -------------------------------------------------------------------------------- /schemas/classes/classes/protobuf.properties: -------------------------------------------------------------------------------- 1 | # Topic to ProtoBuf class mapping 2 | 3 | # Format is: = 4 | 5 | # This application uses Solace SMF style topic wildcard matching, and performs topic dispatch per message. 6 | # 7 | # Use of wildcards allowed: 8 | # * single-level wildcard, matches 0-or-more characters, up to the next topic level /, can be prefixed 9 | # e.g. aaa/*/ccc; aaa/b*/ccc; */bb*/*; will all match topic aaa/bbb/ccc 10 | # invalid format: a*a; aaa/*bb/ccc 11 | # > multi-level wildcard, matches 1-or-more levels, must occur at very end of subscription, cannot be prefixed 12 | # e.g. aaa/>; aaa/bbb/>; >; will all match topic aaa/bbb/ccc 13 | # invalid format: aaa/b>; aaa/>/ccc 14 | # # borrowed from MQTT, multi-level wildcard, matches 0-or-more levels, must occur at very end of a subscription 15 | # e.g. aaa/bbb/#; aaa/bbb/ccc/#; #; will all match topic aaa/bbb/ccc 16 | # 17 | 18 | 19 | # These two are the Solace broker Distributed Trace / Open Telemetry message formats 20 | # the only way to view these messages is to connect PrettyDump directly to the `#telemetry-...` queue created when 21 | # configuring Distributed Trace within a Message VPN. 22 | # Note the special wildcard char # at the end of the subscription, which will match 0-or-more levels 23 | _telemetry/broker/trace/egress/v1/# = solace.messaging.proto.broker.trace.egress.v1.EgressV1$SpanData 24 | _telemetry/broker/trace/receive/v1/# = solace.messaging.proto.broker.trace.receive.v1.ReceiveV1$SpanData 25 | 26 | 27 | # Sparkplug B spec: https://sparkplug.eclipse.org/specification/version/2.2/documents/sparkplug-specification-2.2.pdf 28 | # page 12 for topic definition 29 | spBv1.0/> = com.cirruslink.sparkplug.protobuf.SparkplugBProto$Payload 30 | 31 | 32 | # OpenTelemetry OTLP Protobuf definitions, from snooping the OTel Collector -> backend (e.g. Jaeger) using HTTP Gateway mode 33 | # https://opentelemetry.io/docs/specs/otlp/ 34 | # https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md 35 | # https://central.sonatype.com/artifact/io.opentelemetry.proto/opentelemetry-proto?smo=true 36 | # Microgatway mode first 37 | POST/v1/traces = io.opentelemetry.proto.trace.v1.TracesData 38 | # REST Messaging mode 39 | v1/traces = io.opentelemetry.proto.trace.v1.TracesData 40 | -------------------------------------------------------------------------------- /scripts/copy-last-n-msgs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # leave these values hardcode here, or make empty to have prompts 4 | HOST="http://localhost:8080" 5 | USER="admin" 6 | PW="admin" 7 | VPN="default" 8 | S_QUEUE="" 9 | D_QUEUE="" 10 | NUM=0 11 | 12 | echo "┌ This utility copies the last/newest n messages from one queue to another. ┐" 13 | echo "│ It uses SEMPv1 to retrieve message details (RGMIDs) and SEMPv1 again to │" 14 | echo "│ copy each message one-by-one. Requires curl and xmllint. │" 15 | echo "└───────────────────────────────────────────────────────────────────────────┘" 16 | 17 | # read in some vars, or hardcode them above 18 | if [[ $HOST == "" ]]; then 19 | echo -n "SEMP URL (e.g. https://aaron.messaging.solace.cloud:943): " 20 | read HOST 21 | fi 22 | if [[ $USER == "" ]]; then 23 | echo -n "SEMP username: " 24 | read USER 25 | fi 26 | if [[ $PW == "" ]]; then 27 | echo -n "SEMP password: " 28 | stty -echo 29 | read PW 30 | stty echo 31 | echo 32 | fi 33 | if [[ $VPN == "" ]]; then 34 | echo -n "Message VPN: " 35 | read VPN 36 | fi 37 | if [[ $S_QUEUE == "" ]]; then 38 | echo -n "Source Queue: " 39 | read S_QUEUE 40 | fi 41 | if [[ $D_QUEUE == "" ]]; then 42 | echo -n "Dest Queue: " 43 | read D_QUEUE 44 | fi 45 | if [[ $NUM == 0 ]]; then 46 | echo -n "Number of messages from back of queue: " 47 | read NUM 48 | fi 49 | 50 | # here we go, try to get message details... 51 | 52 | RGMIDs=`curl -s --compressed -u $USER:$PW $HOST/SEMP -d "$S_QUEUE$VPN$NUM" | xmllint --xpath '//replication-group-msg-id/text()' -` 53 | 54 | if [[ $RGMIDs == "" ]]; then 55 | echo "## Problem with SEMP request, probably too many messages! Try less than 4900." 56 | echo "## (or modify this script to support paging via )" 57 | exit 1 58 | fi 59 | 60 | echo 61 | echo "SEMP call successful. Copying messages..." 62 | 63 | IFS=$'\n' 64 | names=($RGMIDs) 65 | 66 | COUNT=0; 67 | # change this to a regular forward for loop to copy the messages newest-to-oldest 68 | for (( i=0; i < ${#names[@]}; i++ )) # reverse order (newest-to-oldest) 69 | #for (( i=${#names[@]}-1; i >= 0; i-- )) # original order (oldest-to-newest) 70 | do 71 | #echo "$i: ${names[$i]}" 72 | RESP=`curl -q -s -u $USER:$PW $HOST/SEMP -d "$VPN$S_QUEUE$D_QUEUE${names[$i]}"` 73 | if [[ $RESP != *"ok"* ]]; then 74 | echo "Had problems with ${names[$i]}:" 75 | echo $RESP 76 | echo 77 | echo "$COUNT messages (out of $NUM) successfully copied." 78 | echo "Quitting!" 79 | exit 1 80 | fi 81 | ((COUNT++)); 82 | done 83 | 84 | echo "Last $COUNT messages successfully copied from Queue '$S_QUEUE'." 85 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/LotsOfSubTopics.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import com.solacesystems.jcsmp.JCSMPException; 4 | import com.solacesystems.jcsmp.JCSMPFactory; 5 | import com.solacesystems.jcsmp.JCSMPProperties; 6 | import com.solacesystems.jcsmp.JCSMPSession; 7 | import com.solacesystems.jcsmp.Queue; 8 | import com.solacesystems.jcsmp.SessionEventArgs; 9 | import com.solacesystems.jcsmp.SessionEventHandler; 10 | import com.solacesystems.jcsmp.Topic; 11 | 12 | public class LotsOfSubTopics { 13 | 14 | 15 | public static void main(String... args) throws JCSMPException { 16 | JCSMPProperties properties = new JCSMPProperties(); 17 | properties.setProperty(JCSMPProperties.HOST, args[0]); 18 | properties.setProperty(JCSMPProperties.VPN_NAME, args[1]); // message-vpn 19 | properties.setProperty(JCSMPProperties.USERNAME, args[2]); // client-username 20 | properties.setProperty(JCSMPProperties.PASSWORD, args[3]); // client-password 21 | final String queueName = args[4]; 22 | 23 | JCSMPSession session = JCSMPFactory.onlyInstance().createSession(properties, null, new SessionEventHandler() { 24 | 25 | @Override 26 | public void handleEvent(SessionEventArgs arg0) { 27 | if (arg0.getException() != null) { 28 | System.err.println("Received this: " + arg0); 29 | } else { 30 | System.out.println("Received this: " + arg0); 31 | } 32 | } 33 | }); 34 | session.connect(); 35 | 36 | // Create a Flow be able to bind to and consume messages from the Queue. 37 | // final ConsumerFlowProperties flow_prop = new ConsumerFlowProperties(); 38 | // flow_prop.setEndpoint(queue); 39 | // flow_prop.setAckMode(JCSMPProperties.SUPPORTED_MESSAGE_ACK_CLIENT); // best practice 40 | // flow_prop.setActiveFlowIndication(true); // Flow events will advise when 41 | // https://docs.solace.com/API-Developer-Online-Ref-Documentation/java/com/solacesystems/jcsmp/JCSMPSession.html#addSubscription-com.solacesystems.jcsmp.Endpoint-com.solacesystems.jcsmp.Subscription-int- 42 | 43 | // final XMLMessageConsumer consumer = session.getMessageConsumer((XMLMessageListener)null); // blocking non-async 44 | final int TOTAL = 100; 45 | final Queue queue = JCSMPFactory.onlyInstance().createQueue(queueName); 46 | long start = System.currentTimeMillis(); 47 | System.out.print("Starting... "); 48 | for (int i=0;iNUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /schemas/README.md: -------------------------------------------------------------------------------- 1 | # ProtoBuf Definitions 2 | 3 | 4 | 5 | 6 | There are 3 options for including your Protobuf definitions with PrettyDump: 7 | 8 | 1. Build the Java source files, copy them into the `src/main/java` directory, and rebuild 9 | 1. Compile the Java source files into class files, and copy into `schemas/classes/classes` directory, and rebuild 10 | - Or, if using the prebuilt distribution, you can simply copy the class files into `lib/protobufs` directory 11 | 1. Build the class files into a JAR and include inside the `lib` directory, and rebuild 12 | - If using the prebuilt distribution, you can copy the JAR files into `lib` directory, **but** you'll need to update the classpath variable of the scripts in the `bin` directory 13 | 14 | 15 | ## Step 1: build Java source files 16 | 17 | First, you need to download the ProtoBuf compiler. You will find this on GitHub at https://github.com/protocolbuffers/protobuf/releases 18 | 19 | Choose the appropriate release for your system. For example: `protoc-25.3-linux-x86_64.zip` 20 | 21 | Unzip the compiler. And then copy your `.proto` definitions files into the same directory. 22 | 23 | Make a directory to store the Java source files, packages, directories. E.g. `mkdir src` 24 | 25 | Run the compiler for each of your `.proto` definitions to build the Java source files. 26 | For example, if your definition file is `receive_v1.proto`, then: 27 | ``` 28 | bin/protoc --java_out src receive_v1.proto 29 | ``` 30 | 31 | Or just: 32 | ``` 33 | bin/protoc --java_out src *.proto 34 | ``` 35 | 36 | If you wish, you can copy these source files (everything under `src`) into `src/main/java` and rebuild the PrettyDump project, then you're done. Otherwise, continue to Step 2. 37 | 38 | 39 | ## Step 2: compile into class files 40 | 41 | For this step, you will need a Java compiler `javac` on your path. These steps were tested to work with JDK 8 and JDK 11. 42 | I try to use JDK 8 for compiling distributions in order to work with the most people. 43 | 44 | First, you will need to download the most recent v3 Protobuf Java JAR library. Either from a central Maven repository, or from within 45 | the distribution of PrettyDump once it's compiled and downloads the required library. 46 | 47 | - https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java/versions 48 | - https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java 49 | 50 | Next, make a directory to store the compiled Java classes. E.g. `mkdir classes` 51 | 52 | Then, build the Java source files into class files: 53 | ``` 54 | find ./src/ -type f -name "*.java" -exec javac -cp "./protobuf-java-3.25.3.jar:./src/" -d ./classes '{}' + 55 | ``` 56 | 57 | If using Command Prompt on Windows, the command would be different. 58 | 59 | 60 | ## Step 3: build into a JAR if you want 61 | 62 | ls classes | xargs jar cf solace-dt-protobufs.jar -C classes 63 | 64 | Then copy the JAR into the `lib` directory and rebuild 65 | 66 | 67 | 68 | 69 | ## Step 4: update the topic-to-proto mapping 70 | 71 | Locate the file `protobuf.properties` (either inside the `lib` directory of the built distribution, or inside `./schemas/classes/classes` dir). Add one line for each topic subscription -> ProtoBuf definition Java class. This may take a bit of investigation into the generated source files. But should look something like: 72 | ``` 73 | solace/topic/to/match/>=.$ 74 | ``` 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/KeyboardHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.IOException; 21 | import java.io.InputStreamReader; 22 | 23 | public class KeyboardHandler { 24 | 25 | 26 | static void sttyCooked() { 27 | doStty(false); 28 | } 29 | 30 | static void sttyRaw() { 31 | doStty(true); 32 | } 33 | 34 | /** 35 | * Call 'stty' to set raw or cooked mode. 36 | * 37 | * @param mode if true, set raw mode, otherwise set cooked mode 38 | */ 39 | private static void doStty(final boolean mode) { 40 | String [] cmdRaw = { 41 | "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty" 42 | }; 43 | String [] cmdCooked = { 44 | "/bin/sh", "-c", "stty sane cooked < /dev/tty" 45 | }; 46 | try { 47 | Process process; 48 | if (mode) { 49 | process = Runtime.getRuntime().exec(cmdRaw); 50 | } else { 51 | process = Runtime.getRuntime().exec(cmdCooked); 52 | } 53 | BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); 54 | String line = in.readLine(); 55 | if ((line != null) && (line.length() > 0)) { 56 | System.err.println("WEIRD?! Normal output from stty: " + line); 57 | } 58 | while (true) { 59 | BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); 60 | line = err.readLine(); 61 | if ((line != null) && (line.length() > 0)) { 62 | System.err.println("Error output from stty: " + line); 63 | } 64 | try { 65 | process.waitFor(); 66 | break; 67 | } catch (InterruptedException e) { 68 | // if (debugToStderr) { 69 | e.printStackTrace(); 70 | // } 71 | } 72 | } 73 | int rc = process.exitValue(); 74 | if (rc != 0) { 75 | System.err.println("stty returned error code: " + rc); 76 | } 77 | } catch (IOException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | 82 | 83 | 84 | static void enableSignalOverride() { 85 | 86 | // sun.misc.Signal.handle(new sun.misc.Signal("INT"), // SIGINT 87 | // signal -> { 88 | // System.out.println("Interrupted by Ctrl+C"); 89 | // System.exit(0); 90 | // }); 91 | 92 | 93 | } 94 | 95 | public static void main(String... args) throws IOException { 96 | 97 | final Thread shutdownThread = new Thread(() -> { 98 | sttyCooked(); 99 | System.out.println(); 100 | }); 101 | Runtime.getRuntime().addShutdownHook(shutdownThread); 102 | 103 | try { 104 | sttyRaw(); 105 | 106 | while (true) { 107 | int keypress; 108 | if (System.in.available() > 0) { 109 | keypress = System.in.read(); 110 | System.out.print(keypress + ": " + (char)keypress + ", "); 111 | if (keypress == 113) break; 112 | continue; 113 | } 114 | try { 115 | Thread.sleep(10); 116 | } catch (InterruptedException e) { 117 | break; 118 | } 119 | } 120 | } finally { 121 | sttyCooked(); 122 | System.out.println(); 123 | } 124 | } 125 | 126 | 127 | 128 | 129 | 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/ThinkingAnsiHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import org.fusesource.jansi.AnsiConsole; 20 | 21 | public class ThinkingAnsiHelper { 22 | 23 | private static boolean isFilteringOn = false; 24 | 25 | // private static final char[] LOOPS = new char[] { '━', '\\', '|', '/' }; 26 | 27 | private static int lastStringsWidth = 0; 28 | private static final boolean isEnabled; 29 | // private static int charIndex = 0; 30 | // private static int rainbowIndex = 0; 31 | private static final String BACKSPACES; 32 | static { 33 | if (AnsiConsole.getTerminalWidth() == 0) { // happens when running inside Eclipse 34 | isEnabled = false; 35 | // StringBuilder sb = new StringBuilder(); 36 | // for (int i=0; i<200; i++) { 37 | // sb.append((char)8); // a backspace char 38 | // } 39 | // BACKSPACES = sb.toString(); 40 | BACKSPACES = "\n"; 41 | } else { 42 | BACKSPACES = /* (char)27 +*/ "\u001b[200D"; // 200 backspace chars 43 | isEnabled = true; 44 | } 45 | } 46 | static { 47 | // Ansi ansi = new Ansi(); 48 | // for (int i=0; i= 60 || screenPosX >= PayloadHelper.currentScreenWidth * 0.8 - 1) { // time to rewind 88 | clearReset(); 89 | System.out.print(new AaAnsi(false).fg(AaAnsi.rainbowTable[(rainbowIndex++) % AaAnsi.rainbowTable.length])); 90 | } else { // normal 91 | System.out.print(LOOPS[charIndex]); 92 | screenPosX++; 93 | } 94 | }*/ 95 | 96 | private static final String THINKING_PATTERN_PRINT = "🔎 %sRecv'd=%d, Filtered=%d, Printed=%d" + BACKSPACES; 97 | private static final String THINKING_PATTERN_GATHER = "🔎 %sRecv'd=%d, Filtered=%d, Gathered=%d (%d max)" + BACKSPACES; 98 | 99 | public static String makeStringGathered(String optionalPreString, long numReceived, long numFiltered, long numGathered, int max) { 100 | if (optionalPreString == null || optionalPreString.isEmpty()) { 101 | return String.format(THINKING_PATTERN_GATHER, "", numReceived, numFiltered, numGathered, max); 102 | } 103 | return String.format(THINKING_PATTERN_GATHER, optionalPreString + " ", numReceived, numFiltered, numGathered, max); 104 | } 105 | 106 | public static String makeStringPrinted(String optionalPreString, long numReceived, long numFiltered, long numPrinted) { 107 | if (optionalPreString == null || optionalPreString.isEmpty()) { 108 | return String.format(THINKING_PATTERN_PRINT, "", numReceived, numFiltered, numPrinted); 109 | } 110 | return String.format(THINKING_PATTERN_PRINT, optionalPreString + " ", numReceived, numFiltered, numPrinted); 111 | } 112 | 113 | public static void tick2(String filterString) { 114 | if (!isFilteringOn) { 115 | isFilteringOn = true; 116 | AaAnsi.resetAnsi(System.out); 117 | } 118 | if (!isEnabled) return; // don't print anything 119 | System.out.print(filterString); 120 | lastStringsWidth = filterString.length(); // so we can blank out the line when resetting 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/TestStuff.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.CharBuffer; 5 | import java.nio.charset.CharacterCodingException; 6 | import java.nio.charset.Charset; 7 | import java.nio.charset.CharsetDecoder; 8 | import java.nio.charset.CodingErrorAction; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Arrays; 11 | 12 | import com.solace.labs.aaron.UsefulUtils; 13 | 14 | public class TestStuff { 15 | 16 | 17 | public static void main(String... args) throws CharacterCodingException { 18 | 19 | // byte[] arr = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 66, 9, 65, 10, 11, -12, 65, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37 }; 20 | byte[] arr = new byte[256]; 21 | for (int i=-128; i<128; i++) { 22 | arr[i+128] = (byte)i; 23 | } 24 | // System.out.println(Arrays.toString(arr)); 25 | String s = new String(arr,Charset.forName("ibm437")); 26 | System.out.println(Arrays.toString(s.getBytes(StandardCharsets.UTF_8))); 27 | System.out.println(Arrays.toString(StandardCharsets.UTF_16.newEncoder().encode(CharBuffer.wrap(UsefulUtils.HARDCODED )).array())); 28 | 29 | for (int i=0; i<256; i++) { 30 | // System.out.print("'" + s.charAt(i) + "',"); 31 | System.out.print("'" + s.charAt(i) + "' "); 32 | System.out.print(Arrays.toString(StandardCharsets.UTF_8.newEncoder().encode(CharBuffer.wrap(new char[] { s.charAt(i) })).array())); 33 | System.out.print(","); 34 | if (i % 16 == 15) System.out.println(); 35 | } 36 | System.out.println(); 37 | // CharsetDecoder DECODER = StandardCharsets.ISO_8859_1.newDecoder(); 38 | System.out.println(Charset.availableCharsets().keySet()); 39 | // CharsetDecoder DECODER = Charset.forName("Big5").newDecoder(); 40 | // CharsetDecoder DECODER = Charset.forName("GB18030").newDecoder(); 41 | CharsetDecoder DECODER = Charset.forName("windows-1252").newDecoder(); 42 | DECODER.replaceWith("*"); 43 | // System.out.println(DECODER.unmappableCharacterAction()); 44 | // System.out.println(DECODER.malformedInputAction()); 45 | DECODER.onUnmappableCharacter(CodingErrorAction.REPLACE); 46 | // DECODER.onMalformedInput(CodingErrorAction.REPLACE); 47 | 48 | ByteBuffer buffer = ByteBuffer.wrap(arr); 49 | 50 | // String decoded = DECODER.decode(buffer).toString(); 51 | // System.out.println(decoded); 52 | // for (int i=0; i= 0 && c < 32) || c >= 127 && c < 160) { 60 | arr2[i] = '·'; 61 | // } else { 62 | // arr2[i] = c; 63 | } 64 | // System.out.print("'" + arr2[i] + "',"); 65 | System.out.print(arr2[i]); 66 | } 67 | System.out.println(); 68 | System.out.println(arr2.length); 69 | System.out.println(); 70 | for (int i=0; i> 8); 90 | System.out.println((int)emoji.charAt(0) & 0x00ff); 91 | for (int i=0; i'); 78 | if (indentFactor > 0) ansi.a('\n'); 79 | } else if (previous == Tag.END) { // aa debug june 25 80 | if (indentFactor > 0) ansi.a('\n'); 81 | } 82 | startTagForLater = AaAnsi.n(); // reset for new start tag 83 | if (indentFactor > 0 && level > 0) { 84 | startTagForLater.a(UsefulUtils.indent(indentFactor * level)); 85 | } 86 | startTagForLater.a('<').fg(Elem.KEY).a(qName); 87 | for (int i=0; i 0) ts = UsefulUtils.guessIfTimestampLong(key, bi.longValue()); 105 | if (ts != null) { 106 | return AaAnsi.n().fg(Elem.NUMBER).a(val).faintOn().a(ts); 107 | } else { 108 | return AaAnsi.n().fg(Elem.NUMBER).a(val); // yup! 109 | } 110 | } catch (NumberFormatException e) { 111 | return AaAnsi.n().fg(Elem.FLOAT).a(val); // nope! not an int, but a float 112 | } 113 | } catch (NumberFormatException e) { // nope, not a number at all 114 | if (val.equalsIgnoreCase("true") || val.equalsIgnoreCase("false")) { // is it a Boolean? 115 | return AaAnsi.n().fg(Elem.BOOLEAN).a(val); 116 | } 117 | return AaAnsi.n().fg(Elem.STRING).a(val); // assume it's a string 118 | } 119 | } 120 | 121 | @Override 122 | public final void endElement(String namespaceURI, String localName, String qName) throws SAXException { 123 | level--; 124 | String chars = doTrimOnCharacters ? characterDataSb.toString().trim() : characterDataSb.toString(); 125 | if (previous == Tag.START) { 126 | if (startTagForLater != null) { // the previous tag is a start tag 127 | if (chars.length() > 0) { 128 | ansi.aa(startTagForLater).reset().a('>').aa(guessAndFormatChars(chars, qName, indentFactor)).reset(); 129 | ansi.a("'); 130 | } else { // closing a startTagForLater tag right away, and no chars, so make it a singleton 131 | ansi.aa(startTagForLater).reset().a("/>"); 132 | } 133 | startTagForLater = null; 134 | } else { // already been blanked (maybe by a comment?) 135 | if (chars.length() > 0) { 136 | ansi.aa(guessAndFormatChars(chars, qName, indentFactor)).reset(); 137 | } 138 | ansi.a("'); 139 | } 140 | } else { // previous tag was another END tag 141 | if (indentFactor > 0) ansi.a('\n'); // aaron debug june 25 142 | if (indentFactor > 0 && level > 0) { 143 | ansi.a(UsefulUtils.indent(indentFactor * level)); 144 | } 145 | if (chars.length() > 0) ansi.aa(guessAndFormatChars(chars, qName, indentFactor)).reset(); 146 | ansi.a("'); 147 | } 148 | // if (indent > 0) ansi.a('\n'); // aaron debug june 25 149 | characterDataSb.setLength(0); 150 | previous = Tag.END; 151 | } 152 | 153 | @Override 154 | public void comment(char[] ch, int start, int length) throws SAXException { 155 | if (indentFactor > 0) { // show if not compact 156 | if (startTagForLater != null) { 157 | ansi.aa(startTagForLater).reset().a('>'); 158 | // if (indent > 0) ansi.a('\n'); 159 | startTagForLater = null; 160 | } 161 | ansi.fg(Elem.MSG_BREAK).a(""); 162 | // ansi.fg(Elem.MSG_BREAK).a("\n"); 163 | } 164 | } 165 | 166 | @Override 167 | public void startDTD(String name, String publicId, String systemId) throws SAXException { } 168 | 169 | @Override 170 | public void endCDATA() throws SAXException { } 171 | 172 | @Override 173 | public void startCDATA() throws SAXException { } 174 | 175 | @Override 176 | public void endDTD() throws SAXException { } 177 | 178 | @Override 179 | public void startEntity(String name) throws SAXException { } 180 | 181 | @Override 182 | public void endEntity(String name) throws SAXException { } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/SaxParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.Reader; 23 | import java.io.StringReader; 24 | 25 | import javax.xml.XMLConstants; 26 | import javax.xml.parsers.ParserConfigurationException; 27 | import javax.xml.parsers.SAXParserFactory; 28 | 29 | import org.xml.sax.InputSource; 30 | import org.xml.sax.SAXException; 31 | import org.xml.sax.SAXNotRecognizedException; 32 | import org.xml.sax.SAXNotSupportedException; 33 | import org.xml.sax.XMLReader; 34 | import org.xml.sax.helpers.DefaultHandler; 35 | 36 | /** 37 | * This class could be used to SAX parse any XML with the passed-in Handler. 38 | */ 39 | public class SaxParser { 40 | 41 | private static final String XML_FEATURE_DISABLE_DTD = "http://apache.org/xml/features/disallow-doctype-decl"; 42 | private static final String XML_FEATURE_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; 43 | private static final String XML_FEATURE_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; 44 | private static final String XML_FEATURE_LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; 45 | private static final String XML_PARAMETER_LEXICAL_HANDLER = "http://xml.org/sax/properties/lexical-handler"; 46 | 47 | protected static final SAXParserFactory saxFactory; 48 | 49 | // private static final Logger logger = LoggerFactory.getLogger(SaxParser.class); 50 | 51 | private static void setFactoryFeature(String feature, boolean value) { 52 | try { 53 | saxFactory.setFeature(feature,value); 54 | // logger.info("SAXParserFactory now enabled for: "+feature); 55 | } catch (SAXNotSupportedException | SAXNotRecognizedException | ParserConfigurationException e) { 56 | // logger.info("Not supported with this implementation of SAXParserFactory: "+e.toString()); 57 | } 58 | } 59 | 60 | static { 61 | saxFactory = SAXParserFactory.newInstance(); 62 | saxFactory.setNamespaceAware(true); 63 | saxFactory.setValidating(true); 64 | // Since an Unmarshaller parses XML and does not support any 65 | // flags for disabling XXE, it's imperative to parse the untrusted 66 | // XML through a configurable secure parser first, generate a 67 | // Source object as a result, and pass the source object to the 68 | // Unmarshaller. - https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet 69 | setFactoryFeature(XMLConstants.FEATURE_SECURE_PROCESSING,true); 70 | setFactoryFeature(XML_FEATURE_DISABLE_DTD,true); 71 | setFactoryFeature(XML_FEATURE_GENERAL_ENTITIES,false); 72 | setFactoryFeature(XML_FEATURE_PARAMETER_ENTITIES,false); 73 | setFactoryFeature(XML_FEATURE_LOAD_EXTERNAL_DTD,false); 74 | 75 | } 76 | 77 | private static void setFeatureSilent(XMLReader xmlReader, String feature, boolean value) { 78 | try { 79 | xmlReader.setFeature(feature,value); 80 | } catch (SAXNotSupportedException | SAXNotRecognizedException e) { 81 | // fail silently at this point 82 | } 83 | } 84 | 85 | private static void makeReaderSecure(XMLReader xmlReader) { 86 | // these first 5 work. But they shouldn't be needed since the factory is set with these already! 87 | setFeatureSilent(xmlReader,XMLConstants.FEATURE_SECURE_PROCESSING,true); 88 | setFeatureSilent(xmlReader,XML_FEATURE_DISABLE_DTD,true); 89 | setFeatureSilent(xmlReader,XML_FEATURE_GENERAL_ENTITIES,false); 90 | setFeatureSilent(xmlReader,XML_FEATURE_PARAMETER_ENTITIES,false); 91 | setFeatureSilent(xmlReader,XML_FEATURE_LOAD_EXTERNAL_DTD,false); 92 | // these were added for WF, but don't work on my dev machine. Maybe not for SAX parsers? 93 | setFeatureSilent(xmlReader,XMLConstants.ACCESS_EXTERNAL_DTD,false); 94 | setFeatureSilent(xmlReader,XMLConstants.ACCESS_EXTERNAL_SCHEMA,false); 95 | setFeatureSilent(xmlReader,XMLConstants.ACCESS_EXTERNAL_STYLESHEET,false); 96 | } 97 | 98 | public static void parseString(String xml, DefaultHandler handler) throws SaxParserException { 99 | parseReader(new BufferedReader(new StringReader(xml)), handler); 100 | } 101 | 102 | private static void parseSource(InputSource inputSource, DefaultHandler handler) throws SaxParserException { 103 | if (handler == null) 104 | throw new NullPointerException("DefaultHandler passed to parse() is null"); 105 | try { 106 | XMLReader xmlReader = saxFactory.newSAXParser().getXMLReader(); 107 | xmlReader.setContentHandler(handler); 108 | makeReaderSecure(xmlReader); 109 | xmlReader.setErrorHandler(handler); 110 | try { 111 | xmlReader.setProperty(XML_PARAMETER_LEXICAL_HANDLER, handler); 112 | } catch (SAXNotSupportedException | SAXNotRecognizedException e) { 113 | // ignore this... during schema loading at program startup, will throw an exception since SempReplySchemaLoader uses a DefaultHandler 114 | // during runtime, the GenericSempSaxHandler implements the required methods 115 | } 116 | inputSource.setEncoding("UTF-8"); // only parsing Java Strings, and payload already converted if here 117 | xmlReader.parse(inputSource); 118 | } catch (ParserConfigurationException e) { 119 | throw new SaxParserException(e); 120 | } catch (SAXException e) { 121 | throw new SaxParserException(e); 122 | } catch (IOException e) { 123 | throw new SaxParserException(e); 124 | } 125 | } 126 | 127 | public static void parseStream(InputStream inputStream, DefaultHandler handler) throws SaxParserException { 128 | try { 129 | parseSource(new InputSource(inputStream),handler); 130 | } finally { 131 | try { 132 | inputStream.close(); 133 | } catch (IOException e) { 134 | } 135 | } 136 | } 137 | 138 | public static void parseReader(Reader reader, DefaultHandler handler) throws SaxParserException { 139 | try { 140 | parseSource(new InputSource(reader),handler); 141 | } finally { 142 | try { 143 | reader.close(); 144 | } catch (IOException e) { 145 | } 146 | } 147 | } 148 | 149 | private SaxParser() { 150 | // can't instantiate 151 | throw new AssertionError("Not allowed to instantiate this class"); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/GsonUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import static com.solace.labs.aaron.UsefulUtils.indent; 20 | 21 | import java.io.IOException; 22 | import java.io.StringReader; 23 | import java.math.BigInteger; 24 | 25 | import org.apache.logging.log4j.LogManager; 26 | import org.apache.logging.log4j.Logger; 27 | 28 | import com.google.gson.stream.JsonReader; 29 | import com.google.gson.stream.JsonToken; 30 | 31 | public class GsonUtils { 32 | 33 | private static final Logger logger = LogManager.getLogger(GsonUtils.class); 34 | private static String lastKeyName = ""; 35 | 36 | static AaAnsi parseJsonDunnoWhich(String trimmedJson, int indentFactor) throws IOException { 37 | if (trimmedJson.charAt(0) == '{') return parseJsonObject(trimmedJson, indentFactor); 38 | else return parseJsonArray(trimmedJson, indentFactor); 39 | } 40 | 41 | static AaAnsi parseJsonObject(String json, int indentFactor) throws IOException { 42 | return parseJsonObject(json, indentFactor, false); 43 | } 44 | 45 | private static AaAnsi parseJsonObject(String json, int indentFactor, boolean isLenient) throws IOException { 46 | JsonReader reader = new JsonReader(new StringReader(json)); 47 | reader.setLenient(false); 48 | AaAnsi ansi = AaAnsi.n(); 49 | handleObject(reader, ansi, indentFactor, 0); 50 | // if (indentFactor > 0) ansi.a('\n'); 51 | return ansi; 52 | } 53 | 54 | static AaAnsi parseJsonArray(String json, int indentFactor) throws IOException { 55 | return parseJsonArray(json, indentFactor, false); 56 | } 57 | 58 | private static AaAnsi parseJsonArray(String json, int indentFactor, boolean isLenient) throws IOException { 59 | JsonReader reader = new JsonReader(new StringReader(json)); 60 | reader.setLenient(isLenient); 61 | AaAnsi ansi = AaAnsi.n(); 62 | handleArray(reader, ansi, indentFactor, 0); 63 | return ansi; 64 | } 65 | 66 | /** 67 | * Handle an Object. Consume the first token which is BEGIN_OBJECT. Within 68 | * the Object there could be array or non array tokens. We write handler 69 | * methods for both. Note the peek() method. It is used to find out the type 70 | * of the next token without actually consuming it. 71 | * 72 | * @param reader 73 | * @throws IOException 74 | */ 75 | private static void handleObject(JsonReader reader, AaAnsi ansi, int indentFactor, int curIndent) throws IOException { 76 | reader.beginObject(); 77 | boolean empty = !reader.hasNext(); 78 | ansi.reset().fg(empty ? Elem.NULL : Elem.BRACE).a('{').reset(); 79 | if (indentFactor > 0) { 80 | ansi.a(empty ? ' ' : '\n'); 81 | } 82 | while (reader.hasNext()) { 83 | JsonToken token = reader.peek(); 84 | if (token.equals(JsonToken.BEGIN_ARRAY)) { 85 | handleArray(reader, ansi, indentFactor, curIndent + indentFactor); 86 | } else if (token.equals(JsonToken.BEGIN_OBJECT)) { 87 | // ansi.a(indent(indent)); 88 | handleObject(reader, ansi, indentFactor, curIndent + indentFactor); 89 | reader.endObject(); 90 | if (reader.hasNext()) ansi.a(","); 91 | if (reader.peek().equals(JsonToken.END_OBJECT) || reader.peek().equals(JsonToken.END_ARRAY)) { 92 | if (indentFactor > 0) ansi.a(' '); 93 | } else { 94 | if (indentFactor > 0) ansi.a('\n'); 95 | } 96 | } else if (token.equals(JsonToken.END_OBJECT)) { // shouldn't come here b/c we end it above after handling the object 97 | System.out.println("********************************* JSON parser found END_OEJCT token, and it shouldn't!!!!!"); 98 | reader.endObject(); 99 | return; 100 | } else if (token.equals(JsonToken.NAME)) { 101 | handleRegularToken(reader, token, ansi, indentFactor, curIndent + indentFactor); 102 | } else { 103 | handleRegularToken(reader, token, ansi, indentFactor, 0); 104 | // orig 105 | // if (reader.hasNext()) ansi.a(","); 106 | // if (indentFactor > 0) ansi.a('\n'); 107 | if (reader.hasNext()) { 108 | ansi.a(","); 109 | if (indentFactor > 0) ansi.a('\n'); 110 | } else { // no more 111 | if (indentFactor > 0) ansi.a(' '); 112 | } 113 | } 114 | } 115 | // orig 116 | // ansi.a(indent(indent)).a("}"); 117 | ansi.fg(empty ? Elem.NULL : Elem.BRACE).a("}").reset(); 118 | } 119 | 120 | /** 121 | * Handle a json array. The first token would be JsonToken.BEGIN_ARRAY. 122 | * Arrays may contain objects or primitives. 123 | * 124 | * @param reader 125 | * @throws IOException 126 | */ 127 | private static void handleArray(JsonReader reader, AaAnsi ansi, int indentFactor, int curIndent) throws IOException { 128 | reader.beginArray(); 129 | boolean empty = !reader.hasNext(); 130 | ansi.reset().fg(empty ? Elem.NULL : Elem.BRACE).a("[").reset(); 131 | if (indentFactor > 0) ansi.a(empty ? ' ' : '\n'); 132 | while (true) { 133 | JsonToken token = reader.peek(); 134 | if (token.equals(JsonToken.END_ARRAY)) { 135 | reader.endArray(); 136 | break; 137 | } else if (token.equals(JsonToken.BEGIN_OBJECT)) { 138 | ansi.a(indent(curIndent + indentFactor)); 139 | handleObject(reader, ansi, indentFactor, curIndent + indentFactor); 140 | } else if (token.equals(JsonToken.BEGIN_ARRAY)) { 141 | ansi.a(indent(curIndent + indentFactor)); 142 | handleArray(reader, ansi, indentFactor, curIndent + indentFactor); 143 | } else if (token.equals(JsonToken.END_OBJECT)) { 144 | reader.endObject(); 145 | if (reader.hasNext()) ansi.a(","); 146 | // if (indentFactor > 0) ansi.a('\n'); 147 | if (reader.peek().equals(JsonToken.END_OBJECT) || reader.peek().equals(JsonToken.END_ARRAY)) { 148 | if (indentFactor > 0) ansi.a(' '); 149 | } else { 150 | if (indentFactor > 0) ansi.a('\n'); 151 | } 152 | } else if (token.equals(JsonToken.NAME)) { 153 | throw new AssertionError(); 154 | } else { 155 | handleRegularToken(reader, token, ansi, indentFactor, curIndent + indentFactor); 156 | // orig 157 | // if (reader.hasNext()) ansi.a(","); 158 | // if (indentFactor > 0) ansi.a('\n'); 159 | if (reader.hasNext()) { 160 | ansi.a(","); 161 | if (indentFactor > 0) ansi.a('\n'); 162 | } else { // no more 163 | if (indentFactor > 0) ansi.a(' '); 164 | } 165 | } 166 | } 167 | // orig 168 | // ansi.a(indent(indent)).a("]"); 169 | // if (reader.hasNext()) ansi.a(","); 170 | // if (indentFactor > 0) ansi.a('\n'); 171 | ansi.fg(empty ? Elem.NULL : Elem.BRACE).a("]").reset(); 172 | if (reader.hasNext()) { 173 | ansi.a(","); 174 | if (indentFactor > 0) ansi.a('\n'); 175 | } else { // no more 176 | if (indentFactor > 0) ansi.a(' '); 177 | } 178 | } 179 | 180 | 181 | // private static final boolean DARK_MODE = false; 182 | 183 | 184 | /** 185 | * Handle non array non object tokens 186 | * 187 | * @param reader 188 | * @param token 189 | * @throws IOException 190 | */ 191 | private static void handleRegularToken(JsonReader reader, JsonToken token, AaAnsi ansi, int indentFactor, int curIndent) throws IOException { 192 | ansi.a(indent(curIndent)); 193 | if (token.equals(JsonToken.NAME)) { 194 | // ansi.fg(Color.BLUE); 195 | lastKeyName = reader.nextName(); 196 | ansi.fg(Elem.KEY).a("\"" + lastKeyName + "\"").reset().a(":"); 197 | if (curIndent > 0) ansi.a(" "); 198 | } else if (token.equals(JsonToken.STRING)) { 199 | ansi.fg(Elem.STRING).a("\"" + reader.nextString() + "\"").reset(); 200 | } else if (token.equals(JsonToken.NUMBER)) { 201 | String num = reader.nextString(); // we use string to preserve any formatting (e.g. "25.00" stays that way, not 25 202 | // ansi.fg(Elem.NUMBER).a(num).reset(); 203 | try { 204 | BigInteger bi = new BigInteger(num); 205 | ansi.fg(Elem.NUMBER).a(num); 206 | if (indentFactor > 0) { 207 | String ts = UsefulUtils.guessIfTimestampLong(lastKeyName, bi.longValue()); 208 | if (ts != null) { 209 | ansi.faintOn().a(ts); 210 | } 211 | } 212 | ansi.reset(); 213 | } catch (NumberFormatException e) { 214 | ansi.fg(Elem.FLOAT).a(num).reset(); 215 | } 216 | 217 | } else if (token.equals(JsonToken.BOOLEAN)) { 218 | // ansi.fgMagenta().a(reader.nextString()).reset(); 219 | ansi.fg(Elem.BOOLEAN).a(reader.nextBoolean()).reset(); 220 | } else if (token.equals(JsonToken.NULL)) { 221 | // ansi.fgRed().a(reader.nextString()).reset(); 222 | ansi.fg(Elem.NULL).a("null").reset(); 223 | reader.nextNull(); 224 | } else { // nothing else it could be? https://javadoc.io/doc/com.google.code.gson/gson/2.6.2/com/google/gson/stream/JsonToken.html 225 | ansi.fg(Elem.UNKNOWN).a("").reset(); 226 | logger.warn("Discovered an \"unknown\" value:\n" + ansi.toString()); 227 | reader.skipValue(); 228 | } 229 | } 230 | 231 | } 232 | 233 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/Banner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import org.fusesource.jansi.AnsiConsole; 23 | 24 | public class Banner { 25 | 26 | static int maxLen = 0; 27 | static int pieceLen = 0; 28 | 29 | enum Which { 30 | DUMP, 31 | WRAP, 32 | ; 33 | } 34 | 35 | private static Map banners = new HashMap<>(); 36 | static { 37 | banners.put(Which.DUMP, new String[] { 38 | " __________ __ __ ________", 39 | " \\______ \\_______ _____/ |__/ |_ ___.__.\\______ \\ __ __ _____ ______ ", 40 | " | ___/\\_ __ \\_/ __ \\ __\\ __< | | | | \\| | \\/ \\\\____ \\ ", 41 | " | | | | \\/\\ ___/| | | | \\___ | | ` \\ | / Y Y \\ |_> > ", 42 | " |____| |__| \\___ >__| |__| / ____|/_______ /____/|__|_| / __/ ", 43 | " \\/ \\/ \\/ by Aaron \\/|__| ", 44 | " " 45 | }); 46 | // banners.put(Which.DUMP, new String[] { 47 | // " __________ __ __ __________ .__ __ ", 48 | // " \\______ \\______ _____/ |__/ |_ ___.__.\\______ \\______|__| _____/ |_ ", 49 | // " | ___|_ __ \\/ __ \\ __\\ __< | | | ___|_ __ \\ |/ \\ __\\ ", 50 | // " | | | | \\| ___/| | | | \\___ | | | | | \\/ | | \\ | ", 51 | // " |____| |__| \\___ >__| |__| / ____| |____| |__| |__|___| /__| ", 52 | // " \\/ \\/ by Aaron \\/ ", 53 | // " " 54 | // }); 55 | banners.put(Which.WRAP, new String[] { 56 | " __________ __ __ __ __ ", 57 | " \\______ \\_______ _____/ |__/ |_ ___.__./ \\ / \\____________ ______ ", 58 | " | ___/\\_ __ \\_/ __ \\ __\\ __< | |\\ \\/\\/ /\\_ __ \\__ \\ \\____ \\ ", 59 | " | | | | \\/\\ ___/| | | | \\___ | \\ / | | \\// __ \\| |_> > ", 60 | " |____| |__| \\___ >__| |__| / ____| \\__/\\ / |__| (____ / __/ ", 61 | " \\/ \\/ \\/ by Aaron \\/|__| ", 62 | " " 63 | }); 64 | } 65 | 66 | private static String printBannerStandard(String[] banner) { 67 | AaAnsi ansi = AaAnsi.n(); 68 | ansi.fg(10).a(banner[0]).a('\n'); 69 | ansi.fg(2).a(banner[1]).a('\n'); 70 | ansi.fg(14).a(banner[2]).a('\n'); 71 | ansi.fg(6).a(banner[3]).a('\n'); 72 | ansi.fg(12).a(banner[4]).a('\n'); 73 | ansi.fg(4).a(banner[5]).a('\n'); 74 | return ansi.reset().toString(); 75 | } 76 | 77 | 78 | private static String printBannerLight(String[] banner) { 79 | AaAnsi ansi = AaAnsi.n(); 80 | ansi.fg(20).a(banner[0]).a('\n'); 81 | ansi.fg(90).a(banner[1]).a('\n'); 82 | ansi.fg(124).a(banner[2]).a('\n'); 83 | ansi.fg(100).a(banner[3]).a('\n'); 84 | ansi.fg(28).a(banner[4]).a('\n'); // should be 40 85 | ansi.fg(30).a(banner[5]).a('\n'); 86 | return ansi.reset().toString(); 87 | } 88 | 89 | // 22, 28, 34, 40, 46, 83, 120, 157, 194, 231 90 | private static String printBannerMatrix(String[] banner) { 91 | AaAnsi ansi = AaAnsi.n(); 92 | ansi.fg(83).a(banner[0]).a('\n'); 93 | ansi.fg(120).a(banner[1]).a('\n'); 94 | ansi.fg(46).a(banner[2]).a('\n'); 95 | ansi.fg(40).a(banner[3]).a('\n'); 96 | ansi.fg(34).a(banner[4]).a('\n'); // should be 40 97 | ansi.fg(22).a(banner[5]).a('\n'); 98 | return ansi.reset().toString(); 99 | } 100 | 101 | static String printBanner(Which which) { 102 | String[] banner = banners.get(which); 103 | switch (AaAnsi.getColorMode()) { 104 | case VIVID: 105 | case STANDARD: 106 | return printBannerVivid2(banner); 107 | case MATRIX: 108 | return printBannerMatrix(banner); 109 | case LIGHT: 110 | return printBannerLight(banner); 111 | default: 112 | return printBannerStandard(banner); // uh, minimal? and off, but that will be plain 113 | } 114 | } 115 | 116 | /* Original one, without the "shadow" or 3d effect */ 117 | /*private static String printBannerVivid() { 118 | for (String s : banner) { 119 | maxLen = Math.max(maxLen, s.length()); 120 | } 121 | // System.out.println("maxLen = " + maxLen); 122 | pieceLen = maxLen / 6; 123 | // System.out.println("pieceLen = " + pieceLen); 124 | 125 | AaAnsi ansi = new AaAnsi(); 126 | int col = 196; 127 | for (int i=0; i<6; i++) { 128 | ansi.fg(col); 129 | // ansi.a(piece(banner[i], 0, 0, i*5)); 130 | ansi.a(piece(banner[i], 0, 0, 0)); 131 | for (int j=1; j<6; j++) { 132 | ansi.fg(col + j); 133 | // ansi.a(piece(banner[i], j, i*5, i*5)); 134 | ansi.a(piece(banner[i], j, 0, 0)); 135 | } 136 | ansi.a('\n'); 137 | // col -= (201-171); 138 | col -= (196-166); 139 | } 140 | return ansi.reset().toString(); 141 | } 142 | 143 | private static String piece(String s, int p, int i, int o) { 144 | int len = s.length(); 145 | if (p == 5) return (s.substring(Math.min(p*pieceLen + i, len), len)); // all of it 146 | return (s.substring(Math.min(p*pieceLen + i, len), Math.min(p*pieceLen + o + pieceLen, len))); 147 | }*/ 148 | 149 | 150 | private static final int STARTING_VIVID_COLOR = 196; 151 | 152 | private static int getCol(int row, int pieceLen, int pos) { 153 | return (STARTING_VIVID_COLOR - (row*30)) + Math.min(5, (pos / pieceLen)); 154 | } 155 | 156 | private static boolean anythingBelow(String[] banner, int i, int j) { 157 | if (i == 6) return false; 158 | if (i == 5) return banner[6].charAt(j) != ' '; 159 | if (i == 4) return banner[6].charAt(j) != ' ' || banner[5].charAt(j) != ' '; 160 | if (i == 3) return banner[6].charAt(j) != ' ' || banner[5].charAt(j) != ' ' || banner[4].charAt(j) != ' '; 161 | throw new IllegalStateException(); 162 | } 163 | 164 | // RAELLY REALLY BAD CODE, BAD LOGIC, ALGORITHM... I'm kind of embarrassed 165 | // all this does is add kind of a shadow/perspective view of the banner text down-right of the text 166 | private static void preprocessBanner(String[] banner) { 167 | for (int i=3; i<7; i++) { 168 | StringBuilder sb = new StringBuilder(); 169 | for (int j=0; j < banner[i].length(); j++) { 170 | if (banner[i].charAt(j) != ' ') { 171 | sb.append(banner[i].charAt(j)); 172 | continue; 173 | } 174 | if (j > 1) { 175 | if (j > banner[i-1].length()-1) { 176 | sb.append(banner[i].charAt(j)); 177 | continue; 178 | } 179 | if (anythingBelow(banner,i,j)) { 180 | sb.append(banner[i].charAt(j)); 181 | continue; 182 | } 183 | char ul = banner[i-1].charAt(j-2); 184 | if (ul == '*') { 185 | sb.append(banner[i].charAt(j)); 186 | continue; 187 | } 188 | // char u = banner[i-1].charAt(j); 189 | // char l = banner[i].charAt(j-2); 190 | // if (ul != ' '/* || (u != ' ' && l != ' ') */) { 191 | if (ul == '_' || ul == '|' || ul == '\\' || ul == '/' || ul == '>') { 192 | if (Character.isAlphabetic(banner[i].charAt(j-1)) ) { //|| Character.isAlphabetic(banner[i].charAt(j+1))) { 193 | sb.append(banner[i].charAt(j)); 194 | continue; 195 | } 196 | sb.append('*'); 197 | continue; 198 | } 199 | } 200 | sb.append(banner[i].charAt(j)); 201 | continue; 202 | } 203 | banner[i] = sb.toString(); 204 | } 205 | } 206 | 207 | 208 | private static String printBannerVivid2(String[] banner) { 209 | int maxLen = 1; 210 | for (String s : banner) { 211 | maxLen = Math.max(maxLen, s.length()); 212 | } 213 | int pieceLen = maxLen / 6; 214 | preprocessBanner(banner); // make the drop shadow 215 | AaAnsi ansi = AaAnsi.n(); 216 | for (int i=0; i '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /schemas/build-custom-docker-with-schemas.md: -------------------------------------------------------------------------------- 1 | # No Java, no problem 2 | 3 | How to build a Docker image of PrettyDump with custom Protobuf definitions. 4 | 5 | 6 | 7 | ## Step 0: Load in the latest PrettyDump docker image: 8 | 9 | ```bash 10 | $ docker load -i ./solace-pretty-dump-latest.tgz 11 | Loaded image: solace-pretty-dump:latest 12 | ``` 13 | 14 | 15 | 16 | 17 | ## Step 1: build Protobuf Java source files 18 | 19 | First, you need to download the ProtoBuf compiler. You will find this on GitHub at https://github.com/protocolbuffers/protobuf/releases 20 | 21 | Choose the appropriate release for your system. For example: `protoc-25.3-linux-x86_64.zip` 22 | 23 | Unzip the compiler. And then copy your `.proto` definitions files into the same directory. 24 | 25 | Make a directory to store the Java source files, packages, directories. E.g.: 26 | ```bash 27 | $ mkdir src 28 | ``` 29 | 30 | Run the compiler for each of your `.proto` definitions to build the Java source files. 31 | For example, if your definition file is `receive_v1.proto`, then: 32 | ```bash 33 | $ bin/protoc --java_out src receive_v1.proto 34 | ``` 35 | 36 | Or just use wildcards if you have lots of files: 37 | ```bash 38 | $ bin/protoc --java_out src *.proto 39 | ``` 40 | 41 | It should (?) generate one Java source file for each Protobuf defintion. 42 | 43 | 44 | 45 | 46 | ## Step 2: build Docker container to use as image baseline 47 | 48 | ```bash 49 | $ docker create --name pretty-baseline solace-pretty-dump:latest 50 | 9c9ddb0b9d52dd29df4bf176db5826d5076a3eb81722409b839d19e80a995872 51 | 52 | $ docker ps -a | grep pretty 53 | CONTAINER ID IMAGE COMMAND CREATED NAMES 54 | b4e9d2dd3640 solace-pretty-dump:latest "./bin/prettydump" 9 seconds ago pretty-baseline 55 | ``` 56 | 57 | Copy out required Protobuf JAR file (used for building the Java class files in Step 4), and properties file (Step 5). The first command is to verify the exact version that's inside the container (you can't use wildcards with `docker cp`). 58 | ```bash 59 | $ docker export pretty-baseline | tar t | grep protobuf-java 60 | opt/pretty/lib/protobuf-java-3.25.3.jar 61 | 62 | $ docker cp pretty-baseline:opt/pretty/lib/protobuf-java-3.25.3.jar . 63 | Successfully copied 1.88MB to /home/alee/dev/proto/. 64 | 65 | $ docker cp pretty-baseline:opt/pretty/lib/classes/protobuf.properties . 66 | Successfully copied 3.07kB to /home/alee/dev/proto/. 67 | ``` 68 | 69 | 70 | 71 | 72 | ## Step 3: download a JDK 73 | 74 | Goto OpenJDK website: https://jdk.java.net/22/. Grab the link for the build for your OS (right-click, copy link address). 75 | Then download it, and unzip it: 76 | 77 | ```bash 78 | $ wget https://download.java.net/java/GA/jdk22//36/GPL/openjdk-22_linux-aarch64_bin.tar.gz 79 | $ tar zxf openjdk-22_linux-x64_bin.tar.gz 80 | ``` 81 | 82 | This should make a directory called `jdk-22` 83 | 84 | 85 | 86 | 87 | ## Step 4: build the class files 88 | 89 | ```bash 90 | $ mkdir classes 91 | $ find ./src/ -type f -name "*.java" -exec ./jdk-22/bin/javac -cp "./protobuf-java-3.25.3.jar:./src/" -d ./classes '{}' + 92 | ``` 93 | 94 | This should make a bunch of Java class files under the `classes` directory 95 | 96 | ```bash 97 | $ ls -R1 classes 98 | 99 | classes/com/blah/proto/package/name: 100 | v1 101 | 102 | classes/com/blah/proto/package/name/v1: 103 | 'ReceiveV1$SpanData$1.class' 104 | 'ReceiveV1$SpanData$TransactionEvent$TransactionIdCase.class' 105 | 'ReceiveV1$SpanData$Builder$UserPropertiesConverter.class' 106 | 'ReceiveV1$SpanData$TransactionEvent$Type$1.class' 107 | 'ReceiveV1$SpanData$Builder.class' 108 | 'ReceiveV1$SpanData$TransactionEvent$Type.class' 109 | 'ReceiveV1$SpanData$TransactionEvent$LocalTransactionId$Builder.class' 110 | 'ReceiveV1$SpanData.class' 111 | 'ReceiveV1$SpanData$TransactionEvent$LocalTransactionId.class' 112 | 'ReceiveV1$SpanDataOrBuilder.class' 113 | 'ReceiveV1$SpanData$TransactionEvent$LocalTransactionIdOrBuilder.class' 114 | ReceiveV1.class 115 | ``` 116 | 117 | 118 | 119 | 120 | 121 | 122 | ## Step 5: update the topic-to-protobuf properties file 123 | 124 | The `protobuf.properties` file (Step 2) is used by PrettyDump to figure out which messages (based on topics / subscriptions) should use which Protobuf deserializers. There is some gathering of info for this step. 125 | 126 | _**You will need:**_ `[topic-subscription] = [package-name].[class-name]$[proto-message-name]` 127 | 128 | ### Package Name 129 | 130 | In each `.proto` definitions file, it should list the _package_ name: 131 | ```bash 132 | $ grep package receive_v1.proto 133 | package com.blah.proto.package.name.v1; 134 | ``` 135 | 136 | Nota that there mmight be a specified Java package name as well, use that instead. 137 | 138 | 139 | 140 | 141 | ### Class Name 142 | 143 | The generated _class_ name should be a PascalCase version of the `.proto` filename. E.g.`receive_v1.proto` -> "ReceiveV1". 144 | This should also be seen inside the corresponding `classes` subdirectory: 145 | ```bash 146 | $ ls -R1 classes | grep "\\.class" | grep -v "\\$" 147 | ReceiveV1.class 148 | ``` 149 | 150 | 151 | 152 | 153 | ### Message Name 154 | 155 | The _Message_ name can also be found inside the `.proto` definition as a top-level Message object: 156 | 157 | ```bash 158 | $ grep "^message" receive_v1.proto 159 | message SpanData { 160 | ``` 161 | 162 | **NOTE:** I'm not sure if it is possible/allowed to have multiple "root level" Message definitions inside a single `.proto` file..? 🤔 163 | 164 | You should also be able to see a generated `.class` file that matches this Message: 165 | ```bash 166 | $ ls -R1 classes | grep ReceiveV1.SpanData.class 167 | ReceiveV1$SpanData.class 168 | ``` 169 | 170 | (note the "$" sign, an inner class). 171 | 172 | So now you can add an entry / entries to the `protobuf.properties` file that tells PrettyDump that when a message is received with a topic that matches a particular subscription, to use this 173 | specific Protobuf definition. E.g.: 174 | 175 | ``` 176 | root/topic/to/match/> = com.blah.proto.package.name.v1.ReceiveV1$SpanData 177 | ``` 178 | 179 | Use of topic subscription wildcards in effect: 180 | - `*` single-level wildcard, can have a prefix 181 | - `>` multi-level 1-or-more wildcard, must occur at the end following a `/` 182 | - `#` (MQTT-style) multi-level 0-or-more wildcard, must occur at the end following a `/` 183 | 184 | Repeat Step 5 for all Protobuf definitions and all Solace topics that match. 185 | 186 | 187 | ## Step 6: copy the modified files back into the container 188 | 189 | ```bash 190 | $ docker cp protobuf.properties pretty-baseline:opt/pretty/lib/classes 191 | Successfully copied 3.07kB to pretty-baseline:opt/pretty/lib/classes 192 | 193 | $ docker cp classes/* pretty-baseline:opt/pretty/lib/classes 194 | Successfully copied 634kB to pretty-baseline:opt/pretty/lib/classes 195 | ``` 196 | 197 | 198 | 199 | 200 | ## Step 7: build a new Docker image from the modified container 201 | 202 | You need the container ID of the `pretty-baseline` container. User `docker ps -a` to find it. 203 | ```bash 204 | $ docker ps -a | grep pretty 205 | CONTAINER ID IMAGE COMMAND 206 | b4e9d2dd3640 solace-pretty-dump:latest "./bin/prettydump" 207 | 208 | $ docker commit b4e9d2dd3640 pretty-modded 209 | sha256:b542460354cbaf155a2cf0ca83208acd6b63dd9c58e15ce00c6d7fecde03d68d 210 | 211 | $ docker images | grep pretty 212 | pretty-modded latest b542460354cb 17 seconds ago 121MB 213 | solace-pretty-dump latest 4103ff6066d1 4 hours ago 120MB 214 | ``` 215 | 216 | You now have a modified PrettyDump image to use with your custom Protobuf definitions! 🎉 Share it with your friends and colleagues..! 217 | ```bash 218 | $ docker image save pretty-modded:latest > prettydump-modded.tar 219 | $ gzip prettydump-modded.tar 220 | $ mv prettydump-modded.tar.gz prettydump-modded.tgz 221 | ``` 222 | 223 | 224 | 225 | 226 | 227 | 228 | ## Step 8: run the new Docker image! 229 | 230 | To run with the "standard" 16-color palette, simply do: 231 | 232 | ```bash 233 | $ docker run -it --rm pretty-modded 192.168.10.20 vpn user pw ">" 234 | ``` 235 | 236 | Substituting your broker URL, VPN name, username, and password as appropriate. Supports Direct topic subscriptions, consuming from a queue, and browsing a queue. For help with command line options, check [the README on GitHub](https://github.com/SolaceLabs/pretty-dump?tab=readme-ov-file#command-line-parameters), or use `-h` or `--help`: 237 | 238 | ```bash 239 | $ docker run -it --rm pretty-modded -h 240 | Usage: prettydump [host:port] [msg-vpn] [username] [password] [topics|q:queue|b:queue|f:queue] [indent] 241 | or: prettydump [indent] for "shortcut" mode 242 | ... 243 | ``` 244 | 245 | 246 | ### MOAR COLORS! 247 | 248 | To run PrettyDump with an extended 256-color palette, use the additional environment variables: 249 | 250 | ```bash 251 | $ docker run -it --rm -e "PRETTY_COLORS=vivid" -e "TERM=xterm-256color" pretty-modded:latest ... 252 | 253 | ~ or, for white/light coloured backgrounds ~ 254 | 255 | $ docker run -it --rm -e "PRETTY_COLORS=light" -e "TERM=xterm-256color" pretty-modded:latest ... 256 | ``` 257 | 258 | Probably easiset to just make an `alias`: 259 | ```bash 260 | $ alias pretty='docker run -it --rm -e "PRETTY_COLORS=vivid" -e"TERM=xterm-256color" pretty-modded:latest' 261 | $ pretty public.messaging.solace.cloud public public public ">" 262 | ``` 263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/Elem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import java.math.BigDecimal; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | import org.apache.logging.log4j.LogManager; 24 | import org.apache.logging.log4j.Logger; 25 | 26 | import com.google.protobuf.ByteString; 27 | import com.solace.labs.aaron.AaAnsi.ColorMode; 28 | import com.solacesystems.common.util.ByteArray; 29 | import com.solacesystems.jcsmp.Destination; 30 | import com.solacesystems.jcsmp.SDTMap; 31 | import com.solacesystems.jcsmp.SDTStream; 32 | 33 | /** 34 | * Used by AaAnsi, my wrapper around the JAnsi library to give me better controls over colours. 35 | * @author Aaron Lee 36 | */ 37 | public enum Elem { 38 | 39 | KEY, 40 | DATA_TYPE, 41 | PAYLOAD_TYPE, 42 | NULL, 43 | STRING, 44 | CHAR, 45 | NUMBER, 46 | FLOAT, 47 | BOOLEAN, 48 | BYTES, 49 | BYTES_CHARS, 50 | BRACE, 51 | DESTINATION, 52 | TOPIC_SEPARATOR, 53 | MSG_BREAK, 54 | WARN, 55 | ERROR, 56 | UNKNOWN, 57 | DEFAULT, 58 | ; 59 | 60 | private static final Logger logger = LogManager.getLogger(Elem.class); 61 | static Map> colorMap = new HashMap<>(); 62 | static { 63 | colorMap.put(AaAnsi.ColorMode.STANDARD, new HashMap<>()); 64 | Map map = colorMap.get(AaAnsi.ColorMode.STANDARD); 65 | map.put(KEY, new Col(12)); 66 | map.put(DATA_TYPE, new Col(4)); 67 | map.put(PAYLOAD_TYPE, new Col(15)); 68 | map.put(NULL, new Col(1, false, true)); 69 | map.put(STRING, new Col(2)); 70 | map.put(CHAR, new Col(10)); 71 | map.put(NUMBER, new Col(3)); 72 | map.put(FLOAT, new Col(3)); 73 | map.put(BOOLEAN, new Col(13)); 74 | map.put(BYTES, new Col(5)); 75 | map.put(BYTES_CHARS, new Col(6)); 76 | map.put(BRACE, new Col(-1)); 77 | map.put(DESTINATION, new Col(14)); 78 | map.put(TOPIC_SEPARATOR, new Col(6)); 79 | map.put(MSG_BREAK, new Col(-1,true)); 80 | map.put(WARN, new Col(208)); 81 | map.put(ERROR, new Col(9)); 82 | map.put(UNKNOWN, new Col(198, false, true)); 83 | map.put(DEFAULT, new Col(-1)); 84 | 85 | colorMap.put(AaAnsi.ColorMode.MINIMAL, new HashMap<>()); 86 | map = colorMap.get(AaAnsi.ColorMode.MINIMAL); 87 | // map.put(KEY, new Col(12)); 88 | // map.put(DATA_TYPE, new Col(-1)); 89 | // map.put(PAYLOAD_TYPE, new Col(-1)); 90 | // map.put(NULL, new Col(2, false, true)); 91 | // map.put(STRING, new Col(2)); 92 | // map.put(CHAR, new Col(2)); 93 | // map.put(NUMBER, new Col(-1)); 94 | // map.put(FLOAT, new Col(-1)); 95 | // map.put(BOOLEAN, new Col(2)); 96 | // map.put(BYTES, new Col(5)); 97 | // map.put(BYTES_CHARS, new Col(2)); 98 | // map.put(BRACE, new Col(-1)); 99 | // map.put(DESTINATION, new Col(14)); 100 | // map.put(TOPIC_SEPARATOR, new Col(6)); 101 | // map.put(MSG_BREAK, new Col(-1)); 102 | // map.put(WARN, new Col(11)); 103 | // map.put(ERROR, new Col(9)); 104 | // map.put(UNKNOWN, new Col(13, false, true)); 105 | // map.put(DEFAULT, new Col(-1)); 106 | map.put(KEY, new Col(-1)); 107 | map.put(DATA_TYPE, new Col(-1)); 108 | map.put(PAYLOAD_TYPE, new Col(15)); 109 | map.put(NULL, new Col(2, false, true)); 110 | map.put(STRING, new Col(-1)); 111 | map.put(CHAR, new Col(-1)); 112 | map.put(NUMBER, new Col(-1)); 113 | map.put(FLOAT, new Col(-1)); 114 | map.put(BOOLEAN, new Col(-1)); 115 | map.put(BYTES, new Col(5)); 116 | map.put(BYTES_CHARS, new Col(-1)); 117 | map.put(BRACE, new Col(-1)); 118 | // map.put(DESTINATION, new Col(14)); 119 | // map.put(TOPIC_SEPARATOR, new Col(6)); 120 | map.put(DESTINATION, new Col(42)); 121 | map.put(TOPIC_SEPARATOR, new Col(36)); 122 | map.put(MSG_BREAK, new Col(-1, true)); 123 | map.put(WARN, new Col(11)); 124 | map.put(ERROR, new Col(9)); 125 | map.put(UNKNOWN, new Col(13, false, true)); 126 | map.put(DEFAULT, new Col(-1)); 127 | 128 | colorMap.put(AaAnsi.ColorMode.VIVID, new HashMap<>()); 129 | map = colorMap.get(AaAnsi.ColorMode.VIVID); 130 | map.put(KEY, new Col(75)); 131 | map.put(DATA_TYPE, new Col(33)); 132 | map.put(PAYLOAD_TYPE, new Col(195)); 133 | map.put(NULL, new Col(198, false, true)); 134 | map.put(STRING, new Col(47)); 135 | map.put(CHAR, new Col(119)); 136 | // defaults.put(NUMBER, new Col(226)); 137 | map.put(NUMBER, new Col(214)); 138 | // defaults.put(FLOAT, new Col(214)); 139 | map.put(FLOAT, new Col(226)); 140 | map.put(BOOLEAN, new Col(207)); 141 | map.put(BYTES, new Col(99)); 142 | map.put(BYTES_CHARS, new Col(87)); 143 | map.put(BRACE, new Col(230)); 144 | map.put(DESTINATION, new Col(49)); 145 | // map.put(TOPIC_SEPARATOR, new Col(43)); 146 | map.put(TOPIC_SEPARATOR, new Col(36)); 147 | map.put(MSG_BREAK, new Col(-1, true)); 148 | map.put(WARN, new Col(208)); 149 | map.put(ERROR, new Col(9)); 150 | map.put(UNKNOWN, new Col(196, false, true)); 151 | // defaults.put(DEFAULT, new Col(66)); 152 | map.put(DEFAULT, new Col(245)); 153 | 154 | colorMap.put(AaAnsi.ColorMode.LIGHT, new HashMap<>()); 155 | map = colorMap.get(AaAnsi.ColorMode.LIGHT); 156 | // map.put(KEY, new Col(26)); 157 | map.put(KEY, new Col(21)); 158 | // map.put(DATA_TYPE, new Col(33)); 159 | map.put(DATA_TYPE, new Col(26)); 160 | map.put(PAYLOAD_TYPE, new Col(17)); 161 | map.put(NULL, new Col(198, false, true)); 162 | map.put(STRING, new Col(28)); 163 | // map.put(CHAR, new Col(40)); 164 | map.put(CHAR, new Col(34)); 165 | // map.put(NUMBER, new Col(136)); 166 | map.put(NUMBER, new Col(130)); 167 | // map.put(FLOAT, new Col(142)); 168 | map.put(FLOAT, new Col(136)); 169 | map.put(BOOLEAN, new Col(57)); 170 | map.put(BYTES, new Col(92)); 171 | map.put(BYTES_CHARS, new Col(33)); 172 | map.put(BRACE, new Col(52)); 173 | // map.put(DESTINATION, new Col(30)); 174 | map.put(DESTINATION, new Col(23)); 175 | // map.put(TOPIC_SEPARATOR, new Col(42)); 176 | map.put(TOPIC_SEPARATOR, new Col(30)); 177 | map.put(MSG_BREAK, new Col(247)); 178 | map.put(WARN, new Col(172)); 179 | map.put(ERROR, new Col(160)); 180 | map.put(UNKNOWN, new Col(196, false, true)); 181 | map.put(DEFAULT, new Col(241)); 182 | 183 | // 22, 28, 34, 40, 46, 83, 120, 157, 194, 231 184 | colorMap.put(AaAnsi.ColorMode.MATRIX, new HashMap<>()); 185 | map = colorMap.get(AaAnsi.ColorMode.MATRIX); 186 | map.put(KEY, new Col(40)); 187 | map.put(DATA_TYPE, new Col(34)); 188 | map.put(PAYLOAD_TYPE, new Col(34)); 189 | map.put(NULL, new Col(47, false, true)); 190 | map.put(STRING, new Col(46)); 191 | map.put(CHAR, new Col(82)); 192 | map.put(NUMBER, new Col(83)); 193 | map.put(FLOAT, new Col(83)); 194 | map.put(BOOLEAN, new Col(120)); 195 | map.put(BYTES, new Col(28)); 196 | map.put(BYTES_CHARS, new Col(34)); 197 | map.put(BRACE, new Col(157)); 198 | map.put(DESTINATION, new Col(47)); 199 | map.put(TOPIC_SEPARATOR, new Col(34)); 200 | map.put(MSG_BREAK, new Col(35, true)); 201 | map.put(WARN, new Col(154)); 202 | map.put(ERROR, new Col(157)); 203 | map.put(UNKNOWN, new Col(157, false, true)); 204 | map.put(DEFAULT, new Col(28)); 205 | 206 | colorMap.put(AaAnsi.ColorMode.OFF, new HashMap<>()); 207 | map = colorMap.get(AaAnsi.ColorMode.OFF); 208 | map.put(WARN, new Col(11)); 209 | map.put(ERROR, new Col(9)); 210 | map.put(DEFAULT, new Col(-1)); 211 | } 212 | 213 | static Elem guessByType(Object value) { 214 | if (value == null) return Elem.NULL; 215 | if (value instanceof Number) { 216 | if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) return Elem.FLOAT; 217 | return Elem.NUMBER; 218 | } 219 | if (value instanceof String) { 220 | return ((String)value).isEmpty() ? Elem.NULL : Elem.STRING; 221 | } 222 | if (value instanceof Character) return Elem.CHAR; 223 | if (value instanceof ByteArray || value instanceof ByteString) return Elem.BYTES; 224 | if (value instanceof Boolean) return Elem.BOOLEAN; 225 | if (value instanceof Destination) return Elem.DESTINATION; 226 | if (value instanceof SDTMap || value instanceof SDTStream) return Elem.UNKNOWN; // doesn't matter, this will get formatted anyway 227 | logger.warn("Found a value that couldn't be guessed! " + value.toString() + ", " + value.getClass().getName()); 228 | return Elem.UNKNOWN; 229 | } 230 | 231 | @Override 232 | public String toString() { 233 | return this.name() + ": " + lookup.get(this); 234 | } 235 | 236 | private static Map lookup = new HashMap<>(); 237 | static { // defaults 238 | for (Elem elem : Elem.values()) { 239 | lookup.put(elem, colorMap.get(AaAnsi.ColorMode.STANDARD).get(elem)); 240 | } 241 | } 242 | 243 | static void updateColors(ColorMode mode) { 244 | Map whichCols; 245 | if (colorMap.containsKey(mode)) { 246 | whichCols = colorMap.get(mode); 247 | for (Elem elem : whichCols.keySet()) { 248 | elem.updateColor(whichCols.get(elem)); 249 | } 250 | } 251 | } 252 | 253 | private void updateColor(Col newColor) { 254 | lookup.put(this, newColor); 255 | } 256 | 257 | // static final int[] MATRIX_COLORS = new int[] { /* 22, */ 28, 34, 40, 46, /* 83, 120, */ 157 }; 258 | static final int[] MATRIX_COLORS = new int[] { /* 22, */ 28, 34, 40, 46, 120, /* 157 */ }; 259 | 260 | Col getCurrentColor() { 261 | if (AaAnsi.getColorMode() == ColorMode.MATRIX) { 262 | return new Col(MATRIX_COLORS[(int)(Math.random() * MATRIX_COLORS.length)]); 263 | // if you comment this out, check: public AaAnsi aa(AaAnsi aa) 264 | } 265 | // else... 266 | return lookup.get(this); 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/ConfigState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import java.lang.reflect.Method; 20 | import java.nio.charset.Charset; 21 | import java.nio.charset.CharsetDecoder; 22 | import java.nio.charset.CodingErrorAction; 23 | import java.nio.charset.StandardCharsets; 24 | import java.util.ArrayList; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.regex.Pattern; 29 | 30 | import com.solace.labs.topic.Sub; 31 | 32 | import dev.solace.aaron.useful.BoundedLinkedList; 33 | 34 | public class ConfigState { 35 | 36 | private static final int TOPICS_LENGTH_LIST_SIZE = 50; 37 | private static final int TOPIC_LEVELS_LENGTH_LIST_SIZE = 200; 38 | 39 | 40 | boolean isShutdown = false; // are we done yet? 41 | boolean isConnected = false; 42 | boolean isFlowActive = false; 43 | boolean includeTimestamp = false; 44 | boolean noExport = true; 45 | boolean isCompressed = false; 46 | 47 | int highlightTopicLevel = -1; 48 | int INDENT = 2; // default starting value, keeping it all-caps for retro v0.0.1 value 49 | boolean oneLineMode = false; 50 | boolean noPayload = false; 51 | boolean onlyPayload = false; 52 | boolean autoResizeIndent = false; // specify -1 as indent for this MODE 53 | boolean autoSpaceTopicLevels = false; // specify +something to space out the levels 54 | boolean autoTrimPayload = false; 55 | 56 | enum DisplayType { 57 | NORMAL, 58 | RAW, 59 | DUMP, 60 | ; 61 | } 62 | DisplayType payloadDisplay = DisplayType.NORMAL; 63 | 64 | BoundedLinkedList.ComparableList topicsLengthList = new BoundedLinkedList.ComparableList<>(TOPICS_LENGTH_LIST_SIZE); 65 | List> topicLevelsLengthList = new ArrayList<>(); 66 | BoundedLinkedList lastNMessagesList = null; 67 | 68 | long currentMsgCount = 0; 69 | long filteredCount = 0; 70 | Pattern filterRegexPattern = null; 71 | 72 | Charset charset = StandardCharsets.UTF_8; 73 | CharsetDecoder decoder = charset.newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE); 74 | 75 | Map protobufCallbacks = new HashMap<>(); 76 | 77 | 78 | static String DTF_FORMAT = "HH:mm:ss.SS "; 79 | 80 | public void setAutoTrimPayload(boolean trim) { 81 | autoTrimPayload = trim; 82 | } 83 | 84 | public void toggleAutoTrimPayload() { 85 | autoTrimPayload = !autoTrimPayload; 86 | } 87 | 88 | public void enableLastNMessage(int amount) { 89 | lastNMessagesList = new BoundedLinkedList<>(amount); 90 | } 91 | 92 | public boolean isOneLineMode() { 93 | return oneLineMode; 94 | } 95 | 96 | public boolean isNoPayload() { 97 | return noPayload; 98 | } 99 | 100 | public boolean isAutoResizeIndent() { 101 | return autoResizeIndent; 102 | } 103 | 104 | public boolean isAutoTrimPayload() { 105 | return autoTrimPayload; 106 | } 107 | 108 | public void setAutoSpaceTopicLevels(boolean enabled) { 109 | autoSpaceTopicLevels = enabled; 110 | } 111 | 112 | public boolean isAutoSpaceTopicLevelsEnabled() { 113 | return autoSpaceTopicLevels; 114 | } 115 | 116 | public boolean isLastNMessagesEnabled() { 117 | return lastNMessagesList != null; 118 | } 119 | 120 | public int getHighlightedTopicLevel() { 121 | return highlightTopicLevel; 122 | } 123 | 124 | public void setHighlightedTopicLevel(int level) { 125 | highlightTopicLevel = level; 126 | } 127 | 128 | public int getLastNMessagesSize() { 129 | if (isLastNMessagesEnabled()) return lastNMessagesList.size(); 130 | else return 0; 131 | } 132 | 133 | public int getLastNMessagesCapacity() { 134 | if (isLastNMessagesEnabled()) return lastNMessagesList.capacity(); 135 | else return 0; 136 | } 137 | 138 | public BoundedLinkedList getLastNMessages() { 139 | return lastNMessagesList; 140 | } 141 | 142 | 143 | 144 | /** this method tracks what the longest topic string has been for the last 50 messages, so things line up nicely with indent mode "-1" 145 | * only used in "-1" auto-indent mode 146 | */ 147 | public void updateTopicIndentValue(int maxTopicLength) { 148 | if (autoResizeIndent) { 149 | topicsLengthList.add(maxTopicLength); 150 | if (topicsLengthList.max() + 1 != INDENT) { // changed our current max 151 | // int from = Math.abs(INDENT); 152 | INDENT = topicsLengthList.max() + 1; // so INDENT will always be at least 3 (even MQTT spec states topic must be length > 1) 153 | // System.out.println(new org.fusesource.jansi.Ansi().reset().a(org.fusesource.jansi.Ansi.Attribute.INTENSITY_FAINT).a("** changing INDENT from " + from + " to " + Math.abs(INDENT) + "**").reset().toString()); 154 | // } else { 155 | // System.out.println(new org.fusesource.jansi.Ansi().reset().a(org.fusesource.jansi.Ansi.Attribute.INTENSITY_FAINT).a("** keeping INDENT = " + Math.abs(INDENT) + "**").reset().toString()); 156 | } 157 | } 158 | } 159 | 160 | 161 | /** returns a topic string that has had all its levels spaced out by the appropriate amount accoding to the topic lengths list */ 162 | public String updateTopicSpaceOutLevels(String topic) { 163 | if (autoSpaceTopicLevels) { 164 | String[] levels = topic.split("/"); 165 | if (topicLevelsLengthList.size() < levels.length) { 166 | for (int i=topicLevelsLengthList.size(); i < levels.length; i++) { 167 | topicLevelsLengthList.add(new BoundedLinkedList.ComparableList(TOPIC_LEVELS_LENGTH_LIST_SIZE)); // Keep column sizes for 200 msgs 168 | } 169 | } 170 | for (int i=0; i < topicLevelsLengthList.size(); i++) { 171 | if (i < levels.length) { 172 | topicLevelsLengthList.get(i).add(levels[i].length()); 173 | } else { 174 | topicLevelsLengthList.get(i).add(0); 175 | } 176 | } 177 | StringBuilder sb = new StringBuilder(); 178 | for (int i=0; i < levels.length; i++) { 179 | sb.append(levels[i]); 180 | int max = topicLevelsLengthList.get(i).max(); 181 | if (i < levels.length-1) { 182 | if (max > levels[i].length()) { 183 | sb.append(UsefulUtils.pad(max - levels[i].length(), /*'⋅'*/ '·' )); 184 | } 185 | if (INDENT == Integer.MIN_VALUE) sb.append("·/");// ("⋅/"); // always space out topic-only mode 186 | else sb.append('/'); 187 | } 188 | } 189 | return sb.toString(); 190 | } else { 191 | return topic; 192 | } 193 | } 194 | 195 | 196 | 197 | public int getCurrentIndent() { 198 | return INDENT; 199 | } 200 | 201 | /** for auto-indent one-line "-1" mode */ 202 | int getFormattingIndent() { 203 | if (oneLineMode) return 0; 204 | return INDENT; 205 | // return Math.min(INDENT, currentScreenWidth - 15); 206 | } 207 | 208 | /** Throws NumberFormat if it can't be parsed, or IllegalArgument if it is a number, but invalid */ 209 | public void dealWithIndentParam(String indentStr) throws NumberFormatException, IllegalArgumentException { 210 | // first, switch any pluses to minuses 211 | if (indentStr.startsWith("+") && indentStr.length() >= 2) { 212 | autoSpaceTopicLevels = true; 213 | indentStr = "-" + indentStr.substring(1); 214 | } 215 | int indent = Integer.parseInt(indentStr); // might throw 216 | if ((indent < -250 && indent != -300) || indent > 8) throw new IllegalArgumentException(); 217 | INDENT = indent; 218 | if (INDENT < 0) { 219 | oneLineMode = true; 220 | if (INDENT == -1) { 221 | autoResizeIndent = true; // use auto-resizing based on max topic length 222 | INDENT = 3; // starting value (1 + 2 for padding) 223 | updateTopicIndentValue(2); // now update it 224 | } else if (INDENT == -2) { // two line mode 225 | INDENT = Math.abs(INDENT); 226 | } else if (INDENT == -300) { // special no-payload mode 227 | onlyPayload = true; 228 | } else { 229 | INDENT = Math.abs(INDENT) + 2; // TODO why is this 2? I think 1 for ... and 1 for space 230 | // updateTopicIndentValue(INDENT); // now update it TODO why do we need to update if not auto-indenting? 231 | } 232 | } else if (INDENT == 0) { 233 | if (indentStr.equals("-0")) { // special case, print topic only 234 | INDENT = Integer.MIN_VALUE; // not necessary anymore 235 | oneLineMode = true; 236 | noPayload = true; 237 | } else if (indentStr.equals("00")) { 238 | INDENT = 2; 239 | noPayload = true; 240 | } else if (indentStr.equals("000")) { 241 | noPayload = true; 242 | // INDENT = 0; // that's already done above! 243 | } else if (indentStr.equals("0")) { 244 | // nothing, normal 245 | } else if (indentStr.equals("0000")) { // something new, no user proper or data 246 | INDENT = 0; 247 | noPayload = true; 248 | autoTrimPayload = true; 249 | } else { // shouldn't be anything else (e.g. "0000") 250 | throw new IllegalArgumentException(); 251 | } 252 | } 253 | } 254 | 255 | public void setRegexFilterPattern(Pattern regex) { 256 | filterRegexPattern = regex; 257 | } 258 | 259 | public void incMessageCount() { 260 | currentMsgCount++; 261 | } 262 | 263 | public void incFilteredCount() { 264 | filteredCount++; 265 | } 266 | 267 | public long getMessageCount() { 268 | return currentMsgCount; 269 | } 270 | 271 | public long getFilteredCount() { 272 | return filteredCount; 273 | } 274 | 275 | public void setProtobufCallbacks(Map map) { 276 | protobufCallbacks = map; 277 | } 278 | 279 | 280 | 281 | 282 | /** used when calculating one-line indent values, includes a space at the end of the timestamp */ 283 | public int getTimestampIndentIfEnabled() { 284 | if (includeTimestamp) return DTF_FORMAT.length(); 285 | else return 0; 286 | } 287 | 288 | public void setCharset(Charset charset) { 289 | this.charset = charset; 290 | // I think replace is the default action anyhow? This must be leftover from when I was erroring? 291 | decoder = this.charset.newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/SdtUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import static com.solace.labs.aaron.UsefulUtils.indent; 20 | 21 | import java.util.Iterator; 22 | 23 | import com.solacesystems.common.util.ByteArray; 24 | import com.solacesystems.jcsmp.SDTException; 25 | import com.solacesystems.jcsmp.SDTMap; 26 | import com.solacesystems.jcsmp.SDTStream; 27 | import com.solacesystems.jcsmp.Topic; 28 | 29 | public class SdtUtils { 30 | 31 | static int countElements(SDTMap map) { 32 | int count = 0; 33 | try { 34 | Iterator it = map.keySet().iterator(); 35 | while (it.hasNext()) { 36 | Object value = map.get(it.next()); 37 | if (value instanceof SDTMap) { 38 | count += countElements((SDTMap)value); 39 | } else if (value instanceof SDTStream) { 40 | count += countElements((SDTStream)value); 41 | } else count++; // just a regular element 42 | } 43 | } catch (SDTException e) { // shouldn't happen, we know it's well-defined since we received it 44 | return -1; 45 | } 46 | return count; 47 | } 48 | 49 | static int countElements(SDTStream stream) { 50 | int count = 0; 51 | try { 52 | while (stream.hasRemaining()) { 53 | Object value = stream.read(); 54 | if (value instanceof SDTMap) { 55 | count += countElements((SDTMap)value); 56 | } else if (value instanceof SDTStream) { 57 | count += countElements((SDTStream)value); 58 | } else count++; // just a regular element 59 | } 60 | } catch (SDTException e) { // shouldn't happen, we know it's well-defined since we received it 61 | return -1; 62 | } 63 | return count; 64 | } 65 | 66 | static AaAnsi printMap(SDTMap map, final int indentFactor) { 67 | try { 68 | return privPrintMap(map, indentFactor, indentFactor).reset(); 69 | } catch (SDTException e) { 70 | throw new IllegalArgumentException("Could not parse SDT", e); 71 | } 72 | } 73 | 74 | private static AaAnsi privPrintMap(SDTMap map, final int curIndent, final int indentFactor) throws SDTException { 75 | AaAnsi ansi = AaAnsi.n(); 76 | privPrintMap(map, curIndent, indentFactor, ansi); 77 | if (indentFactor <= 0) return AaAnsi.n().fg(Elem.BRACE).a('{').aa(ansi).a('}'); 78 | else return ansi; 79 | } 80 | 81 | private static void privPrintMap(SDTMap map, final int curIndent, final int indentFactor, AaAnsi ansi) throws SDTException { 82 | if (map == null) { 83 | return; 84 | } else if (map.isEmpty()) { 85 | ansi.a(indent(curIndent)).fg(Elem.NULL).a("").reset(); 86 | return; 87 | } 88 | // String strIndent = indent(indent); 89 | Iterator it = map.keySet().iterator(); 90 | while (it.hasNext()) { 91 | String key = it.next(); 92 | Object value = map.get(key); 93 | String strValue = String.valueOf(value); 94 | AaAnsi ansiValue = null; // AaAnsi.n().fg(Elem.guessByType(value)).a(strValue); 95 | String type = "NULL"; // default value of the type for now, will get overwritten with actual object type 96 | // that's how SdkPerf JCSMP does it: 97 | if (value != null) { 98 | Class valuClass = value.getClass(); 99 | type = valuClass.getSimpleName(); 100 | if (type.endsWith("Impl")) { 101 | type = type.substring(0, type.length()-4); 102 | } else if (value instanceof CharSequence) { 103 | strValue = '"' + strValue + '"'; // double quotes for strings 104 | // strValue = '“' + strValue + '”'; // double quotes for strings 105 | } else if (value instanceof Character && indentFactor > 0) { 106 | strValue = "'" + strValue + "'"; // single quotes for chars 107 | } else if (value instanceof ByteArray) { 108 | if (((ByteArray)value).getLength() > 16 && indentFactor > 0) { 109 | 110 | } else { 111 | strValue = UsefulUtils.bytesToSpacedHexString(((ByteArray)value).asBytes()); 112 | } 113 | } 114 | if (value instanceof SDTMap) { 115 | AaAnsi inner = AaAnsi.n(); 116 | if (indentFactor > 0) inner.a("\n"); 117 | // else inner.a(" "); 118 | inner.aa(privPrintMap((SDTMap) value, curIndent + indentFactor, indentFactor)); 119 | ansiValue = inner; 120 | // strValue = inner.toString(); // overwrite 121 | } else if (value instanceof SDTStream) { 122 | AaAnsi inner = AaAnsi.n(); 123 | if (indentFactor > 0) inner.a("\n"); 124 | // else inner.a(" "); 125 | inner.aa(privPrintStream((SDTStream) value, curIndent + indentFactor, indentFactor)); 126 | ansiValue = inner; 127 | // strValue = inner.toString(); // overwrite 128 | } else { // everything else 129 | ansiValue = AaAnsi.n().fg(Elem.guessByType(value)).a(strValue); // update 130 | } 131 | } else { // value is null, but there is no way to query the map for the data type 132 | ansiValue = AaAnsi.n().fg(Elem.NULL).a(strValue); // update 133 | } 134 | ansi.reset().a(indent(curIndent)); 135 | // if (indentFactor > 0) ansi.a("Key "); 136 | // ansi.fg(Elem.KEY).a("'").a(key).a("'").reset(); 137 | // if (indentFactor > 0) ansi.a(' '); 138 | if (indentFactor > 0) ansi.a("Key ").fg(Elem.KEY).a("'").a(key).a("' ");//.reset(); 139 | else ansi.fg(Elem.KEY).a(key); // no more single quotes on key name for compressed view 140 | 141 | // ansi.reset().a(indent(indent)); 142 | // if (indentFactor > 0) { 143 | // ansi.a("Key ").fg(Elem.KEY).a("'").a(key).a("' ").reset(); 144 | // } else { 145 | // ansi.fg(Elem.KEY).a(key).reset(); 146 | // } 147 | if (indentFactor > 0) { 148 | ansi.fg(Elem.DATA_TYPE); 149 | ansi.a('(').a(type).a(')'); 150 | // } else { 151 | // ansi.reset().a('='); 152 | } 153 | ansi.reset(); 154 | if (indentFactor > 0) ansi.a(':').a(' '); 155 | else ansi.a('='); 156 | if (value instanceof Topic) ansi.colorizeTopic(strValue, -1).reset(); 157 | else { 158 | ansi.aa(ansiValue).reset(); 159 | if (indentFactor > 0) { // i.e. not compressed 160 | if (value instanceof Long) { 161 | String ts = UsefulUtils.guessIfTimestampLong(key, (long)value); 162 | if (ts != null) { 163 | // ansi.fg(Elem.CHAR).a(ts); 164 | ansi.fg(Elem.NUMBER).faintOn().a(ts).reset(); 165 | } 166 | } else if (value instanceof String) { 167 | String ts = UsefulUtils.guessIfTimestampString(key, (String)value); 168 | if (ts != null) { 169 | ansi.fg(Elem.STRING).faintOn().a(ts).reset(); 170 | } 171 | } 172 | } 173 | } 174 | if (it.hasNext()) { 175 | if (indentFactor > 0) ansi.a("\n"); 176 | else ansi.a(","); 177 | } 178 | } 179 | } 180 | 181 | 182 | 183 | 184 | static AaAnsi printStream(SDTStream stream, final int indentFactor) { 185 | try { 186 | return privPrintStream(stream, indentFactor, indentFactor).reset(); 187 | } catch (SDTException e) { 188 | throw new IllegalArgumentException("Could not parse SDT", e); 189 | } 190 | } 191 | 192 | private static AaAnsi privPrintStream(SDTStream stream, final int curIndent, final int indentFactor) throws SDTException { 193 | AaAnsi ansi = AaAnsi.n(); 194 | privPrintStream(stream, curIndent, indentFactor, ansi); 195 | // if (indentFactor <= 0) return new AaAnsi().fg(Elem.BRACE).a('[') + ansi.toString() + new AaAnsi().fg(Elem.BRACE).a(']').reset(); 196 | if (indentFactor <= 0) return AaAnsi.n().fg(Elem.BRACE).a('[').a(' ').aa(ansi).a(' ').a(']'); 197 | else return ansi; 198 | } 199 | 200 | 201 | 202 | private static void privPrintStream(SDTStream stream, final int indent, final int indentFactor, AaAnsi ansi) throws SDTException { 203 | if (stream == null) { 204 | return; 205 | } else if (!stream.hasRemaining()) { 206 | ansi.a(indent(indent)).fg(Elem.NULL).a("").reset(); 207 | } 208 | // String strIndent = ; 209 | while (stream.hasRemaining()) { 210 | Object value = stream.read(); 211 | String strValue = String.valueOf(value); 212 | AaAnsi strValue2 = null;// AaAnsi.n().fg(Elem.guessByType(value)).a(String.valueOf(strValue)); 213 | String type = "NULL"; 214 | if (value != null) { 215 | Class valuClass = value.getClass(); 216 | type = valuClass.getSimpleName(); 217 | if (type.endsWith("Impl")) { // maps and streams 218 | type = type.substring(0, type.length()-4); 219 | } else if (value instanceof CharSequence) { 220 | strValue = '"' + strValue + '"'; 221 | } else if (value instanceof Character && indentFactor > 0) { 222 | strValue = "'" + strValue + "'"; 223 | } else if (value instanceof ByteArray) { 224 | strValue = UsefulUtils.bytesToSpacedHexString(((ByteArray)value).asBytes()); 225 | } 226 | // strValue2 = AaAnsi.n().fg(Elem.guessByType(value)).a(String.valueOf(strValue)); // update 227 | if (value instanceof SDTMap) { 228 | AaAnsi inner = AaAnsi.n(); 229 | if (indentFactor > 0) inner.a("\n"); 230 | // else inner.a(" "); 231 | inner.aa(privPrintMap((SDTMap) value, indent + indentFactor, indentFactor)); 232 | // else 233 | // a.a("{ ").reset().a(privPrintMap((SDTMap) value, indent + indentFactor, indentFactor)).a(" }").reset(); 234 | // strValue = inner.toString(); // overwrite 235 | strValue2 = inner; 236 | } else if (value instanceof SDTStream) { 237 | AaAnsi inner = AaAnsi.n(); 238 | if (indentFactor > 0) inner.a("\n"); 239 | // else inner.a(" "); 240 | inner.aa(privPrintStream((SDTStream) value, indent + indentFactor, indentFactor)); 241 | // else 242 | // inner.fg(Elem.BYTES).a("[ ").reset().a(privPrintStream((SDTStream)value, indent + indentFactor, indentFactor)).reset().a(" ]").reset(); 243 | // strValue = inner.toString(); // overwrite 244 | strValue2 = inner; 245 | } else { 246 | strValue2 = AaAnsi.n().fg(Elem.guessByType(value)).a(strValue); // update 247 | } 248 | } else { // value must be null 249 | strValue2 = AaAnsi.n().fg(Elem.NULL).a(strValue); // update 250 | } 251 | ansi.reset().a(indent(indent)); 252 | if (indentFactor > 0) { 253 | ansi.fg(Elem.DATA_TYPE).a('(').a(type).a(")").reset().a(": "); 254 | } else { 255 | // ansi.fg(Elem.DATA_TYPE).a('(').a(type).a(")").reset(); 256 | } 257 | if (value instanceof Topic) ansi.colorizeTopic(strValue, -1).reset(); 258 | else ansi.aa(strValue2).reset(); 259 | if (stream.hasRemaining() ) { 260 | if (indentFactor > 0) ansi.a("\n"); 261 | else ansi.a(","); 262 | } 263 | } 264 | stream.rewind(); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/HelperText.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import java.io.PrintStream; 20 | 21 | /** 22 | * Utility class just to move all the text help out of the PrettyDump class 23 | */ 24 | public class HelperText { 25 | 26 | private static final PrintStream o = System.out; 27 | 28 | 29 | static void printHelpExamples() { 30 | o.println("Command line examples:"); 31 | o.println("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾"); 32 | o.println("prettydump 'solace/>'"); 33 | o.println(" Localhost broker using default credentials, subscribe to \"#noexport/solace/>\", regular indent formatting"); 34 | o.println(); 35 | o.println("prettydump b:q1 0"); 36 | o.println(" Localhost broker using default credentials, browse queue \"q1\", compress payload display"); 37 | o.println(); 38 | o.println("prettydump ws://localhost:8008 testvpn test pa55word '>' -1"); 39 | o.println(" Localhost broker using WebSockets, \"testvpn\" VPN, user \"test\", subscribe to \"#noexport/>\", and enable auto-indent one-line mode"); 40 | o.println(); 41 | o.println("prettydump tcps://demo.messaging.solace.cloud:55443 demo solace-cloud-client pw123 --export 'logs/>' -2"); 42 | o.println(" Solace Cloud broker, subscribe to \"logs/>\", and enable two-line mode"); 43 | o.println(); 44 | o.println("prettydump 10.0.7.49 uat browser pw b:q.stg.logging 00 --count=-100"); 45 | o.println(" Network broker, \"uat\" VPN, user \"browser\", browse queue \"q.stg.logging\" but only dump last 100 messages, and hide payload"); 46 | o.println(); 47 | o.println("prettydump 'tq:>,!bus/>,!stats/>' -50"); 48 | o.println(" Localhost broker, subscribe to everything BUT \"bus\" and \"stats\" topics (need a tempQ for NOT subscriptions), one-line mode, trim topic to 50 chars"); 49 | o.println(); 50 | o.println("prettydump 10.0.7.49 uat app1 secretPw q:dmq.dev.app1 --count=1 --SUB_ACK_WINDOW_SIZE=1 "); 51 | o.println(" Network broker, consume/ACK & dump one message off queue \"dmq.dev.app1\", and set the AD window size to 1"); 52 | o.println(); 53 | o.println("prettydump tcps://stage1:55443 uat1 user1 pw b:massive.q --selector='dmq.dev.app1 --count=1 --SUB_ACK_WINDOW_SIZE=1 "); 54 | o.println(" Network broker, consume/ACK & dump one message off queue \"dmq.dev.app1\", and set the AD window size to 1"); 55 | o.println(); 56 | // TODO what is the meaning of exporting vs. not exporting a not subscription 57 | 58 | 59 | o.println("Runtime examples: type these and press [ENTER]"); 60 | o.println("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾"); 61 | o.println("cm : switch to \"minimal\" colour mode (only topic coloured)"); 62 | o.println("3 : highlight the 3rd topic level, dim the others"); 63 | o.println("t : in one-line/compressed modes: trim the payload to the terminal width"); 64 | // o.println(" in regular mode, trime user props and payload to 30 lines"); 65 | o.println("+ : in one-line mode, enable topic level spacing"); 66 | 67 | } 68 | 69 | 70 | 71 | static void printHelpIndent() { 72 | 73 | o.println(" • 1..8 normal mode, pretty-printed and indented n spaces"); 74 | o.println(" • 0 normal mode, payload and user properties compressed to one line"); 75 | o.println(" • 00 no payload mode, user properties still pretty-printed"); 76 | o.println(" • 000 no payload mode, user properties each compressed to one line"); 77 | o.println(" • 0000 no payload mode, no user properties, headers only"); 78 | o.println(" • -250..-3 one-line mode, topic and payload only, compressed, abs(n) fixed indent"); 79 | o.println(" • -2 two-line mode, topic and payload on two lines (try 'minimal' color mode)"); 80 | o.println(" • -1 one-line mode, automatic variable payload indentation"); 81 | o.println(" • -0 one-line mode, topic only"); 82 | o.println(" • For one-line modes, change '-' to '+' to enable topic level alignment"); 83 | // o.println(" - ±0 one-line mode, topic only, with/without topic spacing"); 84 | // o.println("Runtime: press 't'[ENTER] (or argument '--trim') to auto-trim payload to screen width"); 85 | // o.println("Runtime: press '+' or '-'[ENTER] to toggle topic level spacing during runtime"); 86 | //// o.println("Runtime: press \"t[ENTER]\" to toggle payload trim to terminal width (or argument --trim)"); 87 | //// o.println("Runtime: press \"+ or -[ENTER]\" to toggle topic level spacing/alignment (or argument \"+indent\")"); 88 | // o.println("NOTE: optional content Filter searches entire message body, regardless of indent"); 89 | //// printUsageText(); 90 | // o.println("See README.md for more detailed help with indent."); 91 | 92 | } 93 | 94 | 95 | static void printHelpMoreText() { 96 | printHelpText(false); 97 | o.println(" • e.g. prettydump 'logs/>' -1 ~or~ prettydump q:q1 ~or~ prettydump b:dmq -0"); 98 | o.println(" - Optional indent: integer, valid values:"); 99 | printHelpIndent(); 100 | // o.println(" - Optional indent: integer, default==2 spaces; specifying 0 compresses payload formatting"); 101 | // o.println(" - No payload mode: use indent '00' to only show headers and props, or '000' for compressed"); 102 | // o.println(" - One-line mode: use negative indent value (trim topic length) for topic & payload only"); 103 | // o.println(" - Or use -1 for auto column width adjustment, or -2 for two-line mode"); 104 | // o.println(" - Use negative zero -0 for topic only, no payload"); 105 | // o.println(" - Optional count: stop after receiving n number of msgs; or if < 0, only show last n msgs"); 106 | 107 | o.println(" - Additional non-ordered arguments: for more advanced capabilities"); 108 | o.println(" • --selector=\"mi like 'hello%world'\" Selector for Queue consume and browse"); 109 | o.println(" • --filter=\"ABC123\" client-side regex content filter on any received message dump"); 110 | o.println(" • --count=n stop after receiving n number of msgs; or if < 0, only show last n msgs"); 111 | // o.println(" • --skip=n skip the first n messages received"); 112 | 113 | o.println(" • --raw show original message payload, not pretty-printed; text, JSON, XML only"); 114 | o.println(" • --dump enable binary dump for every message (useful for charset encoding issues)"); 115 | o.println(" • --trim enable paylaod trim for one-line (and two-line) modes"); 116 | o.println(" • --ts print time when PrettyDump received the message (not messages' timestamp)"); 117 | o.println(" • --export disables the automatic prefixing of \"#noexport/\" to the start of all topics"); 118 | o.println(" • --compressed tells JCSMP API to use streaming compression (TCP only, not WebSockets)"); 119 | o.println(" • --defaults show all possible JCSMP Session properties to set/override"); 120 | o.println(" - One-Line runtime options: type the following into the console while the app is running"); 121 | o.println(" • Press \"t\" ENTER to toggle payload trim to terminal width (or argument --trim)"); 122 | o.println(" • Press \"+\" or \"-\" ENTER to toggle topic level spacing/alignment (or argument \"+indent\")"); 123 | o.println(" • Press \"[1-n]\" ENTER to highlight a particular topic level (\"0\" ENTER to revert)"); 124 | o.println(" • Type \"c[svlmxo]\" ENTER\" to set colour mode: standard, vivid, light, minimal, matrix, off"); 125 | o.println("Environment variable options:"); 126 | o.println(" - Default charset is UTF-8. Override by setting: export PRETTY_CHARSET=ISO-8859-1"); 127 | // o.println(" - e.g. export PRETTY_CHARSET=ISO-8859-1 (or \"set\" on Windows)"); 128 | o.println(" - Multiple colour schemes supported. Override by setting: export PRETTY_COLORS=whatever"); 129 | o.println(" • Choose: \"standard\" (default), \"vivid\", \"light\", \"minimal\", \"matrix\", \"off\""); 130 | o.println(); 131 | // o.println("Runtime commands, type these + [ENTER] while running:"); 132 | // o.println(" - Default charset is UTF-8. Override by setting: export PRETTY_CHARSET=ISO-8859-1"); 133 | // o.println(); 134 | o.println("SdkPerf Wrap mode: use any SdkPerf as usual, pipe command to \" | prettydump wrap\" to prettify"); 135 | o.println(); 136 | o.println("See the README.md for more explanations of every feature and capability"); 137 | o.println("https://github.com/SolaceLabs/solace-pretty-dump"); 138 | o.println("https://solace.community/discussion/3238/sdkperf-but-pretty-for-json-and-xml"); 139 | o.println(); 140 | } 141 | 142 | static void printHelpText(boolean full) { 143 | printUsageText(false); 144 | o.println(" - Default protocol TCP; for TLS use \"tcps://\"; or \"ws://\" or \"wss://\" for WebSocket"); 145 | o.println(" - Default parameters will be: localhost:55555 default foo bar '#noexport/>' 2"); 146 | o.println(" - Subscribing options (arg 5, or shortcut mode arg 1), one of:"); 147 | o.println(" • Comma-separated list of Direct topic subscriptions"); 148 | o.println(" - Automatic \"#noexport/\" prefixes added for DMR/MNR; disable with --export"); 149 | o.println(" • q:queueName to consume from queue"); 150 | o.println(" • b:queueName to browse a queue (all messages, or range by MsgSpoolID or RGMID)"); 151 | o.println(" • f:queueName to browse/dump only first oldest message on a queue"); 152 | o.println(" • tq:topics provision a tempQ with optional topics (can use NOT '!' topics)"); 153 | if (full) o.println(" - Indent: integer, default==2; ≥ 0 normal, = 00 no payload, ≤ -0 one-line mode"); 154 | // o.println(" - Optional count: stop after receiving n number of msgs; or if < 0, only show last n msgs"); 155 | o.println(" - Shortcut mode: first arg looks like a topic, or starts '[qbf]:', assume defaults"); 156 | o.println(" • Or if first arg parses as integer, select as indent, rest default options"); 157 | if (full) o.println(" - Additional args: --count, --filter, --selector, --trim, --ts, --raw, --compressed"); 158 | if (full) o.println(" - Any JCSMP Session property (use --defaults to see all)"); 159 | if (full) o.println(" - Environment variables for decoding charset and colour mode"); 160 | if (full) o.println(); 161 | if (full) o.println("prettydump -hm for more help on indent, additional parameters, charsets, and colours"); 162 | if (full) o.println("prettydump -he for examples"); 163 | if (full) o.println(); 164 | } 165 | 166 | static void printUsageText(boolean full) { 167 | o.println("Usage: prettydump [host] [vpn] [user] [pw] [topics|[qbf]:queueName|tq:topics] [indent]"); 168 | o.println(" or: prettydump [indent] for \"shortcut\" mode"); 169 | o.println(); 170 | if (full) o.println("prettydump -h or -hm for help on available arguments and environment variables."); 171 | if (full) o.println("prettydump -he for examples"); 172 | if (full) o.println(); 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Aaron Lee 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/PrettyWrap.java: -------------------------------------------------------------------------------- 1 | package com.solace.labs.aaron; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.nio.ByteBuffer; 7 | import java.util.Arrays; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import org.apache.logging.log4j.LogManager; 13 | import org.apache.logging.log4j.Logger; 14 | import org.fusesource.jansi.Ansi; 15 | import org.fusesource.jansi.AnsiConsole; 16 | 17 | import com.solace.labs.aaron.Banner.Which; 18 | import com.solacesystems.jcsmp.JCSMPException; 19 | 20 | public class PrettyWrap { 21 | 22 | private static final Logger logger = LogManager.getLogger(PrettyWrap.class); 23 | // private static Map protobufCallbacks = new HashMap<>(); 24 | 25 | /** ensures that c is either 0-9a-f */ 26 | private static boolean invalidHex(char c) { 27 | return c < 0x30 || c > 0x66 || (c >= 0x3a && c <= 0x60); 28 | } 29 | 30 | private static int convertChar(char c) { 31 | if (c < 64) { // means it's a number 32 | return (c - 48); 33 | } else { // letter a-f 34 | return (c - 87); 35 | } 36 | } 37 | 38 | private static int parseSdkPerf(String line, byte[] array) { 39 | int count = 0; 40 | int pos = 2; 41 | byte b = 0; 42 | while (pos < 55 && count < 16) { 43 | char c = line.charAt(pos); 44 | if (c == ' ') { 45 | pos++; 46 | continue; 47 | } 48 | char c2 = line.charAt(pos+1); 49 | if (invalidHex(c) || invalidHex(c2)) { 50 | throw new IllegalArgumentException("Line contains non-hex chars: '" + line + "'"); 51 | } 52 | // should check to make sure char is in range! 53 | b = (byte)(convertChar(c) << 4 | convertChar(c2)); 54 | array[count++] = b; 55 | pos += 3; 56 | } 57 | return count; 58 | } 59 | 60 | private static long lineIndentCount = 0; 61 | // private static final int[] bgCols = new int[] { 17, 53, 52, 58, 22, 23 }; 62 | 63 | private static void wrapPrintln(AaAnsi ansi) { 64 | wrapPrintln(ansi.toString()); 65 | } 66 | 67 | private static void wrapPrintln(String s) { 68 | if ("indent".equals("indent") && (AaAnsi.getColorMode() == AaAnsi.ColorMode.VIVID || AaAnsi.getColorMode() == AaAnsi.ColorMode.LIGHT)) { 69 | String[] lines = s.split("\n"); 70 | Ansi ansi = new Ansi(); 71 | // AaAnsi aa = new AaAnsi(); 72 | for (String line : lines) { 73 | ansi.fg(AaAnsi.rainbowTable[(int)((lineIndentCount++)/2 % AaAnsi.rainbowTable.length)]).a('│').reset().a(' ').a(line).a('\n'); 74 | // ansi.bg(bgCols[(int)((lineIndentCount++)/4 % bgCols.length)]).a('│').reset().a(' ').a(line).a('\n'); 75 | // ansi.fg(AaAnsi.rainbowTable[(int)((lineIndentCount++)/2 % AaAnsi.rainbowTable.length)]).bg(bgCols[(int)((lineIndentCount++)/4 % bgCols.length)]).a('│').reset().a(' ').a(line).a('\n'); 76 | // aa.makeFaint().a("│ ").reset().aRaw(line); 77 | } 78 | System.out.print(ansi.toString()); 79 | // System.out.println(aa.toString()); 80 | } else { 81 | System.out.println(s); 82 | } 83 | } 84 | 85 | /* 86 | Destination: Topic 'q1/abc' 87 | */ 88 | static String extractTopic(String line) { 89 | if (line.startsWith("Destination: Topic '") && line.endsWith("'")) { 90 | String topic = line.substring(47, line.length()-1); 91 | return topic; 92 | } 93 | return ""; 94 | } 95 | 96 | static volatile boolean shutdown = false; 97 | 98 | public static void main(String... args) throws JCSMPException, IOException, InterruptedException { 99 | 100 | AnsiConsole.systemInstall(); 101 | // protobufCallbacks = ProtoBufUtils.loadProtobufDefinitions(); 102 | MessageHelper payloadHelper = new MessageHelper(new ConfigState()); // default stuff 103 | BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 104 | // test code 105 | 106 | // String binaryPayload = "^^^^^^^^^^^^^^^^^^ Start Message ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" 107 | // + "Destination: Topic 'a'\n" 108 | // + "Priority: 4\n" 109 | // + "Class Of Service: COS_1\n" 110 | // + "DeliveryMode: DIRECT\n" 111 | // + "Binary Attachment: len=1029\n" 112 | //// + " 1c 10 00 04 14 00 00 00 08 00 4d 19 57 58 e4 44 PK...... ..M.WX.D\n\n" 113 | // + " 2b 00 00 00 10 00 00 00 08 00 4d 19 57 58 e4 44 PK...... ..M.WX.D\n\n" 114 | // + "^^^^^^^^^^^^^^^^^^ End Message ^^^^^^^^^^^^^^^^^^^^^^^^^^^"; 115 | // BufferedReader in2 = new BufferedReader(new StringReader(binaryPayload)); 116 | 117 | 118 | 119 | 120 | 121 | 122 | boolean insideMessage = false; 123 | boolean insidePayloadSection = false; 124 | boolean startingPayload = false; 125 | byte[] perLineArray = new byte[16]; // for reuse 126 | ByteBuffer bb = ByteBuffer.allocate(1024 * 1024); // 1MB, allocate once and reuse! 127 | Arrays.fill(perLineArray, (byte)0); 128 | int msgCount = 0; 129 | boolean legalPayload = true; 130 | boolean ignore = false; 131 | // String topic = ""; 132 | ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); 133 | 134 | System.out.println(Banner.printBanner(Which.WRAP)); 135 | System.out.print(AaAnsi.n().fg(Elem.PAYLOAD_TYPE).a(String.format("PrettyDump WRAP mode for SdkPerf enabled... 😎%n%n")).toString()); 136 | AaAnsi.resetAnsi(System.out); 137 | try { 138 | while (!shutdown) { 139 | String input = in.readLine(); 140 | if (input == null) { // nothing to do 141 | Thread.sleep(50); 142 | // System.out.print("."); 143 | } else { 144 | // seenInput = true; 145 | // if (!seenAtLeastSomeInputInOneSecond.get()) { // first time seeing something 146 | // seenAtLeastSomeInputInOneSecond.set(true); 147 | // System.out.println(Banner.printBanner(Which.WRAP)); 148 | // System.out.print(new AaAnsi().fg(Elem.PAYLOAD_TYPE).a(String.format("PrettyDump WRAP mode for SdkPerf enabled... 😎%n%n")).toString()); 149 | // AaAnsi.resetAnsi(System.out); 150 | // } 151 | // probably should put the most likely/common lines at the top here..! 152 | if (ignore == true && input.isEmpty()) { 153 | ignore = false; // end of ignoring SDTMap section! 154 | // but now what if we're wrapping C SdkPerf and there is no binary section? Just need to dump out the map 155 | } else if (ignore) { 156 | // continue 157 | } else if (input.contains("^^^ Start Message ^^^")) { 158 | assert !insideMessage; 159 | insideMessage = true; 160 | // System.out.println( input); 161 | wrapPrintln(MessageObject.printMessageStart(++msgCount)); 162 | // wrapPrintln(MessageHelper.printMessageStart()); 163 | } else if (input.contains("^^^ End Message ^^^")) { 164 | assert insideMessage; 165 | assert !insidePayloadSection; 166 | insideMessage = false; 167 | // System.out.println(input); 168 | wrapPrintln(MessageObject.printMessageEnd(msgCount)); 169 | // wrapPrintln(MessageHelper.printMessageEnd()); 170 | AaAnsi.resetAnsi(System.out); 171 | // } else if (input.matches("^(?:XML:|Binary Attachment:).*len.*")) { 172 | } else if (input.startsWith("XML: ") || input.startsWith("Binary Attachment: ") || input.startsWith("Binary Attachment String: ") || input.startsWith("User Data: ")) { 173 | // else if we're wrapping CCSMP, then there is no raw payload section, but it will say "Binary Attachment Map:" and we'll just keep dumping stuff out 174 | assert insideMessage; 175 | assert !insidePayloadSection; 176 | insidePayloadSection = true; 177 | startingPayload = true; // should we reset the ByteBuffer just in case? 178 | bb.clear(); 179 | legalPayload = true; 180 | // payloadSb = new StringBuilder(); 181 | wrapPrintln(input); 182 | // } else if (input.startsWith("Binary Attachment Map: ") || input.startsWith("Binary Attachment Stream: ")) { 183 | // commented out because otherwise can't detect end of payload section in C SdkPerf 184 | // makeFancyString = true; // make each line slightly fancier formatting 185 | // wrapPrintln(input); 186 | } else if (input.startsWith("Destination: ") || input.startsWith("JMSDestination: ")) { 187 | if (input.startsWith("Dest")) { 188 | // topic = extractTopic(input); // if it is an actual topic, this will return it; otherwise empty string 189 | } 190 | wrapPrintln(MessageHelper.colorizeDestinationString(input)); 191 | // System.out.println(new AaAnsi().fg(Elem.DESTINATION).a(input).toString()); 192 | } else if (input.startsWith("SDT Map: ") || input.startsWith("SDT Stream: ")) { // this is only for JCSMP and derivatives 193 | ignore = true; 194 | } else if (startingPayload) { // we just started, so we know this line is the first line 195 | assert insideMessage; 196 | assert insidePayloadSection; 197 | try { 198 | bb.put(perLineArray, 0, parseSdkPerf(input, perLineArray)); 199 | } catch (IllegalArgumentException e) { 200 | legalPayload = false; 201 | bb.put(input.getBytes()); 202 | } 203 | startingPayload = false; 204 | } else if (insidePayloadSection) { 205 | if (input.isEmpty() || !input.startsWith(" ")) { // end of payload section! 206 | insidePayloadSection = false; 207 | bb.flip(); // ready to read! 208 | PayloadSection payload = payloadHelper.buildPayloadSection(bb); 209 | if (payload.getType().contains("Non ") || payload.getType().contains("INVALID")) { 210 | wrapPrintln(AaAnsi.n().invalid(payload.getType()).toString()); 211 | } else { 212 | wrapPrintln(AaAnsi.n().fg(Elem.PAYLOAD_TYPE).a(payload.getType()).toString()); 213 | } 214 | wrapPrintln(payload.getFormattedPayload()); 215 | if (!input.isEmpty()) wrapPrintln(input); 216 | } else { // just gathering payload data here 217 | if (legalPayload) { // continue 218 | bb.put(perLineArray, 0, parseSdkPerf(input, perLineArray)); 219 | } else { 220 | bb.put(input.getBytes()); 221 | } 222 | } 223 | // } else if (input.startsWith("JMSProperties: ")) { 224 | // AaAnsi ansi = new AaAnsi().a(input.substring(0,40)).aStyledString(input.substring(40)); 225 | // System.out.println(ansi); 226 | } else if (insideMessage) { 227 | // if (input.length() > 42 && input.charAt(40) == '{' && input.endsWith("}")) { 228 | // String sub = formatMapLookingThing(input.substring(40)); 229 | // wrapPrintln(new StringBuilder().append(input.substring(0,40)).append(sub).toString()); 230 | // } else { 231 | // wrapPrintln(input); 232 | //// wrapPrintln(AaAnsi.n().aStyledString(input).toString()); 233 | // } 234 | wrapPrintln(UsefulUtils.guessIfMapLookingThing(input)); 235 | // if (input.isEmpty()) makeFancyString = false; 236 | // if (makeFancyString) wrapPrintln(AaAnsi.n().aStyledString(input).toString()); 237 | // else wrapPrintln(input); 238 | } else { // outside a message... 239 | if (input.startsWith("PUB MR(5s)")) { 240 | try { 241 | // PUB MR(5s)= 0, SUB MR(5s)= 0, CPU=0 242 | // PUB MR(5s)= 0, SUB MR(5s)= 0, CPU=0 243 | String[] pieces = input.split("="); 244 | // [PUB MR(5s)][ 0, SUB MR(5s)][ 0, CPU][0] 245 | String[] pub = pieces[1].split(","); 246 | // [PUB MR(5s)][ 0][ SUB MR(5s)][ 0, CPU][0] 247 | String[] sub = pieces[2].split(","); 248 | // [PUB MR(5s)][ 0][ SUB MR(5s)][ 0][ CPU][0] 249 | AaAnsi ansi = AaAnsi.n().a(pieces[0]).a('=').fg(Elem.KEY); 250 | if (pub[0].endsWith(" 0")) ansi.faintOn(); 251 | ansi.fg(Elem.KEY).a(pub[0]).a('↑').reset().a(',').a(pub[1]).a('=').fg(Elem.STRING); 252 | if (sub[0].endsWith(" 0")) ansi.faintOn(); 253 | ansi.fg(Elem.STRING).a(sub[0]).a('↓').reset().a(',').a(sub[1]).a('='); 254 | if (pieces[3].equals("0")) ansi.fg(Elem.MSG_BREAK).a(pieces[3]); 255 | else { 256 | ansi.fg(Elem.FLOAT); 257 | if (pieces[3].length() == 1) ansi.faintOn(); 258 | ansi.a(pieces[0]); 259 | } 260 | wrapPrintln(ansi.reset().toString()); 261 | } catch (RuntimeException e) { // oh well! 262 | logger.info("Had an issue trying to pretty-print the message rates",e); 263 | wrapPrintln(input); 264 | } 265 | } else if (input.startsWith("CPU usage")) { // detected shutdown of SdkPerf 266 | pool.schedule(new Runnable() { 267 | @Override 268 | public void run() { 269 | shutdown = true; 270 | } 271 | }, 500, TimeUnit.MILLISECONDS); 272 | } else if (input.startsWith("CLASSPATH: ")) { 273 | // skip it... too long! 274 | } else { // outside message and not a rate, so like a log or something 275 | wrapPrintln(input); 276 | // wrapPrintln(AaAnsi.n().aStyledString(input).toString()); 277 | } 278 | } 279 | } 280 | if (input == "abc") break; 281 | } 282 | } catch (RuntimeException e) { 283 | System.out.println(e); 284 | e.printStackTrace(); 285 | Runtime.getRuntime().halt(1); 286 | } finally { 287 | System.out.println("Goodbye! 👋🏼"); 288 | AnsiConsole.systemUninstall(); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/test/java/com/solace/labs/aaron/ReportingCharsetDecoder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2000-2001 Sun Microsystems, Inc. All Rights Reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Sun designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Sun in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, 22 | * CA 95054 USA or visit www.sun.com if you need additional information or 23 | * have any questions. 24 | */ 25 | 26 | /* Taken from Oracle Java code, package sun.nio.cs.UTF 27 | * 28 | */ 29 | 30 | package com.solace.labs.aaron; 31 | 32 | import java.nio.Buffer; 33 | import java.nio.ByteBuffer; 34 | import java.nio.CharBuffer; 35 | import java.nio.charset.Charset; 36 | import java.nio.charset.CharsetDecoder; 37 | import java.nio.charset.CoderResult; 38 | import java.nio.charset.CodingErrorAction; 39 | 40 | public class ReportingCharsetDecoder extends CharsetDecoder { 41 | 42 | // UTF-16 surrogate-character ranges 43 | // 44 | public static final char MIN_HIGH = '\uD800'; 45 | public static final char MAX_HIGH = '\uDBFF'; 46 | public static final char MIN_LOW = '\uDC00'; 47 | public static final char MAX_LOW = '\uDFFF'; 48 | public static final char MIN = MIN_HIGH; 49 | public static final char MAX = MAX_LOW; 50 | 51 | private static final String replacementPrefix = "<>aa#"; 52 | private int replacementIndex = 0; 53 | 54 | // Range of UCS-4 values that need surrogates in UTF-16 55 | // 56 | public static final int UCS4_MIN = 0x10000; 57 | public static final int UCS4_MAX = (1 << 20) + UCS4_MIN - 1; 58 | 59 | /** 60 | * Tells whether or not the given UCS-4 character must be represented as a 61 | * surrogate pair in UTF-16. 62 | */ 63 | public static boolean neededFor(int uc) { 64 | return (uc >= UCS4_MIN) && (uc <= UCS4_MAX); 65 | } 66 | 67 | /** 68 | * Returns the high UTF-16 surrogate for the given UCS-4 character. 69 | */ 70 | public static char surrogateHigh(int uc) { 71 | assert neededFor(uc); 72 | return (char)(0xd800 | (((uc - UCS4_MIN) >> 10) & 0x3ff)); 73 | } 74 | 75 | /** 76 | * Returns the low UTF-16 surrogate for the given UCS-4 character. 77 | */ 78 | public static char surrogateLow(int uc) { 79 | assert neededFor(uc); 80 | return (char)(0xdc00 | ((uc - UCS4_MIN) & 0x3ff)); 81 | } 82 | 83 | 84 | // Constructor! //////////////////////////////////////////////////////// 85 | // averageCharsPerByte - A positive float value indicating the expected number of characters that will be produced for each input byte 86 | // maxCharsPerByte - A positive float value indicating the maximum number of characters that will be produced for each input byte 87 | public ReportingCharsetDecoder(Charset cs) { 88 | super(cs, 1.0f, 7.0f); // used to be 1. For 3, due to replacement string: \uFFFD \uFFFD int 89 | replaceWith(generateReplacement()); 90 | onMalformedInput(CodingErrorAction.REPLACE); 91 | onUnmappableCharacter(CodingErrorAction.REPLACE); 92 | 93 | } 94 | 95 | private String generateReplacement() { 96 | return replacementPrefix+Integer.toString(replacementIndex++,10); 97 | } 98 | 99 | static final void updatePositions(Buffer src, int sp, Buffer dst, int dp) { 100 | src.position(sp - src.arrayOffset()); 101 | dst.position(dp - dst.arrayOffset()); 102 | } 103 | 104 | private static boolean isNotContinuation(int b) { 105 | return (b & 0xc0) != 0x80; 106 | } 107 | 108 | // [C2..DF] [80..BF] 109 | private static boolean isMalformed2(int b1, int b2) { 110 | return (b1 & 0x1e) == 0x0 || (b2 & 0xc0) != 0x80; 111 | } 112 | 113 | // [E0] [A0..BF] [80..BF] 114 | // [E1..EF] [80..BF] [80..BF] 115 | private static boolean isMalformed3(int b1, int b2, int b3) { 116 | return (b1 == (byte)0xe0 && (b2 & 0xe0) == 0x80) || 117 | (b2 & 0xc0) != 0x80 || (b3 & 0xc0) != 0x80; 118 | } 119 | 120 | // [F0] [90..BF] [80..BF] [80..BF] 121 | // [F1..F3] [80..BF] [80..BF] [80..BF] 122 | // [F4] [80..8F] [80..BF] [80..BF] 123 | // only check 80-be range here, the [0xf0,0x80...] and [0xf4,0x90-...] 124 | // will be checked by Surrogate.neededFor(uc) 125 | private static boolean isMalformed4(int b2, int b3, int b4) { 126 | return (b2 & 0xc0) != 0x80 || (b3 & 0xc0) != 0x80 || 127 | (b4 & 0xc0) != 0x80; 128 | } 129 | 130 | private static CoderResult lookupN(ByteBuffer src, int n) { 131 | for (int i=1;i> 2) == -2) { 143 | // 5 bytes 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 144 | if (src.remaining() < 4) 145 | return CoderResult.UNDERFLOW; 146 | return lookupN(src, 5); 147 | } 148 | if ((b1 >> 1) == -2) { 149 | // 6 bytes 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 150 | if (src.remaining() < 5) 151 | return CoderResult.UNDERFLOW; 152 | return lookupN(src, 6); 153 | } 154 | return CoderResult.malformedForLength(1); 155 | case 2: // always 1 156 | return CoderResult.malformedForLength(1); 157 | case 3: 158 | b1 = src.get(); 159 | int b2 = src.get(); // no need to lookup b3 160 | return CoderResult.malformedForLength( 161 | ((b1 == (byte)0xe0 && (b2 & 0xe0) == 0x80) || 162 | isNotContinuation(b2))?1:2); 163 | case 4: // we don't care the speed here 164 | b1 = src.get() & 0xff; 165 | b2 = src.get() & 0xff; 166 | if (b1 > 0xf4 || 167 | (b1 == 0xf0 && (b2 < 0x90 || b2 > 0xbf)) || 168 | (b1 == 0xf4 && (b2 & 0xf0) != 0x80) || 169 | isNotContinuation(b2)) 170 | return CoderResult.malformedForLength(1); 171 | if (isNotContinuation(src.get())) 172 | return CoderResult.malformedForLength(2); 173 | return CoderResult.malformedForLength(3); 174 | default: 175 | assert false; 176 | return null; 177 | } 178 | } 179 | 180 | // called from decode array loop 181 | private static CoderResult malformed(ByteBuffer src, int sp, CharBuffer dst, int dp, int nb) { 182 | src.position(sp - src.arrayOffset()); 183 | CoderResult cr = malformedN(src, nb); 184 | updatePositions(src, sp, dst, dp); 185 | return cr; 186 | } 187 | 188 | // called from decode buffer loop 189 | private static CoderResult malformed(ByteBuffer src, int mark, int nb) { 190 | src.position(mark); 191 | CoderResult cr = malformedN(src, nb); 192 | src.position(mark); 193 | return cr; 194 | } 195 | 196 | private static CoderResult xflow(Buffer src, int sp, int sl, Buffer dst, int dp, int nb) { 197 | updatePositions(src, sp, dst, dp); 198 | return (nb == 0 || sl - sp < nb) 199 | ?CoderResult.UNDERFLOW:CoderResult.OVERFLOW; 200 | } 201 | 202 | private static CoderResult xflow(Buffer src, int mark, int nb) { 203 | CoderResult cr = (nb == 0 || src.remaining() < (nb - 1)) 204 | ?CoderResult.UNDERFLOW:CoderResult.OVERFLOW; 205 | src.position(mark); 206 | return cr; 207 | } 208 | 209 | private CoderResult decodeArrayLoop(ByteBuffer src, CharBuffer dst) { 210 | // This method is optimized for ASCII input. 211 | byte[] sa = src.array(); 212 | int sp = src.arrayOffset() + src.position(); 213 | int sl = src.arrayOffset() + src.limit(); 214 | 215 | char[] da = dst.array(); 216 | int dp = dst.arrayOffset() + dst.position(); 217 | int dl = dst.arrayOffset() + dst.limit(); 218 | int dlASCII = dp + Math.min(sl - sp, dl - dp); 219 | 220 | // ASCII only loop 221 | while (dp < dlASCII && sa[sp] >= 0) 222 | da[dp++] = (char)sa[sp++]; 223 | 224 | while (sp < sl) { 225 | int b1 = sa[sp]; 226 | if (b1 >= 0) { 227 | // 1 byte, 7 bits: 0xxxxxxx 228 | if (dp >= dl) 229 | return xflow(src, sp, sl, dst, dp, 1); 230 | da[dp++] = (char)b1; 231 | sp++; 232 | } else if ((b1 >> 5) == -2) { 233 | // 2 bytes, 11 bits: 110xxxxx 10xxxxxx 234 | if (sl - sp < 2 || dp >= dl) 235 | return xflow(src, sp, sl, dst, dp, 2); 236 | int b2 = sa[sp + 1]; 237 | if (isMalformed2(b1, b2)) 238 | return malformed(src, sp, dst, dp, 2); 239 | da[dp++] = (char) (((b1 << 6) ^ b2) ^ 0x0f80); 240 | sp += 2; 241 | } else if ((b1 >> 4) == -2) { 242 | // 3 bytes, 16 bits: 1110xxxx 10xxxxxx 10xxxxxx 243 | if (sl - sp < 3 || dp >= dl) 244 | return xflow(src, sp, sl, dst, dp, 3); 245 | int b2 = sa[sp + 1]; 246 | int b3 = sa[sp + 2]; 247 | if (isMalformed3(b1, b2, b3)) 248 | return malformed(src, sp, dst, dp, 3); 249 | da[dp++] = (char) (((b1 << 12) ^ (b2 << 6) ^ b3) ^ 0x1f80); 250 | sp += 3; 251 | } else if ((b1 >> 3) == -2) { 252 | // 4 bytes, 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 253 | if (sl - sp < 4 || dl - dp < 2) 254 | return xflow(src, sp, sl, dst, dp, 4); 255 | int b2 = sa[sp + 1]; 256 | int b3 = sa[sp + 2]; 257 | int b4 = sa[sp + 3]; 258 | int uc = ((b1 & 0x07) << 18) | 259 | ((b2 & 0x3f) << 12) | 260 | ((b3 & 0x3f) << 06) | 261 | (b4 & 0x3f); 262 | if (isMalformed4(b2, b3, b4) || 263 | !neededFor(uc)) { 264 | return malformed(src, sp, dst, dp, 4); 265 | } 266 | da[dp++] = surrogateHigh(uc); 267 | da[dp++] = surrogateLow(uc); 268 | sp += 4; 269 | } else 270 | return malformed(src, sp, dst, dp, 1); 271 | } 272 | return xflow(src, sp, sl, dst, dp, 0); 273 | } 274 | 275 | private CoderResult decodeBufferLoop(ByteBuffer src, CharBuffer dst) { 276 | int mark = src.position(); 277 | int limit = src.limit(); 278 | while (mark < limit) { 279 | int b1 = src.get(); 280 | if (b1 >= 0) { 281 | // 1 byte, 7 bits: 0xxxxxxx 282 | if (dst.remaining() < 1) 283 | return xflow(src,mark,1); //overflow 284 | dst.put((char)b1); 285 | mark++; 286 | } else if ((b1 >> 5) == -2) { 287 | // 2 bytes, 11 bits: 110xxxxx 10xxxxxx 288 | if (limit - mark < 2|| dst.remaining() < 1) 289 | return xflow(src, mark, 2); 290 | int b2 = src.get(); 291 | if (isMalformed2(b1,b2)) 292 | return malformed(src, mark, 2); 293 | dst.put((char) (((b1 << 6) ^ b2) ^ 0x0f80)); 294 | mark += 2; 295 | } else if ((b1 >> 4) == -2) { 296 | // 3 bytes, 16 bits: 1110xxxx 10xxxxxx 10xxxxxx 297 | if (limit - mark < 3 || dst.remaining() < 1) 298 | return xflow(src,mark,3); 299 | int b2 = src.get(); 300 | int b3 = src.get(); 301 | if (isMalformed3(b1,b2,b3)) 302 | return malformed(src, mark, 3); 303 | dst.put((char) (((b1 << 12) ^ (b2 << 6) ^ b3) ^ 0x1f80)); 304 | mark += 3; 305 | } else if ((b1 >> 3) == -2) { 306 | // 4 bytes, 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 307 | if (limit - mark < 4 || dst.remaining() < 2) 308 | return xflow(src,mark,4); 309 | int b2 = src.get(); 310 | int b3 = src.get(); 311 | int b4 = src.get(); 312 | int uc = ((b1 & 0x07) << 18) | 313 | ((b2 & 0x3f) << 12) | 314 | ((b3 & 0x3f) << 06) | 315 | (b4 & 0x3f); 316 | if (isMalformed4(b2,b3,b4) || 317 | !neededFor(uc)) { // shortest form check 318 | return malformed(src, mark, 4); 319 | } 320 | dst.put(surrogateHigh(uc)); 321 | dst.put(surrogateLow(uc)); 322 | mark += 4; 323 | } else { 324 | return malformed(src,mark,1); 325 | } 326 | } 327 | return xflow(src,mark,0); 328 | } 329 | 330 | protected CoderResult decodeLoop(ByteBuffer src, CharBuffer dst) { 331 | CoderResult cr; 332 | if (src.hasArray() && dst.hasArray()) { 333 | cr = decodeArrayLoop(src, dst); 334 | } else { 335 | cr = decodeBufferLoop(src, dst); 336 | } 337 | if (cr.isError()) { 338 | replaceWith(generateReplacement()); 339 | } 340 | return cr; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/ProtoBufUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.solace.labs.aaron; 18 | 19 | import static com.solace.labs.aaron.UsefulUtils.indent; 20 | 21 | import java.lang.reflect.InvocationTargetException; 22 | import java.lang.reflect.Method; 23 | import java.net.URL; 24 | import java.util.HashMap; 25 | import java.util.Iterator; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.Map.Entry; 29 | import java.util.Properties; 30 | 31 | import org.apache.logging.log4j.LogManager; 32 | import org.apache.logging.log4j.Logger; 33 | 34 | import com.google.protobuf.AbstractMessage; 35 | import com.google.protobuf.ByteString; 36 | import com.google.protobuf.Descriptors.FieldDescriptor; 37 | import com.google.protobuf.Descriptors.FieldDescriptor.Type; 38 | import com.google.protobuf.MessageOrBuilder; 39 | import com.solace.labs.topic.Sub; 40 | 41 | public class ProtoBufUtils { 42 | 43 | private static final Logger logger = LogManager.getLogger(ProtoBufUtils.class); 44 | 45 | static String pretty(Type dataType) { 46 | switch (dataType) { 47 | case BOOL: return "Bool"; 48 | case BYTES: 49 | case DOUBLE: 50 | case ENUM: 51 | case FIXED32: 52 | case FIXED64: 53 | case FLOAT: 54 | case GROUP: 55 | case INT32: 56 | case INT64: 57 | case MESSAGE: 58 | case SFIXED32: 59 | case SFIXED64: 60 | case SINT32: 61 | case SINT64: 62 | case STRING: 63 | case UINT32: 64 | case UINT64: 65 | default: 66 | return dataType.toString(); 67 | } 68 | } 69 | 70 | static void lookForAllParseFromMethods(String className) { 71 | 72 | try { 73 | Class clazz = Class.forName(className); 74 | // boolean foundMethod = false; 75 | for (Method method : clazz.getMethods()) { // loop through all the methods 76 | if (method.getName().startsWith("parseFrom")) { 77 | System.out.println("Found a parseFrom()"); 78 | 79 | 80 | // if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == byte[].class) { // the one I want! 81 | // } 82 | } 83 | } 84 | 85 | } catch (ClassNotFoundException e) { 86 | // TODO Auto-generated catch block 87 | e.printStackTrace(); 88 | } finally { 89 | 90 | } 91 | 92 | 93 | 94 | 95 | } 96 | 97 | static Map loadProtobufDefinitions() { 98 | Map protobufCallbacks = new HashMap<>(); 99 | 100 | // first thing, test if any protobuf definitions are included... 101 | final String appConfigPath = "protobuf.properties"; 102 | URL url = Thread.currentThread().getContextClassLoader().getResource(appConfigPath); 103 | if (url == null) { 104 | System.out.println(AaAnsi.n().invalid("WARN: could not locate " + appConfigPath + " on classpath").toString()); 105 | logger.warn("Could not find " + appConfigPath + " on classpath. Normally inside ./lib/classes/"); 106 | } else { 107 | try { 108 | // System.out.println("Loading topic subscriptions and protobuf mappings"); 109 | boolean issues = false; 110 | logger.info("Trying to load {} from path: {}", appConfigPath, url); 111 | Properties protobufProps = new Properties(); 112 | protobufProps.load(url.openStream()); 113 | logger.info("This is what I loaded: {}", protobufProps); 114 | for (Entry entry : protobufProps.entrySet()) { 115 | try { 116 | logger.info("Verifying topic subscription {}", entry.getKey().toString()); 117 | Sub sub = new Sub(entry.getKey().toString()); 118 | logger.debug("Attempting to load class {}", entry.getValue().toString()); 119 | Class clazz = Class.forName(entry.getValue().toString()); 120 | boolean foundMethod = false; 121 | for (Method method : clazz.getMethods()) { // loop through all the methods 122 | if (method.getName().startsWith("parseFrom")) { 123 | if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == byte[].class) { // the one I want! 124 | // let's double-check that we can call this method with no issues... 125 | try { 126 | // try to parse an empty payload 127 | method.invoke(null, new byte[] { }); 128 | // com.google.protobuf.GeneratedMessageV3 spanData = (com.google.protobuf.GeneratedMessageV3)method.invoke(null, new byte[] { }); 129 | // looks good!? 130 | foundMethod = true; 131 | protobufCallbacks.put(sub, method); 132 | logger.info("Success! Loaded {} for subscription {}", method.toString(), sub); 133 | } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 134 | logger.warn("Could not instantiate parseFrom(byte[]) method!", e); 135 | issues = true; 136 | } 137 | } 138 | } 139 | } 140 | if (!foundMethod) { // couldn't find the parseFrom(byte[]) method I need 141 | logger.warn("Could not find appropriate parseFrom(byte[]) method in class {}", clazz); 142 | issues = true; 143 | } 144 | } catch (ClassNotFoundException | SecurityException e) { 145 | logger.warn("Could not find class {}", entry.getValue().toString(), e); 146 | issues = true; 147 | } 148 | } 149 | if (issues) { 150 | System.out.println(AaAnsi.n().invalid("WARN: had issues loading Protobuf definitions, check log file").toString()); 151 | } else { 152 | logger.info("Successfully loaded all Protobuf definitions"); 153 | } 154 | } catch (Exception e) { 155 | logger.warn("Caught while trying to load protobuf definitions", e); 156 | System.out.println(AaAnsi.n().invalid("WARN: had issues loading Protobuf definitions, check log file").toString()); 157 | } 158 | } 159 | return protobufCallbacks; 160 | } 161 | 162 | // static ExtensionRegistry registry = ExtensionRegistry.newInstance(); 163 | // 164 | // public static void add(Object o) { 165 | // EgressV1.registerAllExtensions(registry); 166 | // } 167 | 168 | public static AaAnsi decode(MessageOrBuilder msg, int indentFactor) { 169 | Map map = msg.getAllFields(); 170 | return handleMessage(map, indentFactor, indentFactor); 171 | } 172 | 173 | private static AaAnsi handleMessage(Map map, final int indent, final int indentFactor) { 174 | AaAnsi ansi = AaAnsi.n(); 175 | handleMessage(map, ansi, indent, indentFactor); 176 | // if (indentFactor <= 0) return new AaAnsi().fg(Elem.BRACE).a('{') + ansi.toString() + new AaAnsi().fg(Elem.BRACE).a('}').reset(); 177 | if (indentFactor <= 0) return AaAnsi.n().fg(Elem.BRACE).a('{').aa(ansi).a('}');//.reset(); 178 | else return ansi;//.reset().toString(); 179 | } 180 | 181 | private static void handleMessage(Map map, AaAnsi ansi, final int indent, final int indentFactor) { 182 | if (map == null) return; 183 | if (map.isEmpty()) { 184 | ansi.a(indent(indent)).fg(Elem.NULL).a("").reset(); 185 | return; 186 | } 187 | try { 188 | Iterator it = map.keySet().iterator(); 189 | while (it.hasNext()) { 190 | FieldDescriptor fd = it.next(); 191 | Object val = map.get(fd); // can't be null, by protobuf definition 192 | ansi.reset().a(indent(indent)); 193 | // key 194 | // if (indentFactor > 0) ansi.a("Key "); 195 | ansi.fg(Elem.KEY).a("'").a(fd.getName()).a("'"); 196 | if (indentFactor > 0) ansi.a(' '); 197 | ansi.fg(Elem.DATA_TYPE).a('(').a(fd.getType().name()); 198 | if (val instanceof List) ansi.a("[]"); 199 | ansi.a(')').reset(); 200 | if (indentFactor > 0) ansi.a(": "); 201 | // value 202 | // https://protobuf.dev/programming-guides/proto3/#scalar 203 | switch (fd.getType()) { 204 | case DOUBLE: 205 | case FLOAT: 206 | if (val instanceof List) { 207 | List list = (List)val; 208 | ansi.fg(Elem.BRACE).a("[ "); 209 | Iterator listIt = list.iterator(); 210 | if (!listIt.hasNext()) { // empty! 211 | ansi.fg(Elem.NULL).a("").fg(Elem.BRACE).a(" ]"); // empty 212 | } else { 213 | while (listIt.hasNext()) { 214 | // String str = val.toString(); 215 | Object o = listIt.next(); 216 | ansi.fg(Elem.FLOAT).a(o.toString()).reset(); 217 | if (list.iterator().hasNext()) { 218 | ansi.a(','); 219 | if (indentFactor > 0) ansi.a(' '); 220 | } 221 | } 222 | ansi.fg(Elem.BRACE).a(" ]"); 223 | } 224 | } else { 225 | ansi.fg(Elem.FLOAT).a(val.toString()); 226 | } 227 | break; 228 | case INT32: 229 | case INT64: 230 | case SINT32: 231 | case SINT64: 232 | case SFIXED32: 233 | case SFIXED64: 234 | if (val instanceof List) { 235 | List list = (List)val; 236 | ansi.fg(Elem.BRACE).a("[ "); 237 | Iterator listIt = list.iterator(); 238 | if (!listIt.hasNext()) { // empty! 239 | ansi.fg(Elem.NULL).a("").fg(Elem.BRACE).a(" ]"); // empty 240 | } else { 241 | while (listIt.hasNext()) { 242 | Object o = listIt.next(); 243 | ansi.fg(Elem.NUMBER).a(o.toString()); 244 | if (indentFactor > 0) { 245 | String ts = UsefulUtils.guessIfTimestampLong(fd.getName(), (long)val); 246 | if (ts != null) { 247 | ansi.faintOn().a(ts); 248 | } 249 | } 250 | if (list.iterator().hasNext()) { 251 | ansi.reset().a(','); 252 | if (indentFactor > 0) ansi.a(' '); 253 | } 254 | } 255 | ansi.fg(Elem.BRACE).a(" ]"); 256 | } 257 | } else { 258 | ansi.fg(Elem.NUMBER).a(val.toString()); 259 | if (indentFactor > 0) { 260 | String ts = UsefulUtils.guessIfTimestampLong(fd.getName(), (long)val); 261 | if (ts != null) { 262 | ansi.faintOn().a(ts); 263 | } 264 | } 265 | } 266 | break; 267 | case UINT32: 268 | case FIXED32: 269 | // long l = (int)val & 0x00000000ffffffffL; // convert to long 270 | // ansi.fg(Elem.NUMBER).aRaw(val.toString()); 271 | /* 272 | if (val instanceof List) { 273 | List list = (List)val; 274 | ansi.fg(Elem.BRACE).a("[ "); 275 | Iterator listIt = list.iterator(); 276 | if (!listIt.hasNext()) { // empty! 277 | ansi.fg(Elem.NULL).a("").fg(Elem.BRACE).a(" ]"); // empty 278 | } else { 279 | while (listIt.hasNext()) { 280 | Object o = listIt.next(); 281 | ansi.fg(Elem.NUMBER).a(Integer.toUnsignedString((int)o)); 282 | String ts = UsefulUtils.guessIfTimestamp(fd.getName(), (long)val); 283 | if (ts != null) { 284 | ansi.makeFaint().a(ts); 285 | } 286 | if (list.iterator().hasNext()) { 287 | ansi.reset().a(','); 288 | if (indentFactor > 0) ansi.a(' '); 289 | } 290 | } 291 | ansi.fg(Elem.BRACE).a(" ]"); 292 | } 293 | } else { 294 | ansi.fg(Elem.NUMBER).a(Integer.toUnsignedString((int)val)); 295 | String ts = UsefulUtils.guessIfTimestamp(fd.getName(), (long)val); 296 | if (ts != null) { 297 | ansi.makeFaint().a(ts); 298 | } 299 | }*/ 300 | 301 | 302 | 303 | ansi.fg(Elem.NUMBER).a(Integer.toUnsignedString((int)val)); 304 | break; 305 | case UINT64: 306 | case FIXED64: 307 | ansi.fg(Elem.NUMBER).a(Long.toUnsignedString((long)val)); 308 | break; 309 | case BOOL: 310 | ansi.fg(Elem.BOOLEAN).a(val.toString()); 311 | break; 312 | case ENUM: 313 | case STRING: 314 | ansi.fg(Elem.STRING).a('"').a(val.toString()).a('"'); 315 | break; 316 | case BYTES: 317 | if (val instanceof List) { 318 | List list = (List)val; 319 | ansi.fg(Elem.BRACE).a("[ "); 320 | Iterator listIt = list.iterator(); 321 | if (!listIt.hasNext()) { // empty! 322 | ansi.fg(Elem.NULL).a("").fg(Elem.BRACE).a(" ]"); // empty 323 | } else { 324 | while (listIt.hasNext()) { 325 | ByteString bs = (ByteString)listIt.next(); 326 | addByteString(bs, ansi, fd); 327 | if (list.iterator().hasNext()) { 328 | ansi.reset().a(','); 329 | if (indentFactor > 0) ansi.a(' '); 330 | } 331 | } 332 | ansi.fg(Elem.BRACE).a(" ]"); 333 | } 334 | } else { 335 | addByteString((ByteString)val, ansi, fd); 336 | } 337 | break; 338 | case MESSAGE: 339 | // AaAnsi inner = new AaAnsi(); 340 | // if (indentFactor > 0) inner.a('\n'); 341 | if (val instanceof List) { // repeated TODO can anything be repeated?? I think so..! 342 | List list = (List)val; 343 | ansi.fg(Elem.BRACE).a("["); 344 | Iterator listIt = list.iterator(); 345 | if (!listIt.hasNext()) { // empty! 346 | ansi.fg(Elem.NULL).a("").fg(Elem.BRACE).a("]"); // empty 347 | } else { 348 | if (indentFactor > 0) ansi.a('\n'); 349 | while (listIt.hasNext()) { 350 | AbstractMessage obj = (AbstractMessage)listIt.next(); 351 | if (indentFactor > 0) ansi.a(indent(indent + (indent/2))); 352 | ansi.fg(Elem.DATA_TYPE).a("(MESSAGE)").reset(); 353 | if (indentFactor > 0) ansi.a(":\n"); 354 | ansi.aa(handleMessage(obj.getAllFields(), indent + indentFactor, indentFactor)); 355 | if (listIt.hasNext()) { 356 | if (indentFactor > 0) ansi.a('\n'); 357 | else ansi.reset().a(','); 358 | } 359 | } 360 | ansi.fg(Elem.BRACE).a(" ]"); 361 | } 362 | } else { // not repeated, just a message 363 | if (indentFactor > 0) ansi.a('\n'); 364 | ansi.aa(handleMessage(((AbstractMessage)val).getAllFields(), indent+indentFactor, indentFactor)); 365 | } 366 | break; 367 | // case GROUP: 368 | // break; 369 | default: 370 | ansi.fg(Elem.DEFAULT).a(val.toString()).a(" (").a(val.getClass().getName()).a(")");//.a(ansi); 371 | // sb.append(o.toString()).append(" (").append(o.getClass().getName()).append(")\n"); 372 | break; 373 | 374 | } 375 | ansi.reset(); 376 | if (it.hasNext()) { 377 | if (indentFactor > 0) ansi.a("\n"); 378 | else ansi.a(","); 379 | } 380 | // if (fd.getType() != Type.MESSAGE) { 381 | // if (indentFactor > 0) ansi.a('\n'); 382 | // else ansi.a(','); 383 | // } 384 | } 385 | } catch (RuntimeException e) { 386 | System.out.println("This is as far as we got:\n" + ansi.toString()); 387 | throw e; 388 | } 389 | } 390 | 391 | private static void addByteString(ByteString bs, AaAnsi ansi, FieldDescriptor fd) { 392 | if (bs.size() == 4 && fd.getName().toLowerCase().contains("ip")) { // assume IP address 393 | ansi.fg(Elem.BYTES).a(UsefulUtils.bytesToSpacedHexString(bs.toByteArray())); 394 | ansi.faintOn().a(UsefulUtils.ipAddressBytesToIpV4String(bs.toByteArray())); 395 | } else { 396 | ansi.fg(Elem.BYTES).a(UsefulUtils.bytesToSpacedHexString(bs.toByteArray())); 397 | } 398 | } 399 | 400 | 401 | 402 | 403 | 404 | public static void main(String... args) { 405 | 406 | lookForAllParseFromMethods("io.opentelemetry.proto.trace.v1.TracesData"); 407 | 408 | 409 | 410 | 411 | 412 | } 413 | 414 | 415 | 416 | 417 | } 418 | -------------------------------------------------------------------------------- /src/main/java/com/solace/labs/aaron/PayloadSection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Solace Corporation. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package com.solace.labs.aaron; 17 | 18 | import java.io.IOException; 19 | import java.io.StringReader; 20 | import java.nio.ByteBuffer; 21 | import java.nio.charset.StandardCharsets; 22 | import java.util.Arrays; 23 | 24 | import org.apache.logging.log4j.LogManager; 25 | import org.apache.logging.log4j.Logger; 26 | import org.fusesource.jansi.AnsiConsole; 27 | import org.htmlunit.cyberneko.parsers.SAXParser; 28 | import org.htmlunit.cyberneko.xerces.xni.XNIException; 29 | import org.htmlunit.cyberneko.xerces.xni.parser.XMLInputSource; 30 | 31 | import com.solace.labs.aaron.ConfigState.DisplayType; 32 | import com.solace.labs.aaron.MessageHelper.PrettyMsgType; 33 | import com.solace.labs.aaron.utils.DecoderUtils; 34 | import com.solacesystems.jcsmp.impl.sdt.MapImpl; 35 | import com.solacesystems.jcsmp.impl.sdt.MapTLVBuffer; 36 | import com.solacesystems.jcsmp.impl.sdt.StreamImpl; 37 | import com.solacesystems.jcsmp.impl.sdt.StreamTLVBuffer; 38 | 39 | class PayloadSection { // like, the XML payload and the binary payload; but also the user props (map) and user data (binary) could use this 40 | 41 | private static final Logger logger = LogManager.getLogger(PayloadSection.class); 42 | 43 | 44 | final ConfigState config; 45 | int size = 0; 46 | String type = null; // might initialize later if JSON or XML 47 | AaAnsi formatted = AaAnsi.n().fg(Elem.NULL).a(""); // to ensure gets overwritten 48 | int numElements = 0; // primarily for User Properties SDTMap element count 49 | 50 | PayloadSection(ConfigState config) { 51 | this.config = config; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | if (type == null) return formatted.toRawString(); 57 | else return new StringBuilder().append(type).append('\n').append(formatted.toRawString()).toString(); 58 | } 59 | 60 | public String getType() { 61 | return type; 62 | } 63 | 64 | public String getFormattedPayload() { 65 | return formatted.toString(); 66 | } 67 | 68 | public int getSize() { 69 | return size; 70 | } 71 | 72 | public String getSizeString() { 73 | return new StringBuilder().append("(len=").append(size).append(')').toString(); 74 | } 75 | 76 | /** sets `formatted` to be the nice String representation */ 77 | void formatString(final String text, final byte[] bytes, boolean decodedFromBytes) { 78 | formatString(text, bytes, null, decodedFromBytes); 79 | } 80 | 81 | void formatString(final String text, final byte[] bytes, String contentType, boolean decodedFromBytes) { 82 | if (contentType == null) contentType = ""; // empty string, for easier matching later 83 | size = bytes.length; 84 | if (text == null) { // that shouldn't happen? 85 | formatted = AaAnsi.n(); 86 | type = ""; 87 | return; 88 | } else if (text.isEmpty()) { 89 | formatted = AaAnsi.n(); 90 | type = ""; 91 | if (size != 0 && size != 3 && size != 6) { 92 | logger.warn("Empty string passed, but bytes has length " + size + ". This is unexpected."); 93 | } 94 | return; 95 | } 96 | if (config.payloadDisplay == DisplayType.RAW) { // leave completely alone 97 | formatted = AaAnsi.n().a(text); // nice default colour 98 | if (decodedFromBytes) type = config.charset.displayName() + " encoded string"; 99 | } else { 100 | String trimmed = text.trim(); 101 | if ("application/json".equals(contentType)) { // guess it's JSON? 102 | try { 103 | formatted = GsonUtils.parseJsonDunnoWhich(trimmed, config.getFormattingIndent()); 104 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "JSON " + (trimmed.charAt(0) == '{' ? "OBJECT":"ARRAY"); 105 | } catch (IOException e) { 106 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "INVALID JSON payload"; 107 | // formatted = new AaAnsi().setError().a("ERROR: ").a(e.getMessage()).reset().a('\n').a(text).reset().toString(); 108 | formatted = AaAnsi.n().ex(e).a('\n').a(trimmed); 109 | } 110 | } else if (trimmed.startsWith("{") && trimmed.endsWith("}")) { // try JSON object 111 | try { 112 | formatted = GsonUtils.parseJsonObject(trimmed, config.getFormattingIndent()); 113 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "JSON Object"; 114 | } catch (IOException e) { 115 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "INVALID JSON payload"; 116 | // formatted = new AaAnsi().setError().a("ERROR: ").a(e.getMessage()).reset().a('\n').a(text).reset().toString(); 117 | formatted = AaAnsi.n().ex(e).a('\n').a(trimmed); 118 | } 119 | } else if (trimmed.startsWith("[") && trimmed.endsWith("]")) { // try JSON array 120 | try { 121 | formatted = GsonUtils.parseJsonArray(trimmed, config.getFormattingIndent()); 122 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "JSON Array"; 123 | } catch (IOException e) { 124 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "INVALID JSON payload"; 125 | // formatted = new AaAnsi().setError().a("ERROR: ").a(e.getMessage()).reset().a('\n').a(text).reset().toString(); 126 | formatted = AaAnsi.n().ex(e).a('\n').a(trimmed); 127 | } 128 | } else if ((trimmed.startsWith("<") && trimmed.endsWith(">")) || 129 | "application/xml".equals(contentType) || contentType.contains("text/xml") || 130 | "text/html".equals(contentType) || contentType.contains("html")) { // try XML 131 | // I'm looking for the ASCII substitution char, and replacing it with � what is that? FFFD? 132 | // String substitutionReplacedTrimmed = trimmed.replaceAll("\\Q\u001a\\E", "� "); 133 | String substitutionReplacedTrimmed = trimmed.replaceAll("\\Q\u001a\\E", "\ufffd"); 134 | try { 135 | if ("application/xml".equals(contentType) || contentType.contains("text/xml")) throw new SaxParserException(""); // throw to the catch for HTML processing 136 | SaxHandler handler = new SaxHandler(config.getFormattingIndent()); 137 | SaxParser.parseString(substitutionReplacedTrimmed, handler); 138 | formatted = handler.getResult(); // overwrite 139 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "XML document"; 140 | } catch (SaxParserException e) { 141 | // maybe it's HTML? 142 | final StringReader sr = new StringReader(substitutionReplacedTrimmed); 143 | final XMLInputSource htmlInputSource = new XMLInputSource(null, "foo", null, sr, config.charset.name()); 144 | final SAXParser htmlParser = new SAXParser(); 145 | SaxHandler handler = new SaxHandler(config.getFormattingIndent(), false); 146 | htmlParser.setContentHandler(handler); 147 | // this doesn't work that well... first

second

third

become

second

third

148 | // probably works well on modern html that have proper open/close tags 149 | try { 150 | htmlParser.parse(htmlInputSource); 151 | formatted = handler.getResult(); // overwrite 152 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "HTML document"; 153 | } catch (XNIException | IOException e1) { 154 | logger.warn("Couldn't parse xml or html", e); 155 | type = (decodedFromBytes ? config.charset.displayName() + " charset, " : "") + "INVALID XML payload"; 156 | formatted = AaAnsi.n().ex(e).a('\n').a(trimmed); 157 | } 158 | } 159 | } else { // it's neither JSON or XML, but has text content 160 | // type = charset.displayName() + " String"; 161 | if (decodedFromBytes) type = config.charset.displayName() + " encoded string"; 162 | formatted = AaAnsi.n().aStyledString(text).reset(); 163 | // formatted = text; 164 | } 165 | } 166 | boolean malformed = text.contains("\ufffd"); 167 | // System.out.println("MALFORMED: " + malformed); 168 | if (malformed) { 169 | // should be impossible for type == null, but just in case. means that TextMessage, UTF-8, but contains replacement char 170 | type = "non " + (type == null ? StandardCharsets.UTF_8.displayName() + " encoded string" : type); 171 | } 172 | if (config.payloadDisplay != DisplayType.RAW) { 173 | double ratio = (1.0 * formatted.getControlCharsCount() + formatted.getReplacementCharsCount()) / formatted.getTotalCharCount(); 174 | if ((malformed && ratio >= 0.25) || config.payloadDisplay == DisplayType.DUMP) { // 25%, very likely a binary file 175 | formatted = UsefulUtils.printBinaryBytesSdkPerfStyle(bytes, config.getFormattingIndent(), AnsiConsole.getTerminalWidth()); 176 | } else if (malformed || formatted.getControlCharsCount() > 0 || formatted.getReplacementCharsCount() > 0) { // any unusual control chars (not tab, LF, CR, FF, or Esc, or NUL at string end 177 | if (!config.oneLineMode && config.getFormattingIndent() > 0) { // only if not in one-line mode! 178 | formatted.a('\n').aa(UsefulUtils.printBinaryBytesSdkPerfStyle(bytes, config.getFormattingIndent(), AnsiConsole.getTerminalWidth())); 179 | } 180 | } 181 | } 182 | } 183 | 184 | void formatBytes(byte[] bytes, String contentType) { 185 | String parsed = DecoderUtils.decodeToString(config.decoder, bytes); 186 | // boolean malformed = parsed.contains("\ufffd"); 187 | formatString(parsed, bytes, contentType, true); // call the String version 188 | if (!type.startsWith("non") && size > 0) { 189 | if (formatted.getControlCharsCount() > 0 || formatted.getReplacementCharsCount() > 0) { 190 | type = "technically valid " + type; 191 | if (formatted.getReplacementCharsCount() > 0) type += " (contains replacement chars)"; 192 | } 193 | else type = "valid " + type; 194 | } 195 | // if (bytes[0] == 0x1c || bytes[0] == 0x1c || bytes[0] == 0x1c || bytes[0] == 0x1c || bytes[0] == 0x1c || bytes[0] == 0x1c || ) 196 | // formatByteBufferFromWrapMode(ByteBuffer.wrap(bytes)); 197 | } 198 | 199 | /* private boolean checkIfSdtMap(byte[] bytes) { 200 | // check the size matches correctly: 201 | int size = buffer.getInt(); // next 4 bytes 202 | // System.out.println("size bytes ("+size+") and buffer limit ("+buffer.limit()+")!"); 203 | if (buffer.limit() == size) { // looks correct! otherwise maybe just a regular binary msg 204 | byte[] copy = null; 205 | copy = Arrays.copyOfRange(buffer.array(), 0, buffer.limit()); 206 | MapTLVBuffer buf = new MapTLVBuffer(copy); // sneaky hidden but public methods 207 | MapImpl map = new MapImpl(buf); 208 | // String test = SdtUtils.printMap(map, 3).toString(); 209 | formatted = SdtUtils.printMap(map, config.getFormattingIndent()); 210 | type = PrettyMsgType.MAP.toString(); // hack, should be in the msg helper object 211 | return; 212 | } else { 213 | buffer.rewind(); // put back to beginning 214 | } 215 | } 216 | */ 217 | 218 | void formatByteBufferFromWrapMode(ByteBuffer buffer) { 219 | byte[] copy = null; 220 | String tempType = null; 221 | byte first = buffer.get(); // check out the first byte 222 | if (first == 0x1c) { // text message, one byte of size 223 | int size = Byte.toUnsignedInt(buffer.get()); 224 | if (buffer.limit() != size) throw new IllegalArgumentException("size byte ("+size+") did not match buffer limit ("+buffer.limit()+")!"); 225 | buffer.limit(buffer.limit()-1); // text messages are null terminated, so don't read the trailing null 226 | tempType = PrettyMsgType.TEXT.toString(); 227 | // System.out.println("0x1c SDT TextMessage detected, byte lenght: " + size); 228 | } else if (first == 0x1d) { // text message, 2 bytes of size 229 | int size = Short.toUnsignedInt(buffer.getShort()); 230 | if (buffer.limit() != size) throw new IllegalArgumentException("size bytes ("+size+") did not match buffer limit ("+buffer.limit()+")!"); 231 | // System.out.println("0x1d SDT TextMessage detected, byte lenght: " + size); 232 | buffer.limit(buffer.limit()-1); // text messages are null terminated, so don't read the trailing null 233 | tempType = PrettyMsgType.TEXT.toString(); 234 | } else if (first == 0x1e) { // text message, 3 bytes of size 235 | int size = Byte.toUnsignedInt(buffer.get()) << 16; 236 | size |= buffer.getShort(); 237 | if (buffer.limit() != size) throw new IllegalArgumentException("size bytes ("+size+") did not match buffer limit ("+buffer.limit()+")!"); 238 | buffer.limit(buffer.limit()-1); // text messages are null terminated, so don't read the trailing null 239 | tempType = PrettyMsgType.TEXT.toString(); 240 | // System.out.println("0x1e SDT TextMessage detected, byte lenght: " + size); 241 | } else if (first == 0x1f) { // text message, 4 bytes of size 242 | int size = buffer.getInt(); 243 | if (buffer.limit() != size) throw new IllegalArgumentException("size bytes ("+size+") did not match buffer limit ("+buffer.limit()+")!"); 244 | buffer.limit(buffer.limit()-1); // text messages are null terminated, so don't read the trailing null 245 | // System.out.println("0x1f SDT TextMessage detected, byte lenght: " + size); 246 | tempType = PrettyMsgType.TEXT.toString(); 247 | } else if (first == 0x2b) { // SDT Map? 248 | // check the size matches correctly: 249 | int size = buffer.getInt(); // next 4 bytes 250 | // System.out.println("size bytes ("+size+") and buffer limit ("+buffer.limit()+")!"); 251 | if (buffer.limit() == size) { // looks correct! otherwise maybe just a regular binary msg 252 | // copy = Arrays.copyOfRange(buffer.array(), 0, buffer.limit()); // need to do this to copy out the whole bytes! 253 | // // this should work too, ah but might not work if we've advanced some position in the buffer 254 | // nope, should be fine: https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html#get-byte:A- 255 | copy = new byte[buffer.limit()]; 256 | buffer.get(copy); 257 | MapTLVBuffer buf = new MapTLVBuffer(copy); // sneaky hidden but public methods 258 | MapImpl map = new MapImpl(buf); 259 | // String test = SdtUtils.printMap(map, 3).toString(); 260 | formatted = SdtUtils.printMap(map, config.getFormattingIndent()); 261 | type = PrettyMsgType.MAP.toString(); // hack, should be in the msg helper object 262 | return; 263 | } else { 264 | buffer.rewind(); // put back to beginning 265 | } 266 | } else if (first == 0x2f) { // SDT Stream? 267 | // check the size matches correctly: 268 | int size = buffer.getInt(); // next 4 bytes 269 | // System.out.println("size bytes ("+size+") and buffer limit ("+buffer.limit()+")!"); 270 | if (buffer.limit() == size) { // looks correct! otherwise maybe just a regular binary msg 271 | copy = Arrays.copyOfRange(buffer.array(), 0, buffer.limit()); 272 | StreamTLVBuffer buf = new StreamTLVBuffer(copy); // sneaky hidden but public methods 273 | StreamImpl map = new StreamImpl(buf); 274 | formatted = SdtUtils.printStream(map, config.getFormattingIndent()); 275 | type = PrettyMsgType.STREAM.toString(); 276 | return; 277 | } else { 278 | buffer.rewind(); // put back to beginning 279 | } 280 | } else { 281 | buffer.rewind(); // put back to beginning 282 | // tempType = PrettyMsgType.BYTES.toString(); 283 | } 284 | // if we're here, then either a TextMessage that we've shifted around or binary message ready to be read 285 | // pos = buffer.position(); 286 | // limit = buffer.limit(); 287 | // capacity = buffer.capacity(); 288 | // System.out.printf("pos: %d, lim: %d, cap: %d%n", pos, limit, capacity); 289 | copy = Arrays.copyOfRange(buffer.array(), 0, buffer.limit()); 290 | String parsed = DecoderUtils.decodeToString(config.decoder, copy); 291 | // boolean malformed = parsed.contains("\ufffd"); 292 | formatString(parsed, copy, true); // call the String version 293 | if (tempType != null) type = tempType + ", " + type; 294 | // if (malformed) { 295 | // type = "Non " + type; 296 | // if (INDENT > 0) { 297 | // formatted.a('\n').a(UsefulUtils.printBinaryBytesSdkPerfStyle(Arrays.copyOfRange(buffer.array(), 0, buffer.limit()), INDENT, currentScreenWidth)); 298 | // } 299 | // } 300 | } 301 | 302 | } 303 | --------------------------------------------------------------------------------