diff --git a/backend/common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseSearchHandler.java b/backend/common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseSearchHandler.java index 45a5cb1586..42d91f19f7 100644 --- a/backend/common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseSearchHandler.java +++ b/backend/common/src/main/java/org/eclipse/sw360/datahandler/db/ReleaseSearchHandler.java @@ -24,7 +24,10 @@ import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.eclipse.sw360.datahandler.couchdb.lucene.NouveauLuceneAwareDatabaseConnector.prepareWildcardQuery; import static org.eclipse.sw360.nouveau.LuceneAwareCouchDbConnector.DEFAULT_DESIGN_PREFIX; @@ -37,11 +40,22 @@ public class ReleaseSearchHandler { private static final String DDOC_NAME = DEFAULT_DESIGN_PREFIX + "lucene"; + private static final Pattern DIGIT_SEQUENCE_PATTERN = Pattern.compile("\\d+"); private static final NouveauIndexDesignDocument luceneSearchView = new NouveauIndexDesignDocument("releases", new NouveauIndexFunction( "function(doc) {" + + " function normalizeVersionForSort(version) {" + + " if (!version || typeof(version) !== 'string') { return ''; }" + + " var lower = version.toLowerCase();" + + " return lower.replace(/\\d+/g, function(match) {" + + " var normalized = match.replace(/^0+(?!$)/, '');" + + " var length = normalized.length.toString();" + + " while (length.length < 6) { length = '0' + length; }" + + " return '{' + length + normalized + '}';" + + " });" + + " }" + " if(doc.type == 'release') {" + " if (doc.name && typeof(doc.name) == 'string' && doc.name.length > 0) {" + " index('text', 'name', doc.name, {'store': true});" + @@ -49,7 +63,7 @@ public class ReleaseSearchHandler { " }" + " if (doc.version && typeof(doc.version) == 'string' && doc.version.length > 0) {" + " index('text', 'version', doc.version, {'store': true});" + - " index('string', 'version_sort', doc.version);" + + " index('string', 'version_sort', normalizeVersionForSort(doc.version));" + " }" + " if(doc.createdOn && doc.createdOn.length) {"+ " var dt = new Date(doc.createdOn);"+ @@ -102,4 +116,40 @@ public Map> search(String searchText, PaginationDa default -> "createdOn"; }; } + + static String normalizeVersionForSort(String version) { + if (version == null || version.isEmpty()) { + return ""; + } + String lower = version.toLowerCase(Locale.ROOT); + Matcher matcher = DIGIT_SEQUENCE_PATTERN.matcher(lower); + StringBuilder normalized = new StringBuilder(lower.length() + 16); + int cursor = 0; + while (matcher.find()) { + normalized.append(lower, cursor, matcher.start()); + appendNumericToken(normalized, matcher.group()); + cursor = matcher.end(); + } + normalized.append(lower, cursor, lower.length()); + return normalized.toString(); + } + + private static void appendNumericToken(StringBuilder output, String rawNumber) { + int significantStart = 0; + while (significantStart < rawNumber.length() - 1 && rawNumber.charAt(significantStart) == '0') { + significantStart++; + } + String significant = rawNumber.substring(significantStart); + output.append('{'); + appendZeroPaddedLength(output, significant.length()); + output.append(significant).append('}'); + } + + private static void appendZeroPaddedLength(StringBuilder output, int length) { + String lengthAsString = Integer.toString(length); + for (int i = lengthAsString.length(); i < 6; i++) { + output.append('0'); + } + output.append(lengthAsString); + } } diff --git a/backend/common/src/test/java/org/eclipse/sw360/datahandler/db/ReleaseSearchHandlerTest.java b/backend/common/src/test/java/org/eclipse/sw360/datahandler/db/ReleaseSearchHandlerTest.java new file mode 100644 index 0000000000..80a32c67fd --- /dev/null +++ b/backend/common/src/test/java/org/eclipse/sw360/datahandler/db/ReleaseSearchHandlerTest.java @@ -0,0 +1,55 @@ +/* + * Copyright Siemens AG, 2026. Part of the SW360 Portal Project. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.sw360.datahandler.db; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ReleaseSearchHandlerTest { + + @Test + public void should_sort_versions_naturally_for_numeric_segments() { + List versions = new ArrayList<>(Arrays.asList("1.10", "1.2", "1.0", "2.0", "1.9")); + + versions.sort(Comparator.comparing(ReleaseSearchHandler::normalizeVersionForSort)); + + assertEquals(Arrays.asList("1.0", "1.2", "1.9", "1.10", "2.0"), versions); + } + + @Test + public void should_treat_leading_zero_numeric_segments_as_equal_value() { + String v1 = ReleaseSearchHandler.normalizeVersionForSort("1.02"); + String v2 = ReleaseSearchHandler.normalizeVersionForSort("1.2"); + + assertEquals(v1, v2); + } + + @Test + public void should_sort_numeric_suffixes_naturally() { + String alpha2 = ReleaseSearchHandler.normalizeVersionForSort("1.0.0-alpha2"); + String alpha10 = ReleaseSearchHandler.normalizeVersionForSort("1.0.0-alpha10"); + + assertTrue(alpha2.compareTo(alpha10) < 0); + } + + @Test + public void should_pad_numeric_length_prefix_to_six_digits() { + String normalized = ReleaseSearchHandler.normalizeVersionForSort("v123"); + + assertEquals("v{000003123}", normalized); + } +} \ No newline at end of file