Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Quality of Life features to assume role policies. Added Find and Replace, Undo & Redo, and line wrapping for the Session Policy text field. #48

Merged
merged 8 commits into from
Jan 10, 2025
481 changes: 304 additions & 177 deletions src/main/java/com/netspi/awssigner/controller/AWSSignerController.java

Large diffs are not rendered by default.

231 changes: 231 additions & 0 deletions src/main/java/com/netspi/awssigner/controller/RegexHighlighter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package com.netspi.awssigner.controller;

import static com.netspi.awssigner.log.LogWriter.logError;

import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
* RegexHighlighter provides search, highlight, and replace capabilities for text in a JTextArea
* using regex patterns. Matches are highlighted to help the user visually locate and manipulate
* specific segments of text.
*
* Usage:
* 1. Instantiate with a JTextArea.
* 2. Call findAndHighlightNext(regex) to initiate searching.
* 3. Use replaceCurrentMatch or replaceAllMatches as needed.
*/
public class RegexHighlighter {

private final JTextArea textArea;
private final Highlighter highlighter;
private final List<int[]> matchPositions;
private int currentMatchIndex = -1;
private String currentRegex;

/**
* Creates a new RegexHighlighter for a given JTextArea.
*
* @param textArea The JTextArea to search and highlight.
*/
public RegexHighlighter(JTextArea textArea) {
this.textArea = textArea;
this.highlighter = textArea.getHighlighter();
this.matchPositions = new ArrayList<>();
this.currentRegex = null;
}

/**
* Finds matches for the given regex and highlights them. If called repeatedly with the same regex,
* it cycles through found matches. If a new regex is provided, it clears previous highlights
* and searches anew.
*
* @param regex The regex pattern to find.
* @throws PatternSyntaxException If the provided regex is invalid.
*/
public void findAndHighlightNext(String regex) throws PatternSyntaxException {
if (currentRegex == null || !currentRegex.equals(regex)) {
// Perform a new search if the regex changes
findAllMatches(regex);
currentMatchIndex = 0; // Start at the first match
} else if (!matchPositions.isEmpty()) {
// Cycle to the next match if there are matches
currentMatchIndex = (currentMatchIndex + 1) % matchPositions.size();
}

if (!matchPositions.isEmpty()) {
updateHighlights();
}
}

/**
* Finds all matches of the given regex in the text and stores their positions.
*
* @param regex The regex pattern to search for.
* @throws PatternSyntaxException if the regex is invalid
*/
private void findAllMatches(String regex) throws PatternSyntaxException {
clearHighlights();
matchPositions.clear();

currentRegex = regex;
String content = textArea.getText();
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);

while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
matchPositions.add(new int[]{start, end});
}

currentMatchIndex = matchPositions.isEmpty() ? -1 : 0;
}

/**
* Replaces the current highlighted match with the given replacement text.
*
* @param replacement The replacement text.
*/
public void replaceCurrentMatch(String replacement) {
if (!hasCurrentMatch()) {
logError("No current match to replace.");
return;
}

try {
String content = textArea.getText();
int[] currentMatch = matchPositions.get(currentMatchIndex);
int start = currentMatch[0];
int end = currentMatch[1];

// Perform the replacement
String updatedContent = content.substring(0, start)
+ Matcher.quoteReplacement(replacement)
+ content.substring(end);
textArea.setText(updatedContent);

// Adjust subsequent matches due to the changed length
int adjustment = replacement.length() - (end - start);
matchPositions.remove(currentMatchIndex);

for (int i = currentMatchIndex; i < matchPositions.size(); i++) {
matchPositions.get(i)[0] += adjustment;
matchPositions.get(i)[1] += adjustment;
}

highlightAllMatches();

if (!matchPositions.isEmpty()) {
currentMatchIndex = currentMatchIndex % matchPositions.size();
highlightCurrentMatch();
} else {
currentMatchIndex = -1;
}
} catch (Exception e) {
logError("Error replacing current match: " + e.getMessage());
}
}

/**
* Replaces all matches of the current regex in the text with the given replacement text.
*
* @param replacement The replacement text.
*/
public void replaceAllMatches(String replacement) {
if (matchPositions.isEmpty() || currentRegex == null) {
logError("No matches to replace.");
return;
}

try {
String content = textArea.getText();
String updatedContent = content.replaceAll(currentRegex, Matcher.quoteReplacement(replacement));
textArea.setText(updatedContent);

clearHighlights();
matchPositions.clear();
} catch (Exception e) {
logError("Error replacing all matches: " + e.getMessage());
}
}

