//CipherStreamJava/src/main/java/com/mattrixwv/cipherstream/monosubstitution/Substitution.java
//Mattrixwv
// Created: 02-22-22
//Modified: 08-11-24
/*
Copyright (C) 2024 Mattrixwv
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see
* The substitution cipher replaces each letter or digit in the input string * with a corresponding character from a key. The key must contain all letters * of the alphabet and optionally digits, without duplicates. *
*/ public class Substitution{ private static final Logger logger = LoggerFactory.getLogger(Substitution.class); //?Fields /** The string that needs encoded/decoded */ protected String inputString; /** The encoded/decoded string */ protected String outputString; /** The keyword used to encode/decode the input */ protected String keyword; //?Getters /** Persist capitals in the output string */ protected boolean preserveCapitals; /** Persist whitespace in the output string */ protected boolean preserveWhitespace; /** Persist symbols in the output string */ protected boolean preserveSymbols; /** * Ensures all key constraints are followed. * * @param key the key to be used for encoding/decoding * @throws InvalidKeywordException if the key is null, contains duplicates, or has invalid length */ protected void setKeyword(String key) throws InvalidKeywordException{ if(key == null){ throw new InvalidKeywordException("Key cannot be null"); } logger.debug("Original key '{}'", key); //Transform all letters to uppercase logger.debug("Removing case"); key = key.toUpperCase(); //Make sure the key contains no duplicate mappings logger.debug("Ensuring there are no duplicate mappings"); StringBuilder uniqueKey = new StringBuilder(); key.chars().distinct().forEach(c -> uniqueKey.append((char)c)); if(!key.equals(uniqueKey.toString())){ throw new InvalidKeywordException("The key cannot contain duplicate mappings"); } //Make sure the key is a valid length if(key.length() == 26){ logger.debug("Ensuring there are only letters in the key"); //Make sure the key contains all valid characters String tempKey = key.replaceAll("[^A-Z]", ""); if(!tempKey.equals(key)){ throw new InvalidKeywordException("The key must contain all letters"); } } else if(key.length() == 36){ logger.debug("Ensuring there are only alpha-numeric characters in the key"); //Make sure the key contains all valid characters String tempKey = key.replaceAll("[^A-Z0-9]", ""); if(!tempKey.equals(key)){ throw new InvalidKeywordException("The key must contain all letters and can contain all numbers"); } } else{ throw new InvalidKeywordException("The key must contain all letters and can contain all numbers"); } //Save the key logger.debug("Cleaned key '{}'", key); this.keyword = key; } /** * Ensures all input constraints are followed. * * @param inputString the string to be encoded/decoded * @throws InvalidInputException if the input string is null or blank */ protected void setInputString(String inputString) throws InvalidInputException{ if(inputString == null){ throw new InvalidInputException("Input cannot be null"); } logger.debug("Original input string '{}'", inputString); //Remove any data that should not be preserved if(!preserveCapitals){ logger.debug("Removing case"); inputString = inputString.toUpperCase(); } if(!preserveWhitespace){ logger.debug("Removing whitespace"); inputString = inputString.replaceAll("\\s", ""); } if(!preserveSymbols){ logger.debug("Removing symbols"); inputString = inputString.replaceAll("[^a-zA-Z\\s]", ""); } //Save the inputString logger.debug("Cleaned input string '{}'", inputString); this.inputString = inputString; //Make sure there is still input if(this.inputString.isBlank()){ throw new InvalidInputException("Input must contain at least 1 letter"); } } /** * Encodes the inputString using the provided key and stores the result in outputString. */ protected void encode(){ logger.debug("Encoding"); StringBuilder output = new StringBuilder(); //Step through every character in the inputString and convert it for(char ch : inputString.toCharArray()){ logger.debug("Working character {}", ch); if(Character.isUpperCase(ch)){ logger.debug("Encoding uppercase"); output.append(Character.toUpperCase(keyword.charAt(ch - 'A'))); } else if(Character.isLowerCase(ch)){ logger.debug("Encoding lowercase"); output.append(Character.toLowerCase(keyword.charAt(ch - 'a'))); } else if(Character.isDigit(ch) && (keyword.length() == 36)){ logger.debug("Encoding digit"); output.append(keyword.charAt('Z' - 'A' + Integer.valueOf(Character.toString(ch)) + 1)); } else{ logger.debug("Passing symbol through"); output.append(ch); } } //Save the output this.outputString = output.toString(); logger.debug("Encoded message '{}'", outputString); } /** * Decodes the inputString using the provided key and stores the result in outputString. */ protected void decode(){ logger.debug("Decoding"); StringBuilder output = new StringBuilder(); //Step through every character in the inputString and convert it for(char ch : inputString.toCharArray()){ logger.debug("Working character {}", ch); if(Character.isUpperCase(ch)){ logger.debug("Encoding uppercase"); output.append((char)('A' + keyword.indexOf(Character.toUpperCase(ch)))); } else if(Character.isLowerCase(ch)){ logger.debug("Encoding lowercase"); output.append((char)('a' + keyword.indexOf(Character.toUpperCase(ch)))); } else if(Character.isDigit(ch) && (keyword.length() == 36)){ logger.debug("Encoding digit"); output.append((char)('0' + (keyword.indexOf(Character.toUpperCase(ch)) - 26))); } else{ logger.debug("Passing symbol through"); output.append(ch); } } //Save the output this.outputString = output.toString(); logger.debug("Decoded message '{}'", outputString); } //?Constructors /** * Constructs a new {@code Substitution} instance with default settings. */ public Substitution(){ preserveCapitals = false; preserveWhitespace = false; preserveSymbols = false; reset(); } /** * Constructs a new {@code Substitution} instance with specified settings. * * @param preserveCapitals whether to preserve capitalization in the output * @param preserveWhitespace whether to preserve whitespace in the output * @param preserveSymbols whether to preserve symbols in the output */ public Substitution(boolean preserveCapitals, boolean preserveWhitespace, boolean preserveSymbols){ this.preserveCapitals = preserveCapitals; this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; reset(); } /** * Encodes the inputString using the provided key. * * @param key the key to use for encoding * @param inputString the string to be encoded * @return the encoded string * @throws InvalidKeywordException if the key is invalid * @throws InvalidInputException if the input string is invalid */ public String encode(String key, String inputString) throws InvalidKeywordException, InvalidInputException{ setKeyword(key); setInputString(inputString); encode(); return outputString; } /** * Decodes the inputString using the provided key. * * @param key the key to use for decoding * @param inputString the string to be decoded * @return the decoded string * @throws InvalidKeywordException if the key is invalid * @throws InvalidInputException if the input string is invalid */ public String decode(String key, String inputString) throws InvalidKeywordException, InvalidInputException{ setKeyword(key); setInputString(inputString); decode(); return outputString; } //?Getters /** * Gets the current input string. * * @return the input string */ public String getInputString(){ return inputString; } /** * Gets the current output string. * * @return the output string */ public String getOutputString(){ return outputString; } /** * Gets the current keyword. * * @return the keyword */ public String getKeyword(){ return keyword; } /** * Resets all fields to their default values. */ public void reset(){ logger.debug("Resetting fields"); inputString = ""; outputString = ""; keyword = ""; } }