AppMain.java

package edu.jiangxin.apktoolbox.convert.protobuf.supervised;

import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.RTextScrollPane;

import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;

/**
 * Desktop application offering the functionality of the {@link ProtoToJson}-API.
 * <p>
 * The user has the option to select a binary protobuf message which the application will then attempt to decode into
 * readable JSON. The result will then be displayed in an advanced text editor window for further processing.
 * <p>
 * Message decoding requires protobuf descriptors that are put into the {@link #CACHE_DIRECTORY} in advance by the user.
 * Such descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for
 * example:
 * <pre>{@code
 * protoc --descriptor_set_out foo.desc foo.proto
 * }</pre>
 *
 * @author Daniel Tischner {@literal <zabuza.dev@gmail.com>}
 */
public enum AppMain {
	;
	/**
	 * The directory to use for the descriptor cache.
	 */
	@SuppressWarnings({ "WeakerAccess", "CallToSystemGetenv" })
	public static final Path CACHE_DIRECTORY = Path.of(System.getenv("LOCALAPPDATA"), "ProtoToJson", "descriptorCache");
	/**
	 * Preferred width for the GUI.
	 */
	private static final int WIDTH = 700;
	/**
	 * Preferred height for the GUI.
	 */
	private static final int HEIGHT = 700;

