# HG changeset patch # Parent a3114c94d4ffafed3605052b1d44d320824acd33 #242226: Add natural sorting to DataFolder.SortModes diff -r a3114c94d4ff openide.loaders/apichanges.xml --- a/openide.loaders/apichanges.xml Wed Oct 21 06:14:25 2015 +0000 +++ b/openide.loaders/apichanges.xml Wed Oct 21 15:39:04 2015 +0200 @@ -109,6 +109,25 @@ + + + Introduces SortMode for natural sorting. + + + + + +

+ Added support for natural sorting of DataObjects. This means + that the sort is case insensitive and number sequences are + sorted by value rather than lexicographically. +

+
+ + +
Separate template handling diff -r a3114c94d4ff openide.loaders/manifest.mf --- a/openide.loaders/manifest.mf Wed Oct 21 06:14:25 2015 +0000 +++ b/openide.loaders/manifest.mf Wed Oct 21 15:39:04 2015 +0200 @@ -1,6 +1,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.openide.loaders -OpenIDE-Module-Specification-Version: 7.64 +OpenIDE-Module-Specification-Version: 7.65 OpenIDE-Module-Localizing-Bundle: org/openide/loaders/Bundle.properties OpenIDE-Module-Provides: org.netbeans.modules.templates.v1_0 OpenIDE-Module-Layer: org/netbeans/modules/openide/loaders/layer.xml diff -r a3114c94d4ff openide.loaders/src/org/openide/loaders/Bundle.properties --- a/openide.loaders/src/org/openide/loaders/Bundle.properties Wed Oct 21 06:14:25 2015 +0000 +++ b/openide.loaders/src/org/openide/loaders/Bundle.properties Wed Oct 21 15:39:04 2015 +0200 @@ -110,6 +110,7 @@ VALUE_sort_last_modified=By Modification Time VALUE_sort_size=By File Size VALUE_sort_extensions=By Extension +VALUE_sort_natural=Naturally By Name # diff -r a3114c94d4ff openide.loaders/src/org/openide/loaders/DataFolder.java --- a/openide.loaders/src/org/openide/loaders/DataFolder.java Wed Oct 21 06:14:25 2015 +0000 +++ b/openide.loaders/src/org/openide/loaders/DataFolder.java Wed Oct 21 15:39:04 2015 +0200 @@ -1108,6 +1108,15 @@ */ public static final SortMode EXTENSIONS = new FolderComparator(FolderComparator.EXTENSIONS); + /** + * Folder go first (sorted naturally by name) followed by files sorted + * by natural name and extension. Natural means that number sequences + * are evaluated and compared by value rather than lexicographically. + * + * @since org.openide.loaders 7.65 + */ + public static final SortMode NATURAL = new FolderComparator(FolderComparator.NATURAL); + /** Method to write the sort mode to a folder's attributes. * @param folder folder write this mode to */ @@ -1126,6 +1135,10 @@ x = "M"; // NOI18N } else if (this == SIZE) { x = "S"; // NOI18N + } else if (this == EXTENSIONS) { + x = "X"; // NOI18N + } else if (this == NATURAL) { + x = "L"; // NOI18N } else { x = "O"; // NOI18N } @@ -1148,6 +1161,8 @@ case 'O': return NONE; case 'M': return LAST_MODIFIED; case 'S': return SIZE; + case 'X': return EXTENSIONS; + case 'L': return NATURAL; case 'F': default: return FOLDER_NAMES; diff -r a3114c94d4ff openide.loaders/src/org/openide/loaders/FolderComparator.java --- a/openide.loaders/src/org/openide/loaders/FolderComparator.java Wed Oct 21 06:14:25 2015 +0000 +++ b/openide.loaders/src/org/openide/loaders/FolderComparator.java Wed Oct 21 15:39:04 2015 +0200 @@ -69,6 +69,8 @@ public static final int SIZE = 5; /** by extension, then name */ public static final int EXTENSIONS = 6; + /** by natural name (f10.txt > f9.txt) */ + public static final int NATURAL = 7; /** mode to use */ @@ -110,6 +112,8 @@ return compareSize(obj1, obj2); case EXTENSIONS: return compareExtensions(obj1, obj2); + case NATURAL: + return compareNatural(obj1, obj2); default: assert false : mode; return 0; @@ -268,4 +272,124 @@ return fo1.getNameExt().compareTo(fo2.getNameExt()); } } + + private static int compareNatural(Object o1, Object o2) { + + FileObject fo1 = findFileObject(o1); + FileObject fo2 = findFileObject(o2); + + // Folders first. + boolean f1 = fo1.isFolder(); + boolean f2 = fo2.isFolder(); + if (f1 != f2) { + return f1 ? -1 : 1; + } + + int res = compareFileNameNatural(fo1.getNameExt(), fo2.getNameExt()); + return res; + } + + private static int compareFileNameNatural(String name1, String name2) { + + String n1 = name1.toLowerCase(); + String n2 = name2.toLowerCase(); + + int p1; // pointer to first string + int p2; // pointer to second string + + for (p1 = 0, p2 = 0; p1 < n1.length() && p2 < n2.length(); ) { + char c1 = n1.charAt(p1); + char c2 = n2.charAt(p2); + + ReadNumericValueResult nv1 = readNumericValue(n1, p1); + ReadNumericValueResult nv2 = readNumericValue(n2, p2); + + if (nv1 != null && nv2 != null) { + if (nv1.getValue() == nv2.getValue()) { + p1 = nv1.getEndPos(); + p2 = nv2.getEndPos(); + } else { + return nv1.getValue() - nv2.getValue(); + } + } else { + if (c1 != c2) { + return c1 - c2; + } else { + p1 ++; + p2 ++; + } + } + } + boolean unfinished1 = p1 < n1.length(); + boolean unfinished2 = p2 < n2.length(); + if (!unfinished1 && !unfinished2) { + return name1.compareTo(name2); + } else if (unfinished1) { + return 1; // first string is longer (prefix of second string) + } else if (unfinished2) { + return -1; // second string is longer (prefix of first string) + } else { + assert false : "Invalid state in natural comparator"; //NOI18N + return n1.compareTo(n2); + } + } + + /** + * Read numeric value token starting at position {@code pos}. It can be + * delimited by whitespace (so it supports values in strings like "a1b", "a + * 1b", "a1 b", "a 1 b"). + * + * @param s Input string. + * @param pos Position in the input string. + * + * @return The numeric value starting at that position and end position in + * the string, or null if there is no numeric value (e.g. there is a + * white-space only.). + */ + private static ReadNumericValueResult readNumericValue(String s, int pos) { + int val = 0; + boolean num = false; // some number value was read + boolean afterNum = false; // we are reading trailing whitespace + int len = s.length(); + for (int i = pos; i < len; i++) { + char c = s.charAt(i); + if (c >= '0' && c <= '9') { + if (afterNum) { // new, separated number encountered + return new ReadNumericValueResult(val, i); + } + val = val * 10 + (c - '0'); + num = true; + } else if (Character.isWhitespace(c)) { // leading or trailing space + if (num) { + afterNum = true; // in trailing whitespace after number + } + } else { + return num ? new ReadNumericValueResult(val, i) : null; + } + } + return num ? new ReadNumericValueResult(val, len) : null; + } + + /** + * Class for representing result returned from + * {@link #readNumericValue(String, int)}. + */ + private static class ReadNumericValueResult { + + private final int value; + private final int endPos; + + public ReadNumericValueResult(int value, int endPos) { + this.value = value; + this.endPos = endPos; + } + + public int getValue() { + return value; + } + + public int getEndPos() { + return endPos; + } + } } diff -r a3114c94d4ff openide.loaders/src/org/openide/loaders/FolderOrder.java --- a/openide.loaders/src/org/openide/loaders/FolderOrder.java Wed Oct 21 06:14:25 2015 +0000 +++ b/openide.loaders/src/org/openide/loaders/FolderOrder.java Wed Oct 21 15:39:04 2015 +0200 @@ -169,7 +169,24 @@ } // compare by the provided comparator - return ((FolderComparator)(getSortMode())).doCompare(obj1, obj2); + SortMode comparator = getSortMode(); + if (comparator instanceof FolderComparator) { + return ((FolderComparator) comparator).doCompare(obj1, obj2); + } else if ((obj1 instanceof DataObject) // Also support custom + && (obj2 instanceof DataObject)) { // comparators, #242226. + return comparator.compare( + (DataObject) obj1, (DataObject) obj2); + } else { + FileObject fo1 = FolderComparator.findFileObject(obj1); + FileObject fo2 = FolderComparator.findFileObject(obj2); + try { + return comparator.compare( + DataObject.find(fo1), DataObject.find(fo2)); + } catch (DataObjectNotFoundException ex) { + throw new IllegalArgumentException("Expected " //NOI18N + + "DataObjects or Nodes."); //NOI18N + } + } } else { if (i2 == null) { return -1; diff -r a3114c94d4ff openide.loaders/src/org/openide/loaders/SortModeEditor.java --- a/openide.loaders/src/org/openide/loaders/SortModeEditor.java Wed Oct 21 06:14:25 2015 +0000 +++ b/openide.loaders/src/org/openide/loaders/SortModeEditor.java Wed Oct 21 15:39:04 2015 +0200 @@ -58,7 +58,8 @@ DataFolder.SortMode.FOLDER_NAMES, DataFolder.SortMode.LAST_MODIFIED, DataFolder.SortMode.SIZE, - DataFolder.SortMode.EXTENSIONS + DataFolder.SortMode.EXTENSIONS, + DataFolder.SortMode.NATURAL }; /** Names for modes. First is for displaying files */ @@ -69,7 +70,8 @@ DataObject.getString ("VALUE_sort_folder_names"), DataObject.getString ("VALUE_sort_last_modified"), DataObject.getString ("VALUE_sort_size"), - DataObject.getString ("VALUE_sort_extensions") + DataObject.getString ("VALUE_sort_extensions"), + DataObject.getString ("VALUE_sort_natural") }; /** @return names of the two possible modes */ diff -r a3114c94d4ff openide.loaders/test/unit/src/org/openide/loaders/FolderComparatorTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openide.loaders/test/unit/src/org/openide/loaders/FolderComparatorTest.java Wed Oct 21 15:39:04 2015 +0200 @@ -0,0 +1,167 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2015 Sun Microsystems, Inc. + */ +package org.openide.loaders; + +import java.awt.EventQueue; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import org.junit.Test; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileSystem; +import org.openide.filesystems.FileUtil; + +/** + * + * @author jhavlin + */ +public class FolderComparatorTest { + + public FolderComparatorTest() { + } + + @Test + public void testNaturalComparatorBasic() throws IOException { + testNaturalComparator(new String[]{ + "b 10.txt", + "b 9.txt", + "a2.txt", + "a 4 9.txt", + "a10.txt", + "b0070.txt", + "a 3.txt", + "b08.txt" + }, new String[]{ + "a2.txt", + "a 3.txt", + "a 4 9.txt", + "a10.txt", + "b08.txt", + "b 9.txt", + "b 10.txt", + "b0070.txt" + }); + } + + @Test + public void testNaturalComparatorWithSuffixes() throws IOException { + testNaturalComparator(new String[]{ + "a01b", + "a2x", + "a02", + "a1" + }, new String[]{ + "a1", + "a01b", + "a02", + "a2x" + }); + } + + @Test + public void testUseCustomComparator() throws IOException, + InterruptedException, InvocationTargetException { + + FileSystem fs = FileUtil.createMemoryFileSystem(); + + fs.getRoot().createData("aaaa.txt"); + fs.getRoot().createData("bbb.txt"); + fs.getRoot().createData("cc.txt"); + fs.getRoot().createData("d.txt"); + fs.getRoot().refresh(); + + DataFolder.SortMode custom = new DataFolder.SortMode() { + @Override + public int compare(DataObject o1, DataObject o2) { + return o1.getName().length() - o2.getName().length(); + } + }; + + DataFolder df = DataFolder.findFolder(fs.getRoot()); + df.setSortMode(custom); + EventQueue.invokeAndWait(new Runnable() { + @Override + public void run() { + } + }); + DataObject[] children = df.getChildren(); + assertEquals("d.txt", children[0].getName()); + assertEquals("cc.txt", children[1].getName()); + assertEquals("bbb.txt", children[2].getName()); + assertEquals("aaaa.txt", children[3].getName()); + } + + @Test + public void testNaturalComparatorFallback() throws IOException { + testNaturalComparator(new String[]{ + "a01.txt", + "a001.txt", + "A1.txt" + }, new String[]{ + "A1.txt", + "a001.txt", + "a01.txt" + }); + } + + private void testNaturalComparator(String[] fileNames, + String[] expectedOrder) throws IOException { + FolderComparator c = new FolderComparator(FolderComparator.NATURAL); + FileSystem fs = FileUtil.createMemoryFileSystem(); + FileObject root = fs.getRoot(); + List list = new ArrayList(); + for (String n : fileNames) { + FileObject fo = root.createData(n); + assertNotNull(fo); + list.add(DataObject.find(fo)); + } + + Collections.sort(list, c); + for (int i = 0; i < expectedOrder.length; i++) { + assertEquals(expectedOrder[i], list.get(i).getName()); + } + } +}