//MattrixwvWebsite/src/main/java/com/mattrixwv/CipherStreamJava/polySubstitution/Columnar.java //Mattrixwv // Created: 01-16-22 //Modified: 05-04-23 package com.mattrixwv.cipherstream.polysubstitution; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mattrixwv.cipherstream.exceptions.InvalidCharacterException; import com.mattrixwv.cipherstream.exceptions.InvalidInputException; import com.mattrixwv.cipherstream.exceptions.InvalidKeywordException; public class Columnar{ private static final Logger logger = LoggerFactory.getLogger(Columnar.class); //Fields protected String inputString; //The message that needs to be encoded/decoded protected String outputString; //The encoded/decoded message protected String keyword; //The keyword used to create the grid protected char characterToAdd; //The character that is added to the end of a string to bring it to the correct length protected int charsAdded; //The number of characters that were added to the end of the message protected ArrayList> grid; //The grid used to encode/decode the message //Settings protected boolean preserveCapitals; //Persist capitals in the output string protected boolean preserveWhitespace; //Persist whitespace in the output string protected boolean preserveSymbols; //Persist symbols in the output string protected boolean removePadding; //Remove the padding letters added to the cipher //Strip the inputString of all non-letter characters and change them to capitals protected String getCleanInputString(){ logger.debug("Cleaning input string"); return inputString.toUpperCase().replaceAll("[^A-Z]", ""); } //Create the grid from the keyword protected void createGridEncode(){ logger.debug("Creating grid for encoding"); //Add the keyword to the first row in the array grid = new ArrayList<>(); grid.add(new ArrayList<>(Arrays.asList(keyword.chars().mapToObj(c -> (char)c).toArray(Character[]::new)))); //Create the input that will be used for encoding String gridInputString = getCleanInputString(); //Create arrays from the inputString of keyword.size length and add them each as new rows to the grid //?gridRow could be changed to grid.size(), but would it be worth the extra confusion? Probably not unless I really want to min/max for(int gridRow = 1;(keyword.length() * gridRow) <= gridInputString.length();++gridRow){ grid.add(new ArrayList<>(Arrays.asList( gridInputString.substring(keyword.length() * (gridRow - 1), (keyword.length() * gridRow)) .chars().mapToObj(c->(char)c).toArray(Character[]::new) ))); } } protected void createGridDecode(){ logger.debug("Creating grid for decoding"); //Add the keyword to the first row in the array grid = new ArrayList<>(); StringBuilder orderedKeyword = new StringBuilder(); for(int cnt : getKeywordAlphaLocations()){ orderedKeyword.append(keyword.charAt(cnt)); } grid.add(new ArrayList<>(Arrays.asList(orderedKeyword.chars().mapToObj(c -> (char)c).toArray(Character[]::new)))); //Create the input that will be used for encoding String gridInputString = getCleanInputString(); //Make sure the grid has the appropritate number of rows int numRows = inputString.length() / keyword.length(); while(grid.size() <= numRows){ grid.add(new ArrayList<>()); } //Add each character to the grid int rowCnt = 1; for(char ch : gridInputString.toCharArray()){ grid.get(rowCnt++).add(ch); if(rowCnt > numRows){ rowCnt = 1; } } } //Strips invalid characters from the string that needs encoded/decoded protected void setInputStringEncode(String inputString) throws InvalidInputException{ logger.debug("Setting input string for encoding"); //Ensure the input isn't null if(inputString == null){ throw new InvalidInputException("Input must not be null"); } logger.debug("Original input string '{}'", inputString); //Apply removal options 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]", ""); } //Make sure the input is the correct length this.inputString = inputString; StringBuilder inputStringBuilder = new StringBuilder(); inputStringBuilder.append(inputString); int cleanLength = getCleanInputString().length(); int charsToAdd = (cleanLength % keyword.length()); if(charsToAdd != 0){ charsToAdd = keyword.length() - charsToAdd; logger.debug("Appending {} characters", charsToAdd); } for(int cnt = 0;cnt < charsToAdd;++cnt){ inputStringBuilder.append(characterToAdd); } charsAdded = charsToAdd; inputString = inputStringBuilder.toString(); //Save the string logger.debug("Cleaned input string '{}'", inputString); this.inputString = inputString; //Ensure the string isn't blank if(this.inputString.isBlank() || getCleanInputString().isBlank()){ throw new InvalidInputException("Input cannot be blank"); } } protected void setInputStringDecode(String inputString) throws InvalidInputException{ logger.debug("Setting input string for decoding"); //Ensure the input isn't null if(inputString == null){ throw new InvalidInputException("Input must not be null"); } logger.debug("Original input string '{}'", inputString); //Apply removal options 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]", ""); } //Make sure the input is the correct length charsAdded = 0; this.inputString = inputString; //Figure out how many rows there will be int numRows = getCleanInputString().length() / keyword.length(); //Get the number of characters the input is over the limit int numCharsOver = getCleanInputString().length() % keyword.length(); if(numCharsOver > 0){ //Get the first n of the characters in the keyword and their encoded column locations ArrayList originalLocations = getKeywordOriginalLocations(); ArrayList longColumns = new ArrayList<>(); for(int cnt = 0;cnt < numCharsOver;++cnt){ longColumns.add(originalLocations.get(cnt)); } //Add letters where they need to be added to fill out the grid StringBuilder input = new StringBuilder(); int col = 0; int row = 0; for(char ch : inputString.toCharArray()){ if(!Character.isAlphabetic(ch)){ input.append(ch); continue; } if(row < numRows){ input.append(ch); ++row; } else{ if(longColumns.contains(col)){ input.append(ch); row = 0; } else{ input.append(characterToAdd); ++charsAdded; input.append(ch); row = 1; } ++col; } } if(!longColumns.contains(col)){ input.append(characterToAdd); ++charsAdded; } inputString = input.toString(); } //Save the input logger.debug("Cleaned input string '{}'", inputString); this.inputString = inputString; //Ensure the string isn't blank if(this.inputString.isBlank() || getCleanInputString().isBlank()){ throw new InvalidInputException("Input cannot be blank"); } } //Creates the output string from the grid protected void createOutputStringFromColumns(){ logger.debug("Creating output string for encoding"); //Get the current rows of any characters that you added logger.debug("Getting added characters"); ArrayList colsAddedTo = new ArrayList<>(); if(removePadding){ ArrayList cols = getKeywordOriginalLocations(); Collections.reverse(cols); for(int cnt = 0;cnt < charsAdded;++cnt){ colsAddedTo.add(cols.get(cnt)); } } else{ charsAdded = 0; } //Turn the grid into a string logger.debug("Turning grid into string"); StringBuilder gridOutput = new StringBuilder(); for(int col = 0;col < grid.get(0).size();++col){ for(int row = 1;row < grid.size();++row){ gridOutput.append(grid.get(row).get(col)); } if(colsAddedTo.contains(col)){ gridOutput.deleteCharAt(gridOutput.length() - 1); } } //Preserve any remaining symbols in the string logger.debug("Formatting output string"); StringBuilder output = new StringBuilder(); for(int outputLoc = 0, inputLoc = 0;inputLoc < (inputString.length() - charsAdded);){ char inputChar = inputString.charAt(inputLoc++); if(Character.isUpperCase(inputChar)){ output.append(Character.toUpperCase(gridOutput.charAt(outputLoc++))); } else if(Character.isLowerCase(inputChar)){ output.append(Character.toLowerCase(gridOutput.charAt(outputLoc++))); } else{ output.append(inputChar); } } //Save and return the output outputString = output.toString(); logger.debug("Output string '{}'", outputString); } protected void createOutputStringFromRows(){ logger.debug("Creating output string for decoding"); //Turn the grid into a string logger.debug("Transforming grid to a string"); StringBuilder gridOutput = new StringBuilder(); for(int row = 1;row < grid.size();++row){ for(int col = 0;col < grid.get(row).size();++col){ gridOutput.append(grid.get(row).get(col)); } } //Remove any added characters logger.debug("Removing padding"); if(removePadding){ for(int cnt = 0;cnt < charsAdded;++cnt){ gridOutput.deleteCharAt(gridOutput.length() - 1); } } else{ charsAdded = 0; } ArrayList colsAddedTo = new ArrayList<>(); if(removePadding){ ArrayList cols = getKeywordOriginalLocations(); Collections.reverse(cols); for(int cnt = 0;cnt < charsAdded;++cnt){ colsAddedTo.add(cols.get(cnt)); } } //Preserve any remaining symbols in the string logger.debug("Formatting output string"); StringBuilder output = new StringBuilder(); for(int outputLoc = 0, inputLoc = 0, row = 1, col = 0;inputLoc < inputString.length();){ char inputChar = inputString.charAt(inputLoc++); if(((row + 1) == grid.size()) && (colsAddedTo.contains(col)) && (Character.isAlphabetic(inputChar))){ ++col; row = 1; continue; } logger.debug("Working character {}", gridOutput.charAt(outputLoc)); if(Character.isUpperCase(inputChar)){ logger.debug("Adding upper case"); output.append(Character.toUpperCase(gridOutput.charAt(outputLoc++))); ++row; } else if(Character.isLowerCase(inputChar)){ logger.debug("Adding lower case"); output.append(Character.toLowerCase(gridOutput.charAt(outputLoc++))); ++row; } else{ logger.debug("Adding symbol"); output.append(inputChar); } if(row == grid.size()){ ++col; row = 1; } } //Save and return the output outputString = output.toString(); logger.debug("Decoded output string '{}'", outputString); } //Strips invalid characters from the keyword and creates the grid protected void setKeyword(String keyword) throws InvalidKeywordException{ //Ensure the keyword isn't null if(keyword == null){ throw new InvalidKeywordException("Keyword cannot be null"); } logger.debug("Original keyword {}", keyword); //Strip all non-letter characters and change them to uppercase keyword = keyword.toUpperCase().replaceAll("[^A-Z]", ""); this.keyword = keyword; logger.debug("Cleaned keyword {}", keyword); //Make sure a valid keyword is present if(this.keyword.length() < 2){ throw new InvalidKeywordException("The keyword must contain at least 2 letters"); } } //Set the character that is added to the end of the string protected void setCharacterToAdd(char characterToAdd) throws InvalidCharacterException{ if(!Character.isAlphabetic(characterToAdd)){ throw new InvalidCharacterException("Character to add must be a letter"); } logger.debug("Setting character to add {}", characterToAdd); if(!preserveCapitals){ characterToAdd = Character.toUpperCase(characterToAdd); } this.characterToAdd = characterToAdd; logger.debug("Character to add for padding {}", characterToAdd); } //Returns a list of integers that represents the location of the characters of the keyword in alphabetic order protected ArrayList getKeywordAlphaLocations(){ logger.debug("Creating an array of keyword letter locations"); ArrayList orderedLocations = new ArrayList<>(); //Go through every letter and check it against the keyword for(char ch = 'A';ch <= 'Z';++ch){ for(int cnt = 0;cnt < keyword.length();++cnt){ //If the current letter is the same as the letter we are looking for add the location to the array if(keyword.charAt(cnt) == ch){ orderedLocations.add(cnt); } } } //Return the alphabetic locations logger.debug("Array of keyword letters {}", orderedLocations); return orderedLocations; } protected ArrayList getKeywordOriginalLocations(){ logger.debug("Creating array of original keyword locations"); //Figure out the order the columns are in ArrayList orderedLocations = getKeywordAlphaLocations(); //Figure out what order the columns need rearanged to ArrayList originalOrder = new ArrayList<>(); for(int orgCnt = 0;orgCnt < orderedLocations.size();++orgCnt){ for(int orderedCnt = 0;orderedCnt < orderedLocations.size();++orderedCnt){ if(orderedLocations.get(orderedCnt) == orgCnt){ originalOrder.add(orderedCnt); break; } } } //Returning the locations logger.debug("Array of keyword letters {}", originalOrder); return originalOrder; } //Rearanges the grid based on the list of numbers given protected void rearangeGrid(ArrayList listOrder){ logger.debug("Rearanging grid"); //Create a new grid and make sure it is the same size as the original grid int numCol = grid.get(0).size(); ArrayList> newGrid = new ArrayList<>(grid.size()); for(int cnt = 0;cnt < grid.size();++cnt){ newGrid.add(new ArrayList<>(numCol)); } //Step through the list order, pull out the columns, and add them to the new grid for(int col : listOrder){ for(int row = 0;row < grid.size();++row){ newGrid.get(row).add(grid.get(row).get(col)); } } //Save the new grid logger.debug("New grid {}", newGrid); grid = newGrid; } //Encodes inputString using the Columnar cipher and stores the result in outputString protected void encode(){ logger.debug("Encoding"); //Create the grid createGridEncode(); //Figure out the new column order ArrayList orderedLocations = getKeywordAlphaLocations(); //Rearange the grid to the new order rearangeGrid(orderedLocations); //Create the output createOutputStringFromColumns(); } //Decodes inputString using the Columnar cipher and stores the result in outputString protected void decode(){ logger.debug("Decoding"); //Create the grid createGridDecode(); ArrayList originalOrder = getKeywordOriginalLocations(); //Rearange the grid to the original order rearangeGrid(originalOrder); //Create the output createOutputStringFromRows(); } //Constructors public Columnar() throws InvalidCharacterException{ preserveCapitals = false; preserveWhitespace = false; preserveSymbols = false; removePadding = false; setCharacterToAdd('x'); reset(); } public Columnar(boolean preserveCapitals, boolean preserveWhitespace, boolean preserveSymbols, boolean removePadding) throws InvalidCharacterException{ this.preserveCapitals = preserveCapitals; this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; this.removePadding = removePadding; setCharacterToAdd('x'); reset(); } public Columnar(boolean preserveCapitals, boolean preserveWhitespace, boolean preserveSymbols, boolean removePadding, char characterToAdd) throws InvalidCharacterException{ this.preserveCapitals = preserveCapitals; this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; this.removePadding = removePadding; setCharacterToAdd(characterToAdd); reset(); } //Encodes inputString using keyword and returns the result public String encode(String keyword, String inputString) throws InvalidKeywordException, InvalidInputException{ //Set the parameters reset(); setKeyword(keyword); setInputStringEncode(inputString); //Encode and return the message encode(); return outputString; } //Encodes inputString using keyword and returns the result public String decode(String keyword, String inputString) throws InvalidKeywordException, InvalidInputException{ //Set the parameters reset(); setKeyword(keyword); setInputStringDecode(inputString); //Decode and return the message decode(); return outputString; } //Makes sure all variables are empty public void reset(){ logger.debug("Resetting fields"); inputString = ""; outputString = ""; keyword = ""; grid = new ArrayList<>(); charsAdded = 0; } //Getters public String getInputString(){ return inputString; } public String getOutputString(){ return outputString; } public String getKeyword(){ return keyword; } }