View Javadoc
1   package edu.jiangxin.apktoolbox.convert.protobuf.supervised;
2   
3   import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
4   import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
5   import org.fife.ui.rtextarea.RTextScrollPane;
6   
7   import javax.swing.*;
8   import java.awt.*;
9   import java.io.File;
10  import java.io.IOException;
11  import java.io.PrintWriter;
12  import java.io.StringWriter;
13  import java.nio.file.Files;
14  import java.nio.file.Path;
15  import java.util.Objects;
16  import java.util.Optional;
17  
18  /**
19   * Desktop application offering the functionality of the {@link ProtoToJson}-API.
20   * <p>
21   * The user has the option to select a binary protobuf message which the application will then attempt to decode into
22   * readable JSON. The result will then be displayed in an advanced text editor window for further processing.
23   * <p>
24   * Message decoding requires protobuf descriptors that are put into the {@link #CACHE_DIRECTORY} in advance by the user.
25   * Such descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for
26   * example:
27   * <pre>{@code
28   * protoc --descriptor_set_out foo.desc foo.proto
29   * }</pre>
30   *
31   * @author Daniel Tischner {@literal <zabuza.dev@gmail.com>}
32   */
33  public enum AppMain {
34  	;
35  	/**
36  	 * The directory to use for the descriptor cache.
37  	 */
38  	@SuppressWarnings({ "WeakerAccess", "CallToSystemGetenv" })
39  	public static final Path CACHE_DIRECTORY = Path.of(System.getenv("LOCALAPPDATA"), "ProtoToJson", "descriptorCache");
40  	/**
41  	 * Preferred width for the GUI.
42  	 */
43  	private static final int WIDTH = 700;
44  	/**
45  	 * Preferred height for the GUI.
46  	 */
47  	private static final int HEIGHT = 700;
48  
49  	/**
50  	 * Starts the desktop application.
51  	 * <p>
52  	 * Prior to using this, the user has to populate the {@link #CACHE_DIRECTORY} with protobuf descriptor files. Such
53  	 * descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for
54  	 * example:
55  	 * <pre>{@code
56  	 * protoc --descriptor_set_out foo.desc foo.proto
57  	 * }</pre>
58  	 * The user has the option to select a binary protobuf message which the application will then attempt to decode
59  	 * into readable JSON. The result will then be displayed in an advanced text editor window for further processing.
60  	 *
61  	 * @param args Null or empty if the user should be given the option to select a binary protobuf message. Otherwise
62  	 *             one argument which is the path to the binary protobuf message to convert, the application will then
63  	 *             directly proceed to decoding this message instead of asking the user for a message first.
64  	 */
65  	@SuppressWarnings("OverlyBroadCatchBlock")
66  	public static void main(final String[] args) {
67  		try {
68  			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
69  
70  			// Establish API
71  			final DescriptorCache cache = AppMain.createCache();
72  			if (cache.isEmpty()) {
73  				//noinspection HardcodedLineSeparator
74  				AppMain.displayShortMessage(
75  						"The descriptor cache is empty, could not load any descriptor, check the cache directory:\n"
76  								+ AppMain.CACHE_DIRECTORY.toAbsolutePath(), "Empty descriptor cache",
77  						MessageType.ERROR);
78  				return;
79  			}
80  			final ProtoToJson protoToJson = ProtoToJson.fromCache(cache);
81  
82  			// Pick which file to decode
83  			final Optional<Path> protoFile = AppMain.chooseProtoFile(args);
84  			if (protoFile.isEmpty()) {
85  				return;
86  			}
87  
88  			// Decode to JSON
89  			final String protoJson;
90  			//noinspection NestedTryStatement
91  			try {
92  				protoJson = protoToJson.toJson(protoFile.orElseThrow());
93  			} catch (final NoDescriptorFoundException e) {
94  				//noinspection HardcodedLineSeparator
95  				AppMain.displayShortMessage(
96  						"Unable to find a descriptor matching the given JSON message, check the cache directory:\n"
97  								+ AppMain.CACHE_DIRECTORY.toAbsolutePath(), "No matching descriptor found",
98  						MessageType.ERROR);
99  				return;
100 			}
101 
102 			// Display the result
103 			AppMain.displayJson(protoJson);
104 		} catch (final Throwable t) {
105 			final StringWriter sw = new StringWriter();
106 			final PrintWriter pw = new PrintWriter(sw);
107 			t.printStackTrace(pw);
108 			AppMain.displayLongMessage(sw.toString(), "Error", MessageType.ERROR);
109 		}
110 	}
111 
112 	/**
113 	 * Decides which binary protobuf message file to choose for decoding. Either given by {code args} or by asking the
114 	 * user to choose a file.
115 	 *
116 	 * @param args Null or empty if the user should be given the option to select a binary protobuf message. Otherwise
117 	 *             one argument which is the path to the binary protobuf message to convert, the method will then
118 	 *             directly choose this file instead of asking the user for a file first.
119 	 *
120 	 * @return The selected binary protobuf message or empty if the user cancelled the process.
121 	 */
122 	private static Optional<Path> chooseProtoFile(final String[] args) {
123 		if (args != null && args.length > 0) {
124 			return Optional.of(Path.of(args[0]));
125 		}
126 
127 		@SuppressWarnings("AccessOfSystemProperties") final File workingDirectory =
128 				new File(System.getProperty("user.dir"));
129 		final JFileChooser fileChooser = new JFileChooser(workingDirectory);
130 		fileChooser.setDialogTitle("Choose a protobuf message file");
131 
132 		if (fileChooser.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) {
133 			return Optional.empty();
134 		}
135 
136 		return Optional.of(fileChooser.getSelectedFile()
137 				.toPath());
138 	}
139 
140 	private static DescriptorCache createCache() throws IOException {
141 		Files.createDirectories(AppMain.CACHE_DIRECTORY);
142 		return DescriptorCache.fromDirectory(AppMain.CACHE_DIRECTORY);
143 	}
144 
145 	/**
146 	 * Displays the given JSON text in an advanced text editor window.
147 	 *
148 	 * @param json The JSON to display, not null
149 	 */
150 	private static void displayJson(final String json) {
151 		Objects.requireNonNull(json);
152 
153 		AppMain.displayLongMessage(json, "Decoded JSON message", MessageType.PLAIN);
154 	}
155 
156 	/**
157 	 * Displays a long text message in an advanced text editor window.
158 	 *
159 	 * @param message     The message to display, not null
160 	 * @param title       The title of the window, not null
161 	 * @param messageType The type of the message, not null
162 	 */
163 	private static void displayLongMessage(final String message, final String title, final MessageType messageType) {
164 		Objects.requireNonNull(message);
165 		Objects.requireNonNull(title);
166 		Objects.requireNonNull(messageType);
167 
168 		final RSyntaxTextArea textArea = new RSyntaxTextArea(message);
169 		textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JSON);
170 		textArea.setCodeFoldingEnabled(true);
171 		textArea.setEditable(false);
172 
173 		final RTextScrollPane scrollPane = new RTextScrollPane(textArea);
174 		scrollPane.setPreferredSize(new Dimension(AppMain.WIDTH, AppMain.HEIGHT));
175 
176 		//noinspection MagicConstant
177 		JOptionPane.showMessageDialog(null, scrollPane, title, messageType.getCode());
178 	}
179 
180 	/**
181 	 * Displays a short text message in small popup window.
182 	 *
183 	 * @param message     The message to display, not null
184 	 * @param title       The title of the window, not null
185 	 * @param messageType The type of the message, not null
186 	 */
187 	@SuppressWarnings({ "SameParameterValue", "MagicConstant" })
188 	private static void displayShortMessage(final String message, final String title, final MessageType messageType) {
189 		Objects.requireNonNull(message);
190 		Objects.requireNonNull(title);
191 		Objects.requireNonNull(messageType);
192 
193 		JOptionPane.showMessageDialog(null, message, title, messageType.getCode());
194 	}
195 
196 	/**
197 	 * Dialog message types, with codes supported by {@link JOptionPane}.
198 	 */
199 	private enum MessageType {
200 		/**
201 		 * A plain message, no additional indicators or decoration.
202 		 */
203 		PLAIN(JOptionPane.PLAIN_MESSAGE),
204 		/**
205 		 * An error message.
206 		 */
207 		ERROR(JOptionPane.ERROR_MESSAGE),
208 		/**
209 		 * An info message.
210 		 */
211 		INFO(JOptionPane.INFORMATION_MESSAGE),
212 		/**
213 		 * A warning message.
214 		 */
215 		WARNING(JOptionPane.WARNING_MESSAGE),
216 		/**
217 		 * A question message.
218 		 */
219 		QUESTION(JOptionPane.QUESTION_MESSAGE);
220 
221 		/**
222 		 * The corresponding code as supported by {@link JOptionPane}.
223 		 */
224 		private final int code;
225 
226 		/**
227 		 * Creates a message type.
228 		 *
229 		 * @param code The corresponding code as supported by {@link JOptionPane}
230 		 */
231 		MessageType(final int code) {
232 			this.code = code;
233 		}
234 
235 		/**
236 		 * Gets the code of the message type, as supported by {@link JOptionPane}.
237 		 *
238 		 * @return The code of the message type
239 		 */
240 		@SuppressWarnings("WeakerAccess")
241 		public int getCode() {
242 			return code;
243 		}
244 	}
245 }