diff --git a/dotCMS/src/integration-test/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImplTest.java b/dotCMS/src/integration-test/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImplTest.java index 42080893d617..ff2f1aaea8a7 100644 --- a/dotCMS/src/integration-test/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImplTest.java +++ b/dotCMS/src/integration-test/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImplTest.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Optional; +import static com.dotmarketing.portlets.contentlet.business.HostFactoryImpl.SITE_IS_LIVE_OR_STOPPED; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class HostFactoryImplTest extends IntegrationTestBase { @@ -73,4 +75,43 @@ public void test_findLiveAndStopped_shouldOnlyRetunLiveAndStoppedSites() throws } } + + /** + * Method to test: {@link HostFactoryImpl#search(String, String, boolean, int, int, User, boolean)} + * Given Scenario: Create many (20+) sites that have the same text in them + * example1.test.com, example2.test.com..., then just test.com + * ExpectedResult: Exact matches should be at the top of the search results. + * + */ + @Test + public void test_search_shouldReturnExactMatchesFirst() throws DotDataException, DotSecurityException { + // Initialization + final int limit = 15; + final int offset = 0; + final HostFactoryImpl hostFactory = new HostFactoryImpl(); + final long systemMilis = System.currentTimeMillis(); + + final String baseName = "test.com"; + + // generate 20 sites with the name test.com + for (int i = 0; i < 20; i++) { + new SiteDataGen().name("example"+i+"-"+systemMilis+"."+baseName).nextPersisted(true); + } + + //get the site with the name test.com + Host testSite = APILocator.getHostAPI().findByName(baseName, APILocator.systemUser(), false); + + //validate if the site is null + //if is null create a new site with the name test.com + if (testSite == null) { + testSite = new SiteDataGen().name(baseName).nextPersisted(true); + } + + // test the method search at class HostFactoryImpl where the filter is "test.com" and should return it first in list + final Optional> hostsList = hostFactory.search(baseName, SITE_IS_LIVE_OR_STOPPED,false ,limit, offset, APILocator.systemUser(), false); + + //validations + assertTrue( "Test site is not contained in list", hostsList.get().contains(testSite)); + assertEquals("Test site is not the first in the list", testSite, hostsList.get().get(0)); + } } \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImpl.java index 0afe67b35b8d..e16d1d9a661d 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostFactoryImpl.java @@ -37,6 +37,7 @@ import com.dotmarketing.portlets.templates.model.Template; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; +import com.google.common.annotations.VisibleForTesting; import com.liferay.portal.model.User; import io.vavr.control.Try; import org.apache.commons.lang3.concurrent.ConcurrentUtils; @@ -74,61 +75,69 @@ public class HostFactoryImpl implements HostFactory { private final String AND = " AND "; private final String ORDER_BY = " ORDER BY ? "; - private static final StringBuilder SELECT_SYSTEM_HOST = new StringBuilder() - .append("SELECT id FROM identifier WHERE id = '").append(Host.SYSTEM_HOST).append("' "); + private static final String SELECT_SYSTEM_HOST = "SELECT id FROM identifier WHERE id = '"+ Host.SYSTEM_HOST+"' "; - private static final StringBuilder FROM_JOINED_TABLES = new StringBuilder() - .append("INNER JOIN identifier i ") - .append("ON c.identifier = i.id AND i.asset_subtype = '").append(Host.HOST_VELOCITY_VAR_NAME).append("' ") - .append("INNER JOIN contentlet_version_info cvi ") - .append("ON c.inode = cvi.working_inode "); + private static final String FROM_JOINED_TABLES = "INNER JOIN identifier i " + + "ON c.identifier = i.id AND i.asset_subtype = '" + Host.HOST_VELOCITY_VAR_NAME + "' " + + "INNER JOIN contentlet_version_info cvi " + + "ON c.inode = cvi.working_inode "; - private static final StringBuilder SELECT_SITE_INODE = new StringBuilder().append("SELECT c.inode FROM contentlet" + - " c ").append(FROM_JOINED_TABLES); + private static final String SELECT_SITE_INODE = "SELECT c.inode FROM contentlet" + + " c " + + FROM_JOINED_TABLES; + private static final String SELECT_SITE_INODE_AND_ALIASES = "SELECT c.inode, %s" + + " AS aliases FROM contentlet c " + + FROM_JOINED_TABLES; - private static final StringBuilder SELECT_SITE_INODE_AND_ALIASES = new StringBuilder().append("SELECT c.inode, %s" + - " AS aliases FROM contentlet c ").append(FROM_JOINED_TABLES); + private static final String POSTGRES_ALIASES_COLUMN = ContentletJsonAPI + .CONTENTLET_AS_JSON + + "->'fields'->'" + Host.ALIASES_KEY + "'->>'value' "; - private static final StringBuilder POSTGRES_ALIASES_COLUMN = new StringBuilder(ContentletJsonAPI - .CONTENTLET_AS_JSON).append("->'fields'->'").append(Host.ALIASES_KEY).append("'->>'value' "); + private static final String MSSQL_ALIASES_COLUMN = "JSON_VALUE(c." + + ContentletJsonAPI.CONTENTLET_AS_JSON + ", '$.fields." + Host.ALIASES_KEY + ".value') "; + private static final String ALIASES_COLUMN = "c.text_area1"; - private static final StringBuilder MSSQL_ALIASES_COLUMN = new StringBuilder("JSON_VALUE(c.").append - (ContentletJsonAPI.CONTENTLET_AS_JSON).append(", '$.fields.").append(Host.ALIASES_KEY).append(".value') "); + private static final String SITE_NAME_LIKE = "LOWER(%s) LIKE ? "; - private static final StringBuilder ALIASES_COLUMN = new StringBuilder("c.text_area1"); + private static final String SITE_NAME_EQUALS ="LOWER(%s) = ? "; - private static final StringBuilder SITE_NAME_LIKE = new StringBuilder().append("LOWER(%s) LIKE ? "); + private static final String POSTGRES_SITENAME_COLUMN = ContentletJsonAPI + .CONTENTLET_AS_JSON + + "->'fields'->'" + Host.HOST_NAME_KEY + "'->>'value' "; - private static final StringBuilder SITE_NAME_EQUALS = new StringBuilder().append("LOWER(%s) = ? "); + private static final String MSSQL_SITENAME_COLUMN = "JSON_VALUE(c." + + ContentletJsonAPI.CONTENTLET_AS_JSON + ", '$.fields." + Host.HOST_NAME_KEY + ".value')" + + " "; - private static final StringBuilder POSTGRES_SITENAME_COLUMN = new StringBuilder(ContentletJsonAPI - .CONTENTLET_AS_JSON).append("->'fields'->'").append(Host.HOST_NAME_KEY).append("'->>'value' "); + private static final String SITENAME_COLUMN = "c.text1"; - private static final StringBuilder MSSQL_SITENAME_COLUMN = new StringBuilder("JSON_VALUE(c.").append - (ContentletJsonAPI.CONTENTLET_AS_JSON).append(", '$.fields.").append(Host.HOST_NAME_KEY).append(".value')" + - " "); + private static final String ALIAS_LIKE = "LOWER(%s) LIKE ? "; - private static final StringBuilder SITENAME_COLUMN = new StringBuilder("c.text1"); + private static final String EXCLUDE_SYSTEM_HOST = "i.id <> '" + Host + .SYSTEM_HOST + + "' "; - private static final StringBuilder ALIAS_LIKE = new StringBuilder().append("LOWER(%s) LIKE ? "); + private static final String SITE_IS_LIVE = "cvi.live_inode IS NOT NULL"; + @VisibleForTesting + protected static final String SITE_IS_LIVE_OR_STOPPED = "cvi.live_inode IS NOT null or " + + "(cvi.live_inode IS NULL AND cvi.deleted = false )"; + private static final String SITE_IS_STOPPED = "cvi.live_inode IS NULL AND cvi" + + ".deleted = " + + getDBFalse(); - private static final StringBuilder EXCLUDE_SYSTEM_HOST = new StringBuilder().append("i.id <> '").append(Host - .SYSTEM_HOST).append("' "); + private static final String SITE_IS_STOPPED_OR_ARCHIVED = "cvi.live_inode IS NULL"; - private static final StringBuilder SITE_IS_LIVE = new StringBuilder().append("cvi.live_inode IS NOT NULL"); - private static final StringBuilder SITE_IS_LIVE_OR_STOPPED = new StringBuilder().append("cvi.live_inode IS NOT null or " + - "(cvi.live_inode IS NULL AND cvi.deleted = false )"); - private static final StringBuilder SITE_IS_STOPPED = new StringBuilder().append("cvi.live_inode IS NULL AND cvi" + - ".deleted = ").append(getDBFalse()); + private static final String SITE_IS_ARCHIVED = "cvi.live_inode IS NULL AND cvi" + + ".deleted = " + + getDBTrue(); - private static final StringBuilder SITE_IS_STOPPED_OR_ARCHIVED = new StringBuilder().append("cvi.live_inode IS NULL"); + private static final String SELECT_SITE_COUNT = "SELECT COUNT(cvi.working_inode) " + + "FROM contentlet_version_info cvi, identifier i " + "WHERE i.asset_subtype = '" + + Host.HOST_VELOCITY_VAR_NAME + "' " + " AND cvi.identifier = i.id "; - private static final StringBuilder SITE_IS_ARCHIVED = new StringBuilder().append("cvi.live_inode IS NULL AND cvi" + - ".deleted = ").append(getDBTrue()); - - private static final StringBuilder SELECT_SITE_COUNT = new StringBuilder().append("SELECT COUNT(cvi.working_inode) ") - .append("FROM contentlet_version_info cvi, identifier i ").append("WHERE i.asset_subtype = '") - .append(Host.HOST_VELOCITY_VAR_NAME).append("' ").append(" AND cvi.identifier = i.id "); + // query that Exact matches should be at the top of the search results. + private static final String PRIORITIZE_EXACT_MATCHES = + "ORDER BY CASE WHEN LOWER(%s) = ? THEN 0 ELSE 1 END"; /** * Default class constructor. @@ -170,7 +179,7 @@ public Host bySiteName(final String siteName) { final DotConnect dc = new DotConnect(); final StringBuilder sqlQuery = new StringBuilder().append(SELECT_SITE_INODE) .append(WHERE); - sqlQuery.append(getSiteNameColumn(SITE_NAME_EQUALS.toString())); + sqlQuery.append(getSiteNameColumn(SITE_NAME_EQUALS)); dc.setSQL(sqlQuery.toString()); dc.addParam(siteName.toLowerCase()); try { @@ -212,9 +221,9 @@ public Host byAlias(String alias) { String sql = sqlQuery.toString(); if (APILocator.getContentletJsonAPI().isJsonSupportedDatabase()) { if (DbConnectionFactory.isPostgres()) { - sql = String.format(sql, POSTGRES_ALIASES_COLUMN.toString(), POSTGRES_ALIASES_COLUMN.toString()); + sql = String.format(sql, POSTGRES_ALIASES_COLUMN, POSTGRES_ALIASES_COLUMN); } else { - sql = String.format(sql, MSSQL_ALIASES_COLUMN.toString(), MSSQL_ALIASES_COLUMN.toString()); + sql = String.format(sql, MSSQL_ALIASES_COLUMN, MSSQL_ALIASES_COLUMN); } } else { sql = String.format(sql, ALIASES_COLUMN, ALIASES_COLUMN); @@ -628,7 +637,7 @@ public Optional findDefaultHost(final String contentTypeId, final String c @Override public Optional> findLiveSites(final String siteNameFilter, final int limit, final int offset, final boolean showSystemHost, final User user, final boolean respectFrontendRoles) { - return search(siteNameFilter, SITE_IS_LIVE.toString(), showSystemHost, limit, offset, user, + return search(siteNameFilter, SITE_IS_LIVE, showSystemHost, limit, offset, user, respectFrontendRoles); } @@ -637,7 +646,7 @@ public Optional> findStoppedSites(final String siteNameFilter, final final int limit, final int offset, final boolean showSystemHost, final User user, final boolean respectFrontendRoles) { final String condition = - includeArchivedSites ? SITE_IS_STOPPED_OR_ARCHIVED.toString() : SITE_IS_STOPPED.toString(); + includeArchivedSites ? SITE_IS_STOPPED_OR_ARCHIVED : SITE_IS_STOPPED; return search(siteNameFilter, condition, showSystemHost, limit, offset, user, respectFrontendRoles); } @@ -645,14 +654,14 @@ public Optional> findStoppedSites(final String siteNameFilter, final public Optional> findArchivedSites(final String siteNameFilter, final int limit, final int offset, final boolean showSystemHost, final User user, final boolean respectFrontendRoles) { - return search(siteNameFilter, SITE_IS_ARCHIVED.toString(), showSystemHost, limit, offset, user, + return search(siteNameFilter, SITE_IS_ARCHIVED, showSystemHost, limit, offset, user, respectFrontendRoles); } @Override public long count() throws DotDataException { final DotConnect dc = new DotConnect(); - dc.setSQL(SELECT_SITE_COUNT.toString()); + dc.setSQL(SELECT_SITE_COUNT); final List> dbResults = dc.loadResults(); final String total = dbResults.get(0).get("count"); return ConversionUtils.toLong(total, 0L); @@ -682,7 +691,8 @@ public long count() throws DotDataException { * * @return The list of {@link Host} objects that match the specified search criteria. */ - private Optional> search(final String siteNameFilter, final String condition, final boolean + @VisibleForTesting + protected Optional> search(final String siteNameFilter, final String condition, final boolean showSystemHost, final int limit, final int offset, final User user, final boolean respectFrontendRoles) { final DotConnect dc = new DotConnect(); final StringBuilder sqlQuery = new StringBuilder().append(SELECT_SITE_INODE); @@ -690,7 +700,7 @@ private Optional> search(final String siteNameFilter, final String co sqlQuery.append("cvi.identifier = i.id"); if (UtilMethods.isSet(siteNameFilter)) { sqlQuery.append(AND); - sqlQuery.append(getSiteNameColumn(SITE_NAME_LIKE.toString())); + sqlQuery.append(getSiteNameColumn(SITE_NAME_LIKE)); } if (UtilMethods.isSet(condition)) { sqlQuery.append(AND); @@ -700,9 +710,15 @@ private Optional> search(final String siteNameFilter, final String co sqlQuery.append(AND); sqlQuery.append(EXCLUDE_SYSTEM_HOST); } + if (UtilMethods.isSet(siteNameFilter)) { + sqlQuery.append(getSiteNameColumn(PRIORITIZE_EXACT_MATCHES)); + } dc.setSQL(sqlQuery.toString()); if (UtilMethods.isSet(siteNameFilter)) { + // Add the site name filter parameter dc.addParam("%" + siteNameFilter.trim() + "%"); + // Add the site name filter parameter again, but this time for the exact match + dc.addParam(siteNameFilter.trim().replace("%", "")); } if (limit > 0) { dc.setMaxRows(limit); diff --git a/hotfix_tracking.md b/hotfix_tracking.md index 3e487f0fc753..9b1ca181c37c 100644 --- a/hotfix_tracking.md +++ b/hotfix_tracking.md @@ -147,3 +147,4 @@ This maintenance release includes the following code fixes: 120. https://github.com/dotCMS/core/issues/25827 : Recreating a field with same name diff type uses the same id #25827 121. https://github.com/dotCMS/core/issues/25870 : Using showFields field variable replicates title to all items #25870 122. https://github.com/dotCMS/core/issues/26374 : Use of Png filter on images results in a 404 #26374 +123. https://github.com/dotCMS/core/issues/24921 : Filtering does not put exact matches on the top of the site dropdown when searching #24921