/**
* Highlights all matches in yellow. The current match (if any) is highlighted in orange.
*/
private void highlightAllMatches() {
clearHighlights(); // Clear existing highlights
try {
for (int i = 0; i < matchPositions.size(); i++) {
if (i != currentMatchIndex) {
int[] match = matchPositions.get(i);
highlighter.addHighlight(match[0], match[1], new DefaultHighlighter.DefaultHighlightPainter(Color.YELLOW));
}
}
} catch (BadLocationException e) {
logError("Error highlighting matches: " + e.getMessage());
}
}

/**
* Highlights the current match in orange to distinguish it from other matches.
*/
private void highlightCurrentMatch() {
if (!hasCurrentMatch()) {
return;
}

try {
int[] currentMatch = matchPositions.get(currentMatchIndex);
highlighter.addHighlight(currentMatch[0], currentMatch[1], new DefaultHighlighter.DefaultHighlightPainter(Color.ORANGE));
textArea.setCaretPosition(currentMatch[0]); // Move caret to the current match
} catch (BadLocationException e) {
logError("Error highlighting current match: " + e.getMessage());
}
}

/**
* Updates the highlights to reflect the current set of matches and which one is "current".
*/
private void updateHighlights() {
highlightAllMatches();
highlightCurrentMatch();
}

/**
* Clears all highlights from the text area.
*/
private void clearHighlights() {
try {
highlighter.removeAllHighlights();
} catch (Exception e) {
logError("Error clearing highlights: " + e.getMessage());
}
}

/**
* Checks if there is a current match selected.
*
* @return True if there is a current match, false otherwise.
*/
public boolean hasCurrentMatch() {
return currentMatchIndex >= 0 && currentMatchIndex < matchPositions.size();
}

/**
* Returns the count of all found matches.
*
* @return The number of matches found for the current regex.
*/
public int getMatchCount() {
return matchPositions.size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.netspi.awssigner.controller;

import static com.netspi.awssigner.log.LogWriter.logDebug;

import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.function.Consumer;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.JTextComponent;

/**
* A handler class that listens to changes in a JTextComponent's text and triggers a specified callback
* whenever the text changes. It also updates when the field loses focus, ensuring that the final edited
* value is always captured.
*/
public class TextChangeHandler {

private final JTextComponent textComponent;
private final String propertyLoggingName;
private final Consumer<String> onTextChanged;
private String previousValue;

/**
* Creates a new TextChangeHandler for the specified text component.
*
* @param textComponent The JTextComponent to monitor.
* @param propertyLoggingName A descriptive name of what this text field represents (e.g. "Region", "Service").
* @param onTextChanged The callback to invoke whenever the text changes.
*/
public TextChangeHandler(JTextComponent textComponent, String propertyLoggingName, Consumer<String> onTextChanged) {
this.textComponent = textComponent;
this.propertyLoggingName = propertyLoggingName;
this.onTextChanged = onTextChanged;

// Store the initial value
this.previousValue = textComponent.getText();

// Focus listener: When focus is lost, finalize changes if any.
this.textComponent.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
previousValue = textComponent.getText();
logDebug(propertyLoggingName + " Text Component focus gained. Initial value stored: " + previousValue);
}

@Override
public void focusLost(FocusEvent e) {
logDebug(propertyLoggingName + " Text Component focus lost, checking for changes.");
handleTextChanged();
}
});

// Document listener: Detect inline changes as user types
this.textComponent.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
handleTextChanged();
}

@Override
public void removeUpdate(DocumentEvent e) {
handleTextChanged();
}

@Override
public void changedUpdate(DocumentEvent e) {
// Typically not used for plain-text components, but included for completeness.
}
});
}

/**
* Checks if the text has changed since last recorded and, if so, triggers the onTextChanged callback.
*/
private void handleTextChanged() {
String currentText = textComponent.getText();
if (!previousValue.equals(currentText)) {
previousValue = currentText;
logDebug(propertyLoggingName + " Text Component value changed. New value: " + currentText);
onTextChanged.accept(currentText);
}
}
}

This file was deleted.

Loading