	/**
	 * Starts the desktop application.
	 * <p>
	 * Prior to using this, the user has to populate the {@link #CACHE_DIRECTORY} with protobuf descriptor files. Such
	 * descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for
	 * example:
	 * <pre>{@code
	 * protoc --descriptor_set_out foo.desc foo.proto
	 * }</pre>
	 * The user has the option to select a binary protobuf message which the application will then attempt to decode
	 * into readable JSON. The result will then be displayed in an advanced text editor window for further processing.
	 *
	 * @param args Null or empty if the user should be given the option to select a binary protobuf message. Otherwise
	 *             one argument which is the path to the binary protobuf message to convert, the application will then
	 *             directly proceed to decoding this message instead of asking the user for a message first.
	 */
	@SuppressWarnings("OverlyBroadCatchBlock")
	public static void main(final String[] args) {
		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

			// Establish API
			final DescriptorCache cache = AppMain.createCache();
			if (cache.isEmpty()) {
				//noinspection HardcodedLineSeparator
				AppMain.displayShortMessage(
						"The descriptor cache is empty, could not load any descriptor, check the cache directory:\n"
								+ AppMain.CACHE_DIRECTORY.toAbsolutePath(), "Empty descriptor cache",
						MessageType.ERROR);
				return;
			}
			final ProtoToJson protoToJson = ProtoToJson.fromCache(cache);

			// Pick which file to decode
			final Optional<Path> protoFile = AppMain.chooseProtoFile(args);
			if (protoFile.isEmpty()) {
				return;
			}

			// Decode to JSON
			final String protoJson;
			//noinspection NestedTryStatement
			try {
				protoJson = protoToJson.toJson(protoFile.orElseThrow());
			} catch (final NoDescriptorFoundException e) {
				//noinspection HardcodedLineSeparator
				AppMain.displayShortMessage(
						"Unable to find a descriptor matching the given JSON message, check the cache directory:\n"
								+ AppMain.CACHE_DIRECTORY.toAbsolutePath(), "No matching descriptor found",
						MessageType.ERROR);
				return;
			}

			// Display the result
			AppMain.displayJson(protoJson);
		} catch (final Throwable t) {
			final StringWriter sw = new StringWriter();
			final PrintWriter pw = new PrintWriter(sw);
			t.printStackTrace(pw);
			AppMain.displayLongMessage(sw.toString(), "Error", MessageType.ERROR);
		}
	}

	/**
	 * Decides which binary protobuf message file to choose for decoding. Either given by {code args} or by asking the
	 * user to choose a file.
	 *
	 * @param args Null or empty if the user should be given the option to select a binary protobuf message. Otherwise
	 *             one argument which is the path to the binary protobuf message to convert, the method will then
	 *             directly choose this file instead of asking the user for a file first.
	 *
	 * @return The selected binary protobuf message or empty if the user cancelled the process.
	 */
	private static Optional<Path> chooseProtoFile(final String[] args) {
		if (args != null && args.length > 0) {
			return Optional.of(Path.of(args[0]));
		}

		@SuppressWarnings("AccessOfSystemProperties") final File workingDirectory =
				new File(System.getProperty("user.dir"));
		final JFileChooser fileChooser = new JFileChooser(workingDirectory);
		fileChooser.setDialogTitle("Choose a protobuf message file");

		if (fileChooser.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) {
			return Optional.empty();
		}

		return Optional.of(fileChooser.getSelectedFile()
				.toPath());
	}

	private static DescriptorCache createCache() throws IOException {
		Files.createDirectories(AppMain.CACHE_DIRECTORY);
		return DescriptorCache.fromDirectory(AppMain.CACHE_DIRECTORY);
	}

	/**
	 * Displays the given JSON text in an advanced text editor window.
	 *
	 * @param json The JSON to display, not null
	 */
	private static void displayJson(final String json) {
		Objects.requireNonNull(json);

		AppMain.displayLongMessage(json, "Decoded JSON message", MessageType.PLAIN);
	}

	/**
	 * Displays a long text message in an advanced text editor window.
	 *
	 * @param message     The message to display, not null
	 * @param title       The title of the window, not null
	 * @param messageType The type of the message, not null
	 */
	private static void displayLongMessage(final String message, final String title, final MessageType messageType) {
		Objects.requireNonNull(message);
		Objects.requireNonNull(title);
		Objects.requireNonNull(messageType);

		final RSyntaxTextArea textArea = new RSyntaxTextArea(message);
		textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JSON);
		textArea.setCodeFoldingEnabled(true);
		textArea.setEditable(false);

		final RTextScrollPane scrollPane = new RTextScrollPane(textArea);
		scrollPane.setPreferredSize(new Dimension(AppMain.WIDTH, AppMain.HEIGHT));

		//noinspection MagicConstant
		JOptionPane.showMessageDialog(null, scrollPane, title, messageType.getCode());
	}

	/**
	 * Displays a short text message in small popup window.
	 *
	 * @param message     The message to display, not null
	 * @param title       The title of the window, not null
	 * @param messageType The type of the message, not null
	 */
	@SuppressWarnings({ "SameParameterValue", "MagicConstant" })
	private static void displayShortMessage(final String message, final String title, final MessageType messageType) {
		Objects.requireNonNull(message);
		Objects.requireNonNull(title);
		Objects.requireNonNull(messageType);

		JOptionPane.showMessageDialog(null, message, title, messageType.getCode());
	}

	/**
	 * Dialog message types, with codes supported by {@link JOptionPane}.
	 */
	private enum MessageType {
		/**
		 * A plain message, no additional indicators or decoration.
		 */
		PLAIN(JOptionPane.PLAIN_MESSAGE),
		/**
		 * An error message.
		 */
		ERROR(JOptionPane.ERROR_MESSAGE),
		/**
		 * An info message.
		 */
		INFO(JOptionPane.INFORMATION_MESSAGE),
		/**
		 * A warning message.
		 */
		WARNING(JOptionPane.WARNING_MESSAGE),
		/**
		 * A question message.
		 */
		QUESTION(JOptionPane.QUESTION_MESSAGE);

		/**
		 * The corresponding code as supported by {@link JOptionPane}.
		 */
		private final int code;

		/**
		 * Creates a message type.
		 *
		 * @param code The corresponding code as supported by {@link JOptionPane}
		 */
		MessageType(final int code) {
			this.code = code;
		}

		/**
		 * Gets the code of the message type, as supported by {@link JOptionPane}.
		 *
		 * @return The code of the message type
		 */
		@SuppressWarnings("WeakerAccess")
		public int getCode() {
			return code;
		}
	}
}