From e728c307b7bbd7fde9f000194e345c4e457730c8 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:22:31 -0400 Subject: [PATCH] GP-7137 - Updated the register picker and the Register Provider to use a natural sort --- .../editor/VarnodeLocationCellEditor.java | 21 +- .../plugin/core/register/RegisterTree.java | 366 +++++++++--------- .../editor/VarnodeLocationCellEditorTest.java | 36 +- .../widgets/DropDownSelectionTextField.java | 19 +- .../docking/widgets/DropDownTextField.java | 3 +- .../datastruct/AlphaNumericComparator.java | 160 ++++++++ .../AlphaNumericComparatorTest.java | 84 ++++ 7 files changed, 488 insertions(+), 201 deletions(-) create mode 100644 Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java create mode 100644 Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java index 61060b5003..5fbb5fd0d1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditor.java @@ -41,6 +41,7 @@ import ghidra.program.model.listing.Program; import ghidra.program.model.listing.ProgramContext; import ghidra.util.Msg; import ghidra.util.Swing; +import ghidra.util.datastruct.AlphaNumericComparator; class VarnodeLocationCellEditor extends AbstractCellEditor implements TableCellEditor, FocusableEditor { @@ -178,7 +179,11 @@ class VarnodeLocationCellEditor extends AbstractCellEditor RegisterDropDownSelectionDataModel registerModel = new RegisterDropDownSelectionDataModel(registers); - registerEntryTextField = new DropDownSelectionTextField<>(registerModel); + + // A smaller min delay to make the drop-down more responsive. The number of registers + // should not be large enough to make the UI sluggish as the user is typing. + int minDelay = 200; + registerEntryTextField = new DropDownSelectionTextField<>(registerModel, minDelay); registerEntryTextField.setBorder(null); // this allows us to show the matching list when there is no text in the editor @@ -230,7 +235,19 @@ class VarnodeLocationCellEditor extends AbstractCellEditor } } - Collections.sort(registers); + Collections.sort(registers, new RegisterComparator()); return registers; } + + private static class RegisterComparator implements Comparator { + + private AlphaNumericComparator alphaNumericComparator = new AlphaNumericComparator(); + + @Override + public int compare(Register r1, Register r2) { + String s1 = r1.getName().toLowerCase(); + String s2 = r2.getName().toLowerCase(); + return alphaNumericComparator.compare(s1, s2); + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterTree.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterTree.java index 94e58dd78f..4a3da8c3e6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterTree.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/register/RegisterTree.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -28,6 +28,7 @@ import docking.widgets.tree.GTreeNode; import generic.theme.GIcon; import ghidra.program.model.lang.Register; import ghidra.program.model.listing.Program; +import ghidra.util.datastruct.AlphaNumericComparator; import resources.Icons; public class RegisterTree extends GTree { @@ -81,9 +82,8 @@ public class RegisterTree extends GTree { list.add(reg); } } - Collections.sort(list); - Register[] registers = new Register[list.size()]; - return list.toArray(registers); + + return list.toArray(Register[]::new); } void setFiltered(boolean b) { @@ -111,13 +111,6 @@ public class RegisterTree extends GTree { Register[] registers = program.getProgramContext().getRegistersWithValues(); root.setRegisters(registers); selectRegister(currentRegister); - -// TODO: old school - delete me? -// runWhenTreeIsDone(new Runnable() { -// public void run() { -// selectRegister(currentRegister); -// } -// }); } } @@ -133,181 +126,196 @@ public class RegisterTree extends GTree { } return null; } -} -abstract class SearchableRegisterTreeNode extends GTreeNode { - public GTreeNode findNode(Register register) { - List allChildren = getChildren(); - for (GTreeNode child : allChildren) { - if (!(child instanceof RegisterTreeNode)) { - continue; - } - RegisterTreeNode node = (RegisterTreeNode) child; - if (node.getRegister().equals(register)) { - return node; - } + private static class RegisterNodeComparator implements Comparator { - GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register); - if (foundNode != null) { - return foundNode; + private AlphaNumericComparator alphaNumericComparator = + new AlphaNumericComparator(); + + @Override + public int compare(GTreeNode n1, GTreeNode n2) { + String s1 = n1.getName().toLowerCase(); + String s2 = n2.getName().toLowerCase(); + return alphaNumericComparator.compare(s1, s2); + } + } + + private static abstract class SearchableRegisterTreeNode extends GTreeNode { + + private RegisterNodeComparator comparator = new RegisterNodeComparator(); + + void sortChildren() { + List children = new ArrayList<>(children()); + Collections.sort(children, comparator); + setChildren(children); + + for (GTreeNode child : children) { + SearchableRegisterTreeNode regNode = (SearchableRegisterTreeNode) child; + regNode.sortChildren(); } } - return null; - } -} - -class RegisterTreeRootNode extends SearchableRegisterTreeNode { - private Register[] lastRegisters; - - @Override - public Icon getIcon(boolean expanded) { - return null; - } - - @Override - public String getName() { - return "Registers"; - } - - @Override - public String getToolTip() { - return null; - } - - @Override - public boolean isLeaf() { - return false; - } - - public void setRegisters(Register[] registers) { - if (registers == lastRegisters) { - return; - } - removeAll(); // remove all current children before repopulating - - lastRegisters = registers; - HashMap groups = - new HashMap<>(); - - List nodes = new ArrayList<>(); - - for (Register register : registers) { - if (register.getBaseRegister() != register && - !register.getParentRegister().isHidden()) { - continue; - } - String groupName = register.getGroup(); - if (groupName != null) { - RegisterTreeGroupNode group = groups.get(groupName); - if (group == null) { - group = new RegisterTreeGroupNode(groupName); - groups.put(groupName, group); - nodes.add(group); + public GTreeNode findNode(Register register) { + List allChildren = getChildren(); + for (GTreeNode child : allChildren) { + if (!(child instanceof RegisterTreeNode)) { + continue; + } + RegisterTreeNode node = (RegisterTreeNode) child; + if (node.getRegister().equals(register)) { + return node; + } + + GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register); + if (foundNode != null) { + return foundNode; } - group.addRegister(register); } - else { - nodes.add(new RegisterTreeNode(register)); + + return null; + } + } + + private static class RegisterTreeNode extends SearchableRegisterTreeNode { + private static Icon REG_ICON = new GIcon("icon.plugin.register"); + private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider"); + private final Register register; + + public RegisterTreeNode(Register register) { + this.register = register; + for (Register childRegister : register.getChildRegisters()) { + addNode(new RegisterTreeNode(childRegister)); } } - Collections.sort(nodes); - setChildren(nodes); - } -} - -class RegisterTreeNode extends SearchableRegisterTreeNode { - private static Icon REG_ICON = new GIcon("icon.plugin.register"); - private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider"); - private final Register register; - - public RegisterTreeNode(Register register) { - this.register = register; - for (Register childRegister : register.getChildRegisters()) { - addNode(new RegisterTreeNode(childRegister)); - } - } - - @Override - public Icon getIcon(boolean expanded) { - return register.hasChildren() ? REG_GROUP_ICON : REG_ICON; - } - - @Override - public String getName() { - return register.getName() + " (" + register.getBitLength() + getAliases() + ")"; - } - - private String getAliases() { - StringBuffer buf = new StringBuffer(); - for (String alias : register.getAliases()) { - buf.append(buf.length() == 0 ? "; " : ", "); - buf.append(alias); - } - return buf.toString(); - } - - @Override - public String getToolTip() { - return register.getDescription(); - } - - @Override - public boolean isLeaf() { - return !register.hasChildren(); - } - - @Override - public int compareTo(GTreeNode other) { - if (!(other instanceof RegisterTreeNode)) { - return 1; - } - return getName().compareTo(other.getName()); - } - - public Register getRegister() { - return register; - } -} - -class RegisterTreeGroupNode extends SearchableRegisterTreeNode { - private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON; - private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON; - private String name; - - public RegisterTreeGroupNode(String name) { - this.name = name; - } - - @Override - public Icon getIcon(boolean expanded) { - return expanded ? OPEN_ICON : CLOSED_ICON; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getToolTip() { - return null; - } - - @Override - public boolean isLeaf() { - return false; - } - - public void addRegister(Register register) { - addNode(new RegisterTreeNode(register)); - } - - @Override - public int compareTo(GTreeNode o) { - if (!(o instanceof RegisterTreeGroupNode)) { - return -1; - } - return name.compareTo(o.getName()); + + @Override + public Icon getIcon(boolean expanded) { + return register.hasChildren() ? REG_GROUP_ICON : REG_ICON; + } + + @Override + public String getName() { + return register.getName() + " (" + register.getBitLength() + getAliases() + + ")"; + } + + private String getAliases() { + StringBuffer buf = new StringBuffer(); + for (String alias : register.getAliases()) { + buf.append(buf.length() == 0 ? "; " : ", "); + buf.append(alias); + } + return buf.toString(); + } + + @Override + public String getToolTip() { + return register.getDescription(); + } + + @Override + public boolean isLeaf() { + return !register.hasChildren(); + } + + Register getRegister() { + return register; + } + } + + private static class RegisterTreeGroupNode extends SearchableRegisterTreeNode { + private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON; + private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON; + private String name; + + public RegisterTreeGroupNode(String name) { + this.name = name; + } + + @Override + public Icon getIcon(boolean expanded) { + return expanded ? OPEN_ICON : CLOSED_ICON; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getToolTip() { + return null; + } + + @Override + public boolean isLeaf() { + return false; + } + + public void addRegister(Register register) { + addNode(new RegisterTreeNode(register)); + } + } + + private static class RegisterTreeRootNode extends SearchableRegisterTreeNode { + private Register[] lastRegisters; + + @Override + public Icon getIcon(boolean expanded) { + return null; + } + + @Override + public String getName() { + return "Registers"; + } + + @Override + public String getToolTip() { + return null; + } + + @Override + public boolean isLeaf() { + return false; + } + + public void setRegisters(Register[] registers) { + if (registers == lastRegisters) { + return; + } + + removeAll(); // remove all current children before re-populating + + lastRegisters = registers; + HashMap groups = new HashMap<>(); + + List nodes = new ArrayList<>(); + + for (Register register : registers) { + if (register.getBaseRegister() != register && + !register.getParentRegister().isHidden()) { + continue; + } + + String groupName = register.getGroup(); + if (groupName != null) { + RegisterTreeGroupNode group = groups.get(groupName); + if (group == null) { + group = new RegisterTreeGroupNode(groupName); + groups.put(groupName, group); + nodes.add(group); + } + group.addRegister(register); + } + else { + nodes.add(new RegisterTreeNode(register)); + } + } + + setChildren(nodes); + sortChildren(); + } + } } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditorTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditorTest.java index 151bafe73b..8dad42425d 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditorTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/VarnodeLocationCellEditorTest.java @@ -15,15 +15,18 @@ */ package ghidra.app.plugin.core.function.editor; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import org.junit.Test; import generic.test.AbstractGenericTest; import ghidra.program.database.ProgramBuilder; +import ghidra.program.database.ProgramDB; import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.ProgramContext; public class VarnodeLocationCellEditorTest extends AbstractGenericTest { private static final int AARCH64_GENERAL_REGISTER_MAX = 30; @@ -31,24 +34,23 @@ public class VarnodeLocationCellEditorTest extends AbstractGenericTest { @Test public void testAarch64XRegistersUseNumericOrder() throws Exception { ProgramBuilder builder = new ProgramBuilder("TestProgram", ProgramBuilder._AARCH64); - try { - List registers = VarnodeLocationCellEditor.getSortedVisibleRegisters( - builder.getProgram().getProgramContext()); - List xRegisters = new ArrayList<>(); - for (Register register : registers) { - if (register.getName().matches("x\\d+")) { - xRegisters.add(register.getName()); - } - } + ProgramDB p = builder.getProgram(); + builder.dispose(); - List expected = new ArrayList<>(); - for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) { - expected.add("x" + i); + ProgramContext context = p.getProgramContext(); + List registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(context); + + List xRegisters = new ArrayList<>(); + for (Register register : registers) { + if (register.getName().matches("x\\d+")) { + xRegisters.add(register.getName()); } - assertEquals(expected, xRegisters); } - finally { - builder.dispose(); + + List expected = new ArrayList<>(); + for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) { + expected.add("x" + i); } + assertEquals(expected, xRegisters); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownSelectionTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownSelectionTextField.java index 54bc13c11f..13879cc4f9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownSelectionTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownSelectionTextField.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -48,6 +48,21 @@ public class DropDownSelectionTextField extends DropDownTextField { super(dataModel); } + /** + * This constructor allows you to control the delay between keystrokes before the items in the + * view get updated. A reasonable delay allows users to type multiple characters before the UI + * updates. For large data, this prevents the UI from feeling sluggish, since the UI is does + * not have to process each keystroke. For small data, the UI can update in real time, which + * feels responsive. The tradeoff is between feeling the most responsive vs adding a small + * delay to prevent apparent freezing. The default value is #DEFAULT_MIN_UPDATE_DELAY. + * + * @param dataModel the data model + * @param updateMinDelay the min update delay + */ + public DropDownSelectionTextField(DropDownTextFieldDataModel dataModel, int updateMinDelay) { + super(dataModel, updateMinDelay); + } + @Override protected ListSelectionModel createListSelectionModel() { DefaultListSelectionModel model = new DefaultListSelectionModel(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java index 0f388823a5..33480ace28 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java @@ -72,6 +72,7 @@ public class DropDownTextField extends JTextField implements GComponent { private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor(); + protected static final int DEFAULT_MIN_UPDATE_DELAY = 350; private static final int DEFAULT_MAX_UPDATE_DELAY = 2000; private static final int MIN_HEIGHT = 300; private static final int MIN_WIDTH = 200; @@ -136,7 +137,7 @@ public class DropDownTextField extends JTextField implements GComponent { * @param dataModel provides element storage and search capabilities to this component. */ public DropDownTextField(DropDownTextFieldDataModel dataModel) { - this(dataModel, 350); + this(dataModel, DEFAULT_MIN_UPDATE_DELAY); } /** diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java new file mode 100644 index 0000000000..7a17a29c12 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AlphaNumericComparator.java @@ -0,0 +1,160 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.datastruct; + +import java.util.Comparator; + +/** + * A comparator to perform a natural sort for text and numbers. Numbers in the text will compared + * based on their parsed value, not their ASCII value. + */ +public class AlphaNumericComparator implements Comparator { + + @Override + public int compare(String s1, String s2) { + if (s1 == null || s2 == null) { + return s1 == s2 ? 0 : (s1 == null ? -1 : 1); + } + + int i = 0; + int j = 0; + int len1 = s1.length(); + int len2 = s2.length(); + + // Core Pass: Compare semantic characters and mathematical numbers + while (i < len1 && j < len2) { + + char c1 = s1.charAt(i); + char c2 = s2.charAt(j); + + // Skip spaces dynamically + if (c1 == ' ') { + i++; + continue; + } + if (c2 == ' ') { + j++; + continue; + } + + boolean isDigit1 = Character.isDigit(c1); + boolean isDigit2 = Character.isDigit(c2); + if (isDigit1 && isDigit2) { + // Parse full numbers from the current position + long val1 = parseNumber(s1, i); + long val2 = parseNumber(s2, j); + if (val1 != val2) { + return Long.compare(val1, val2); + } + + // Skip ahead past the digit sequences we just parsed + i = skipDigits(s1, i); + j = skipDigits(s2, j); + } + else { + // Compare standard characters alphabetically + if (c1 != c2) { + return Character.compare(c1, c2); + } + i++; + j++; + } + } + + // Clean up any remaining trailing spaces + i = skipSpaces(s1, i); + j = skipSpaces(s2, j); + + // Tie-breaker 1: Shorter structure wins (e.g., "file4" before "file 4") + boolean ended1 = (i == len1); + boolean ended2 = (j == len2); + if (ended1 != ended2) { + return ended1 ? -1 : 1; + } + + // Both strings have the same length. If they have numbers, the numbers have been + // parsed to be the same numeric value, but they may have leading zeros. + // Prefer fewer leading zeros (e.g., "file4" before "file04"). + // A non-zero value means that both strings have digits that were compared. + int lengthCompare = compareDigitLengths(s1, s2); + if (lengthCompare != 0) { + return lengthCompare; + } + + // Default to a string compare + return s1.compareTo(s2); + } + + private long parseNumber(String s, int i) { + long value = 0; + while (i < s.length()) { + char c = s.charAt(i); + if (!Character.isDigit(c)) { + return value; + } + + int intValue = (c - '0'); + value = value * 10 + intValue; + i++; + } + return value; + } + + private int skipDigits(String s, int i) { + int len = s.length(); + while (i < len && Character.isDigit(s.charAt(i))) { + i++; + } + return i; + } + + private int skipSpaces(String s, int i) { + int len = s.length(); + while (i < len && s.charAt(i) == ' ') { + i++; + } + return i; + } + + private int compareDigitLengths(String s1, String s2) { + int len1 = s1.length(); + int len2 = s2.length(); + int i = 0; + int j = 0; + while (i < len1 && j < len2) { + i = skipSpaces(s1, i); + j = skipSpaces(s2, j); + if (i >= len1 || j >= len2) { + break; + } + + if (Character.isDigit(s1.charAt(i)) && Character.isDigit(s2.charAt(j))) { + int skip1 = skipDigits(s1, i) - i; + int skip2 = skipDigits(s2, j) - j; + if (skip1 != skip2) { + return Integer.compare(skip1, skip2); + } + i += skip1; + j += skip2; + } + else { + i++; + j++; + } + } + return 0; + } +} diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java new file mode 100644 index 0000000000..cbc567e10d --- /dev/null +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/AlphaNumericComparatorTest.java @@ -0,0 +1,84 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.datastruct; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import generic.test.AbstractGenericTest; + +public class AlphaNumericComparatorTest extends AbstractGenericTest { + + @Test + public void testIt() { + + List unsorted = Arrays.asList( + + " file10", // Leading spaces + "file02 ", // Trailing spaces + "file 2", // Internal spaces + "file1", // Normal + " file002 ", // Leading and trailing spaces + "file 01", // Internal space before digit + "file20", + + " apple", // Leading spaces + "Apple ", // Trailing spaces + "ba nana", // Internal spaces + "a", + "alpha ", // Trailing spaces + " alphabet", // Leading spaces + "app", + "ap ple", // Internal spaces + + "0.1.0", + "0.1.9", + "1.0", + "0.2.1", + "2.1.0"); + + List sorted = Arrays.asList( + "0.1.0", + "0.1.9", + "0.2.1", + "1.0", + "2.1.0", + "Apple ", + "a", + "alpha ", + " alphabet", + "app", + " apple", + "ap ple", + "ba nana", + "file1", + "file 01", + "file 2", + "file02 ", + " file002 ", + " file10", + "file20"); + + Collections.shuffle(unsorted); + List copy = new ArrayList<>(unsorted); + Collections.sort(copy, new AlphaNumericComparator()); + + assertEquals(sorted, copy); + } +}