diff --git a/pom.xml b/pom.xml index d0d9295..50f290f 100644 --- a/pom.xml +++ b/pom.xml @@ -12,9 +12,22 @@ 17 17 UTF-8 + 22 + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + org.junit.jupiter junit-jupiter @@ -25,11 +38,28 @@ + + org.openjfx + javafx-maven-plugin + 0.0.8 + + ua.com.javarush.gnew.Main + + + + + run + + + + + org.apache.maven.plugins maven-surefire-plugin 3.4.0 + org.apache.maven.plugins maven-surefire-report-plugin diff --git a/readme.md b/readme.md index a17a9ac..fa36d74 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,26 @@ +# Caesar Cipher Encryptor/Decryptor + +### What was implemented? +``` +All main tasks from the technical requirements were completed. +The `Encrypt` and `Decrypt` methods function correctly. +``` +### What was not implemented? +``` +The program does not support command-line interaction via scanner (CLI). +Instead, a graphical user interface (GUI) was implemented. +``` +### Project Features +``` +- Multilingual Support: The program supports: +Encryption and decryption of text in English and Ukrainian. +Automatically detects the language of the text and selects the appropriate alphabet. + +- Brute-force Implementation: A brute-force mode is available for decrypting encrypted text. + +- Graphical User Interface (GUI): A user-friendly GUI was implemented +for easier interaction with the program. +``` ## Run the program diff --git a/src/main/java/ua/com/javarush/gnew/Main.java b/src/main/java/ua/com/javarush/gnew/Main.java index 5906828..b8f6187 100644 --- a/src/main/java/ua/com/javarush/gnew/Main.java +++ b/src/main/java/ua/com/javarush/gnew/Main.java @@ -1,32 +1,80 @@ package ua.com.javarush.gnew; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import ua.com.javarush.gnew.crypto.BruteForceResult; import ua.com.javarush.gnew.crypto.Cypher; +import ua.com.javarush.gnew.exeptions.FileNotFoundException; +import ua.com.javarush.gnew.file.ChangeFileName; import ua.com.javarush.gnew.file.FileManager; import ua.com.javarush.gnew.runner.ArgumentsParser; import ua.com.javarush.gnew.runner.Command; import ua.com.javarush.gnew.runner.RunOptions; +import java.io.IOException; import java.nio.file.Path; -public class Main { +public class Main extends Application { + + + @Override + public void start(Stage primaryStage) throws IOException { + Parent root = FXMLLoader.load(getClass().getClassLoader().getResource("UI.fxml")); + primaryStage.setTitle("Caesar Cipher"); + primaryStage.setScene(new Scene(root)); + primaryStage.show(); + } + + public static Cypher cypher = new Cypher(); + public static FileManager fileManager = new FileManager(); + public static ArgumentsParser argumentsParser = new ArgumentsParser(); + public static ChangeFileName changeFileName = new ChangeFileName(); + public static void main(String[] args) { - Cypher cypher = new Cypher(); - FileManager fileManager = new FileManager(); - ArgumentsParser argumentsParser = new ArgumentsParser(); - RunOptions runOptions = argumentsParser.parse(args); + if (args.length == 0) { + launch(); + } try { + RunOptions runOptions = argumentsParser.parse(args); + String content = fileManager.read(runOptions.getFilePath()); + if (runOptions.getCommand() == Command.ENCRYPT) { - String content = fileManager.read(runOptions.getFilePath()); - String encryptedContent = cypher.encrypt(content, runOptions.getKey()); - String fileName = runOptions.getFilePath().getFileName().toString(); - String newFileName = fileName.substring(0, fileName.length() - 4) + " [ENCRYPTED].txt"; - Path newFilePath = runOptions.getFilePath().resolveSibling(newFileName); - fileManager.write(newFilePath, encryptedContent); + Path newFilePath = changeFileName.newFileName(runOptions, "ENCRYPTED"); + fileManager.write(newFilePath, cypher.encrypt(content, runOptions.getKey())); + } else if (runOptions.getCommand() == Command.DECRYPT) { + + Path newFilePath = changeFileName.newFileName(runOptions, "DECRYPTED"); + fileManager.write(newFilePath, cypher.decrypt(content, runOptions.getKey())); + } else if (runOptions.getCommand() == Command.BRUTEFORCE) { + + BruteForceResult result = cypher.bruteforce(content); + Path newFilePath = changeFileName.newFileNameWithKey(runOptions, "BRUTEFORCED", result.getKey()); + fileManager.write(newFilePath, result.getDecryptedContent()); } + } catch (FileNotFoundException e) { + System.out.println("File not found"); } catch (Exception e) { System.out.println(e.getMessage()); } } -} \ No newline at end of file +} + + + + + + + + + + + + + + + diff --git a/src/main/java/ua/com/javarush/gnew/controller/Controller.java b/src/main/java/ua/com/javarush/gnew/controller/Controller.java new file mode 100644 index 0000000..a140cbf --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/controller/Controller.java @@ -0,0 +1,185 @@ +package ua.com.javarush.gnew.controller; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import ua.com.javarush.gnew.crypto.BruteForceResult; +import ua.com.javarush.gnew.crypto.Cypher; +import ua.com.javarush.gnew.file.ChangeFileName; +import ua.com.javarush.gnew.file.FileManager; +import ua.com.javarush.gnew.runner.Command; +import ua.com.javarush.gnew.runner.RunOptions; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.ResourceBundle; + +public class Controller implements Initializable { + + private final FileChooser fileChooser = new FileChooser(); + private final ChangeFileName changeFileName = new ChangeFileName(); + private final FileManager fileManager = new FileManager(); + private final Cypher cypher = new Cypher(); + private String selectedPath; + + @FXML + private Label actualPathLabel; + + @FXML + private AnchorPane pane; + + @FXML + private ComboBox dropdown; + + @FXML + private Button openNewFileDir; + + @FXML + private Button choesePathButton; + + @FXML + private TextField inputKey; + + @FXML + private Button doCypherButton; + + @FXML + private Label newFilePath; + + @FXML + private Label status; + + @FXML + void choeseButtonCliced(ActionEvent event) { + File selectedFile = fileChooser.showOpenDialog(new Stage()); + if (selectedFile != null) { + updatePathLabel(selectedFile.toString()); + updateUIForSelectedCommand(); + openNewFileDir.setDisable(true); + } + } + + private void updatePathLabel(String path) { + actualPathLabel.setText(path); + } + + private void updateUIForSelectedCommand() { + String selectedCommand = dropdown.getValue(); + if ("Encrypt".equals(selectedCommand) || "Decrypt".equals(selectedCommand)) { + inputKey.setDisable(false); + } else if ("Bruteforce".equals(selectedCommand)) { + doCypherButton.setDisable(false); + } + } + + @FXML + void keyInputSelected(KeyEvent event) { + if (inputKey.isFocused()) { + doCypherButton.setDisable(false); + } + } + + @FXML + void doCypher(ActionEvent event) { + if (isFormValid()) { + try { + processCommand(); + resetUIAfterProcessing(); + } catch (IOException e) { + status.setText("Произошла ошибка: " + e.getMessage()); + } + } else { + status.setText("Поля не заполнены"); + } + } + + private boolean isFormValid() { + return !actualPathLabel.getText().isEmpty() && !dropdown.getSelectionModel().isEmpty(); + } + + private void processCommand() throws IOException { + String selectedCommand = dropdown.getValue(); + Integer key = getKey(); + Path filePath = Path.of(actualPathLabel.getText()); + selectedPath = actualPathLabel.getText(); + RunOptions runOptions = new RunOptions(getCommand(selectedCommand), key, filePath); + String content = fileManager.read(runOptions.getFilePath()); + + if ("Encrypt".equals(selectedCommand)) { + writeToFile(changeFileName.newFileName(runOptions, "ENCRYPTED"), cypher.encrypt(content, runOptions.getKey())); + } else if ("Decrypt".equals(selectedCommand)) { + writeToFile(changeFileName.newFileName(runOptions, "DECRYPTED"), cypher.decrypt(content, runOptions.getKey())); + } else if ("Bruteforce".equals(selectedCommand)) { + BruteForceResult result = cypher.bruteforce(content); + writeToFile(changeFileName.newFileNameWithKey(runOptions, "DECRYPTED", result.getKey()), result.getDecryptedContent()); + } + } + + private Command getCommand(String command) { + return switch (command) { + case "Encrypt" -> Command.ENCRYPT; + case "Decrypt" -> Command.DECRYPT; + case "Bruteforce" -> Command.BRUTEFORCE; + default -> throw new IllegalArgumentException("Unknown command: " + command); + }; + } + + private Integer getKey() { + return "Bruteforce".equals(dropdown.getValue()) ? 0 : Integer.parseInt(inputKey.getText()); + } + + private void writeToFile(Path filePath, String content) throws IOException { + fileManager.write(filePath, content); + } + + private void resetUIAfterProcessing() { + inputKey.clear(); + inputKey.setDisable(true); + doCypherButton.setDisable(true); + openNewFileDir.setDisable(false); + actualPathLabel.setText(""); + status.setText("Done"); + } + + @FXML + void methodChoesed (ActionEvent event) { + choesePathButton.setDisable(false); + inputKey.setVisible(!"Bruteforce".equals(dropdown.getSelectionModel().getSelectedItem())); + } + + @FXML + void newFileOpenPath(ActionEvent event) { + File file = new File(selectedPath); + try { + Desktop.getDesktop().open(file.getParentFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @FXML + void paneMouseClicked(MouseEvent event) { + if (inputKey.isFocused()) { + pane.requestFocus(); + } + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + dropdown.getItems().addAll("Encrypt", "Decrypt", "Bruteforce"); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Текстовые файлы", "*.txt")); + fileChooser.setInitialDirectory(new File("C:\\Users\\evgen\\Desktop")); + } +} diff --git a/src/main/java/ua/com/javarush/gnew/crypto/BruteForceResult.java b/src/main/java/ua/com/javarush/gnew/crypto/BruteForceResult.java new file mode 100644 index 0000000..843cd80 --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/crypto/BruteForceResult.java @@ -0,0 +1,19 @@ +package ua.com.javarush.gnew.crypto; + +public class BruteForceResult { + private String key; + private String decryptedContent; + + public BruteForceResult(String decryptedContent, String key) { + this.decryptedContent = decryptedContent; + this.key = key; + } + + public String getDecryptedContent() { + return decryptedContent; + } + + public String getKey() { + return key; + } +} diff --git a/src/main/java/ua/com/javarush/gnew/crypto/Cypher.java b/src/main/java/ua/com/javarush/gnew/crypto/Cypher.java index 2b01247..bbdf48c 100644 --- a/src/main/java/ua/com/javarush/gnew/crypto/Cypher.java +++ b/src/main/java/ua/com/javarush/gnew/crypto/Cypher.java @@ -1,33 +1,48 @@ package ua.com.javarush.gnew.crypto; +import ua.com.javarush.gnew.language.LanguageDetector; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; public class Cypher { - private final ArrayList originalAlphabet = new ArrayList<>(Arrays.asList('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z')); - - - public String encrypt(String input, int key) { - key = Math.negateExact(key); - - ArrayList rotatedAlphabet = new ArrayList<>(originalAlphabet); - Collections.rotate(rotatedAlphabet, key); - char[] charArray = input.toCharArray(); + public String encrypt(String content, int key) { StringBuilder builder = new StringBuilder(); - for (char symbol : charArray) { - builder.append(processSymbol(symbol, rotatedAlphabet)); + key = Math.negateExact(key); + char[] contentCharArray = content.toCharArray(); + for (char currentChar : contentCharArray) { + builder.append(ProcessSymbol.processSymbol(currentChar, key)); } return builder.toString(); } - private Character processSymbol(char symbol, ArrayList rotatedAlphabet) { - if (!originalAlphabet.contains(symbol)) { - return symbol; - } - int index = originalAlphabet.indexOf(symbol); + public String decrypt(String content, int key) { + key = Math.negateExact(key); + return encrypt(content, key); + } - return rotatedAlphabet.get(index); + public BruteForceResult bruteforce(String content){ + StringBuilder builder = new StringBuilder(); + ArrayList wordsForBruteForce = LanguageDetector.detectorBF(content); + int initKey = 1; + + while (true) { + builder.setLength(0); + char[] contentCharArray = content.toCharArray(); + for (char currentChar : contentCharArray) { + builder.append(ProcessSymbol.processSymbol(currentChar, initKey)); + } + String decryptedText = builder.toString(); + for (String s : wordsForBruteForce) { + if (decryptedText.contains(s)) { + String key = String.valueOf(initKey); + return new BruteForceResult(decryptedText, key); + } + } + initKey++; + } } } + + + + diff --git a/src/main/java/ua/com/javarush/gnew/crypto/ProcessSymbol.java b/src/main/java/ua/com/javarush/gnew/crypto/ProcessSymbol.java new file mode 100644 index 0000000..f14e130 --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/crypto/ProcessSymbol.java @@ -0,0 +1,19 @@ +package ua.com.javarush.gnew.crypto; + +import ua.com.javarush.gnew.language.LanguageDetector; + +import java.util.ArrayList; +import java.util.Collections; + +public class ProcessSymbol { + protected static Character processSymbol(char currentChar, int key) { + ArrayList originalAlphabet = LanguageDetector.detector(currentChar); + if (originalAlphabet == null){ + return currentChar; + } + ArrayList rotatedAlphabet = new ArrayList<>(originalAlphabet); + Collections.rotate(rotatedAlphabet, key); + int index = originalAlphabet.indexOf(currentChar); + return rotatedAlphabet.get(index); + } +} diff --git a/src/main/java/ua/com/javarush/gnew/exeptions/FileNotFoundException.java b/src/main/java/ua/com/javarush/gnew/exeptions/FileNotFoundException.java new file mode 100644 index 0000000..6df2cdb --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/exeptions/FileNotFoundException.java @@ -0,0 +1,22 @@ +package ua.com.javarush.gnew.exeptions; + +public class FileNotFoundException extends RuntimeException { + public FileNotFoundException() { + } + + public FileNotFoundException(String message){ + super(message); + } + + public FileNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public FileNotFoundException(Throwable cause) { + super(cause); + } + + public FileNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/ua/com/javarush/gnew/file/ChangeFileName.java b/src/main/java/ua/com/javarush/gnew/file/ChangeFileName.java new file mode 100644 index 0000000..0a0698c --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/file/ChangeFileName.java @@ -0,0 +1,20 @@ +package ua.com.javarush.gnew.file; + +import ua.com.javarush.gnew.runner.RunOptions; + +import java.nio.file.Path; + +public class ChangeFileName { + + public Path newFileName (RunOptions runOptions, String fileNameEnds){ + String fileName = runOptions.getFilePath().getFileName().toString(); + String newFileName = fileName.substring(0, fileName.length() - 4) + " ["+ fileNameEnds +"].txt"; + return runOptions.getFilePath().resolveSibling(newFileName); + } + + public Path newFileNameWithKey (RunOptions runOptions, String fileNameEnds, String key ){ + String fileName = runOptions.getFilePath().getFileName().toString(); + String newFileName = fileName.substring(0, fileName.length() - 4) + " ["+ fileNameEnds +"] Key " + key +".txt"; + return runOptions.getFilePath().resolveSibling(newFileName); + } +} diff --git a/src/main/java/ua/com/javarush/gnew/file/FileManager.java b/src/main/java/ua/com/javarush/gnew/file/FileManager.java index d60744c..5209a88 100644 --- a/src/main/java/ua/com/javarush/gnew/file/FileManager.java +++ b/src/main/java/ua/com/javarush/gnew/file/FileManager.java @@ -1,15 +1,30 @@ package ua.com.javarush.gnew.file; +import ua.com.javarush.gnew.exeptions.FileNotFoundException; + import java.io.IOException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; public class FileManager { - public String read(Path filePath) throws IOException { - return Files.readString(filePath); + public String read(Path filePath) { + try { + return Files.readString(filePath); + } catch (NoSuchFileException e) { + throw new FileNotFoundException(e); + } catch (IOException e){ + throw new RuntimeException(e); + } } - public void write(Path filePath, String content) throws IOException { - Files.writeString(filePath, content); + public void write(Path filePath, String content) { + try { + Files.writeString(filePath, content); + } catch (NoSuchFileException e) { + throw new FileNotFoundException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/src/main/java/ua/com/javarush/gnew/language/Language.java b/src/main/java/ua/com/javarush/gnew/language/Language.java index 067dd2f..90140a3 100644 --- a/src/main/java/ua/com/javarush/gnew/language/Language.java +++ b/src/main/java/ua/com/javarush/gnew/language/Language.java @@ -1,12 +1,29 @@ package ua.com.javarush.gnew.language; import java.util.ArrayList; +import java.util.Arrays; -public abstract class Language { +public class Language { + public static final ArrayList ALPHABET_ENG = new ArrayList<>(Arrays.asList( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z')); - private final ArrayList alphabet; + public static final ArrayList ALPHABET_UA = new ArrayList<>(Arrays.asList( + 'а', 'б', 'в', 'г', 'ґ', 'д', 'е', 'є', 'ж', 'з', 'и', 'і', 'ї', 'й', 'к', 'л', 'м', + 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ь', 'ю', 'я')); + + public static final ArrayList ALPHABET_ENG_UPPER = new ArrayList<>(Arrays.asList( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')); + + public static final ArrayList ALPHABET_UA_UPPER = new ArrayList<>(Arrays.asList( + 'А', 'Б', 'В', 'Г', 'Ґ', 'Д', 'Е', 'Є', 'Ж', 'З', 'И', 'І', 'Ї', 'Й', 'К', 'Л', 'М', + 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ь', 'Ю', 'Я')); + + public static final ArrayList ENG_WORDS_FOR_BRUTEFORCE = new ArrayList<>(Arrays.asList( + "the", "and", "you", "that", "was", "for", "are", "with", "his", "they", "this")); + + public static final ArrayList UKR_WORDS_FOR_BRUTEFORCE = new ArrayList<>(Arrays.asList( + "щоб", "але", "він", "від", "вони", "бути", "його")); - public Language(ArrayList alphabet) { - this.alphabet = alphabet; - } } diff --git a/src/main/java/ua/com/javarush/gnew/language/LanguageDetector.java b/src/main/java/ua/com/javarush/gnew/language/LanguageDetector.java new file mode 100644 index 0000000..43f6e41 --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/language/LanguageDetector.java @@ -0,0 +1,37 @@ +package ua.com.javarush.gnew.language; + +import java.util.ArrayList; + +public class LanguageDetector { + + public static ArrayList detector (char content) { + + if (Character.isLowerCase(content)){ + if (Language.ALPHABET_ENG.contains(content)) { + return Language.ALPHABET_ENG; + } else if (Language.ALPHABET_UA.contains(content)) { + return Language.ALPHABET_UA; + } + } else { + if (Language.ALPHABET_ENG_UPPER.contains(content)) { + return Language.ALPHABET_ENG_UPPER; + } else if (Language.ALPHABET_UA_UPPER.contains(content)) { + return Language.ALPHABET_UA_UPPER; + } + } + return null; + } + + public static ArrayList detectorBF(String content){ + char[] contentCharArray = content.toCharArray(); + for (char c : contentCharArray) { + char currentChar = Character.toLowerCase(c); + if (Language.ALPHABET_ENG.contains(currentChar)) { + return Language.ENG_WORDS_FOR_BRUTEFORCE; + } else if (Language.ALPHABET_UA.contains(currentChar)) { + return Language.UKR_WORDS_FOR_BRUTEFORCE; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/ua/com/javarush/gnew/runner/ArgumentsParser.java b/src/main/java/ua/com/javarush/gnew/runner/ArgumentsParser.java index eb42462..147d983 100644 --- a/src/main/java/ua/com/javarush/gnew/runner/ArgumentsParser.java +++ b/src/main/java/ua/com/javarush/gnew/runner/ArgumentsParser.java @@ -1,62 +1,49 @@ package ua.com.javarush.gnew.runner; - import java.nio.file.Path; +import java.util.HashMap; + public class ArgumentsParser { public RunOptions parse(String[] args) { Command command = null; Integer key = null; Path filePath = null; + HashMap commandHashMap = new HashMap<>(); + commandHashMap.put("-e", Command.ENCRYPT); + commandHashMap.put("-d", Command.DECRYPT); + commandHashMap.put("-bf", Command.BRUTEFORCE); for (int i = 0; i < args.length; i++) { String arg = args[i]; - - switch (arg) { - case "-e": - command = Command.ENCRYPT; - break; - - case "-d": - command = Command.DECRYPT; - break; - - case "-bf": - command = Command.BRUTEFORCE; - break; - case "-k": - if (i + 1 < args.length) { - key = Integer.parseInt(args[++i]); - } else { - throw new IllegalArgumentException("Missing value for key"); - } - break; - - case "-f": - if (i + 1 < args.length) { - filePath = Path.of(args[++i]); - } else { - throw new IllegalArgumentException("Missing value for file"); - } - break; - - default: - throw new IllegalArgumentException("Unknown argument: " + arg); + if (commandHashMap.containsKey(arg)){ + command = commandHashMap.get(arg); + } else if ("-f".equals(arg)){ + if (i + 1 < args.length) { + filePath = Path.of(args[++i]); + } else { + throw new IllegalArgumentException("Missing value for file"); + } + } else if ("-k".equals(arg) && i + 1 + + + + + + + + + + +