From 7e09771ed34e237825a9c81b90b2b4a49403b5be Mon Sep 17 00:00:00 2001 From: freddyDOTCMS <147462678+freddyDOTCMS@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:16:50 -0600 Subject: [PATCH] #28896 Create Factory method to find categories in all levels (#29021) This is the first PR for https://github.com/dotCMS/core/issues/28896 Later I am going to use this Factory to create a API Method ### Proposed Changes * Create Method to find categories in all levels and apply a filter https://github.com/dotCMS/core/pull/29021/files#diff-bf828c747c99cefe73af7cec527db7f7e64bd4a66ba54a55eb1170f6a2996333R844 * Update the Cache with the result https://github.com/dotCMS/core/pull/29021/files#diff-bf828c747c99cefe73af7cec527db7f7e64bd4a66ba54a55eb1170f6a2996333R863 We already have a method to find the ALL the categories and it update the cache after execute the query, so I created a Util method to share the code. https://github.com/dotCMS/core/pull/29021/files#diff-bf828c747c99cefe73af7cec527db7f7e64bd4a66ba54a55eb1170f6a2996333R168 ### Checklist - [ ] Tests - [ ] Translations - [ ] Security Implications Contemplated (add notes if applicable) ### Additional Info ** any additional useful context or info ** ### Screenshots Original | Updated :-------------------------:|:-------------------------: ** original screenshot ** | ** updated screenshot ** --- .../categories/business/CategoryFactory.java | 70 ++++ .../business/CategoryFactoryImpl.java | 87 ++++- .../business/CategoryFactoryTest.java | 319 ++++++++++++++++++ 3 files changed, 462 insertions(+), 14 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactory.java b/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactory.java index 1018149472a7..01d1e34a71af 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactory.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactory.java @@ -1,9 +1,16 @@ package com.dotmarketing.portlets.categories.business; +import java.util.Collection; import java.util.List; +import com.dotcms.util.PaginationUtil; +import com.dotcms.util.pagination.OrderDirection; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.portlets.categories.model.Category; +import com.liferay.portal.model.User; + +import javax.ws.rs.DefaultValue; +import javax.ws.rs.QueryParam; /** * @@ -273,5 +280,68 @@ public abstract class CategoryFactory { * @throws DotDataException */ abstract protected List getAllChildren(Categorizable parent) throws DotDataException; + + /** + * Return a {@link Category} Collection looking through the entire category tree starting from a specified inode. + * This means the search will begin from the specified inode category and then proceed recursively through its children. + * + * @param searchCriteria Search Criteria + * + * @return List of Category filtered + */ + public abstract Collection findAll(final CategorySearchCriteria searchCriteria) throws DotDataException; + + /** + * Represents Search Criteria for {@link Category} searching, you cans set the follow: + * + * - filter: Value used to filter the Category by, returning only Categories that contain this value in their key, name, or variable name. + * - inode: Entry point on the Category tree to start the searching. + * - orderBy: Field name to order the Category + * - direction: Order by direction, it can be 'ASC' or 'DESC' + */ + public static class CategorySearchCriteria { + final String rootInode; + final String filter; + final String orderBy; + final OrderDirection direction; + + private CategorySearchCriteria (final Builder builder) { + this.rootInode = builder.rootInode; + this.filter = builder.filter; + this.orderBy = builder.orderBy; + this.direction = builder.direction; + } + + public static class Builder { + private String rootInode; + private String filter; + private String orderBy = "category_name"; + private OrderDirection direction = OrderDirection.ASC; + + public Builder rootInode(String rootInode) { + this.rootInode = rootInode; + return this; + } + + public Builder filter(String filter) { + this.filter = filter; + return this; + } + + public Builder orderBy(String orderBy) { + this.orderBy = orderBy; + return this; + } + + public Builder direction(OrderDirection direction) { + this.direction = direction; + return this; + } + + public CategorySearchCriteria build() { + return new CategorySearchCriteria(this); + } + } + } } diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactoryImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactoryImpl.java index 537ed5d8982e..7c732bd68305 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/categories/business/CategoryFactoryImpl.java @@ -21,17 +21,16 @@ import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; import com.dotmarketing.util.VelocityUtil; +import com.liferay.util.StringPool; + import java.io.Serializable; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * @@ -165,15 +164,7 @@ protected List findAll() throws DotDataException { .loadObjectResults(); List categories = convertForCategories(result); - for(final Category category : categories) { - //Updating the cache since we are already loading all the categories - if(catCache.get(category.getInode()) == null) - try { - catCache.put(category); - } catch (DotCacheException e) { - throw new DotDataException(e.getMessage(), e); - } - } + updateCache(categories); return categories; } @@ -843,4 +834,72 @@ protected String suggestVelocityVarName(final String categoryVelVarName) throws throw new DotDataException("Unable to suggest a variable name. Got to:" + var); } + /** + * Default Implementation for {@link CategoryFactory#findAll(CategorySearchCriteria)} + * @param searchCriteria Search Criteria + * + * @return + * @throws DotDataException + */ + public List findAll(final CategorySearchCriteria searchCriteria) + throws DotDataException { + + if (!UtilMethods.isSet(searchCriteria.rootInode) && !UtilMethods.isSet(searchCriteria.filter)) { + return findAll(); + } + + final String query = getFindAllSQLQuery(searchCriteria); + + final DotConnect dc = new DotConnect().setSQL(query); + + if (UtilMethods.isSet(searchCriteria.rootInode) ) { + dc.addObject(searchCriteria.rootInode); + } + + if (UtilMethods.isSet(searchCriteria.filter) ) { + dc.addObject("%" + searchCriteria.filter.toLowerCase() + "%"); + dc.addObject("%" + searchCriteria.filter.toLowerCase() + "%"); + dc.addObject("%" + searchCriteria.filter.toLowerCase() + "%"); + } + + final List categories = convertForCategories(UtilMethods.isSet(searchCriteria.rootInode) ? + dc.loadObjectResults().stream() + .filter(map -> !map.get("inode").equals(searchCriteria.rootInode)) + .collect(Collectors.toList()) : dc.loadObjectResults()); + + updateCache(categories); + return categories; + } + + private static String getFindAllSQLQuery(CategorySearchCriteria searchCriteria) { + final String queryTemplate = "WITH RECURSIVE CategoryHierarchy AS ( " + + "SELECT c.* FROM Category c %s " + + "UNION ALL " + + "SELECT c.* FROM Category c JOIN tree t ON c.inode = t.child JOIN CategoryHierarchy ch ON t.parent = ch.inode " + + ") " + + "SELECT * FROM CategoryHierarchy %s ORDER BY %s %s"; + + final String rootCategoryFilter = UtilMethods.isSet(searchCriteria.rootInode) ? "WHERE c.inode = ?" : StringPool.BLANK; + + final String filterCategories = UtilMethods.isSet(searchCriteria.filter) ? + "WHERE LOWER(category_name) LIKE ? OR " + + "LOWER(category_key) LIKE ? OR " + + "LOWER(category_velocity_var_name) LIKE ?" : StringPool.BLANK; + + final String query = String.format(queryTemplate, rootCategoryFilter, filterCategories, searchCriteria.orderBy, + searchCriteria.direction.toString()); + return query; + } + + private void updateCache(List categories) throws DotDataException { + for(final Category category : categories) { + if(catCache.get(category.getInode()) == null) + try { + catCache.put(category); + } catch (DotCacheException e) { + throw new DotDataException(e.getMessage(), e); + } + } + } + } diff --git a/dotcms-integration/src/test/java/com/dotmarketing/portlets/categories/business/CategoryFactoryTest.java b/dotcms-integration/src/test/java/com/dotmarketing/portlets/categories/business/CategoryFactoryTest.java index 0a9f6b924ea3..adaa4bb53349 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/portlets/categories/business/CategoryFactoryTest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/portlets/categories/business/CategoryFactoryTest.java @@ -1,5 +1,6 @@ package com.dotmarketing.portlets.categories.business; +import static com.dotcms.util.CollectionsUtils.list; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -7,24 +8,46 @@ import static org.junit.Assert.assertTrue; import com.dotcms.IntegrationTestBase; +import com.dotcms.api.vtl.model.DotJSON; +import com.dotcms.cache.DotJSONCacheAddTestCase; +import com.dotcms.datagen.CategoryDataGen; +import com.dotcms.util.IntegrationTestInitService; +import com.dotcms.util.pagination.OrderDirection; +import com.dotmarketing.business.APILocator; import com.dotmarketing.business.FactoryLocator; +import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.categories.model.Category; + +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.liferay.util.StringUtil; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import net.bytebuddy.utility.RandomString; import org.jetbrains.annotations.NotNull; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; /*** * Category Factory Test */ +@RunWith(DataProviderRunner.class) public class CategoryFactoryTest extends IntegrationTestBase { private static CategoryFactory categoryFactory; @BeforeClass public static void prepare() throws Exception { + IntegrationTestInitService.getInstance().init(); categoryFactory = FactoryLocator.getCategoryFactory(); } @@ -276,4 +299,300 @@ public void Test_Has_Dependencies() throws DotDataException { assertTrue(categoryFactory.hasDependencies(root)); } + @DataProvider + public static Object[] findCategoriesFilters() { + final String stringToFilterBy = new RandomString().nextString(); + + return new FilterTestCase[] { + new FilterTestCase(stringToFilterBy, String::toLowerCase), + new FilterTestCase(stringToFilterBy, String::toUpperCase), + new FilterTestCase(stringToFilterBy, (filter) -> + filter.substring(1, filter.length()/2).toLowerCase() + + filter.substring(filter.length()/2).toUpperCase()) + }; + } + + /** + * Method to test: {@link CategoryFactoryImpl#findAll(CategoryFactory.CategorySearchCriteria)} + * When: + * + * - Create a random string to be used as the filter for the test. + * - Create two top-level categories, named topLevelCategory_1 and topLevelCategory_2, and include the filter in their names. + * - For topLevelCategory_1, create four children: + * Include the filter in three of these children: one in the key, one in the name, and one in the variable name. + * The fourth child should not include the filter anywhere. + * - Add a child to the last child of topLevelCategory_1 (the one without the filter) and include the filter in its name. + * Also, create a grandchild and include the filter in its name. + * - Create another child for topLevelCategory_1 and include the filter in the key, name, and variable name. + * - Call the method with the filter + * + * Should: + * + * Return five categories: the three children of topLevelCategory_1 that include the filter and the two grandchildren. + */ + @Test + @UseDataProvider("findCategoriesFilters") + public void getAllCategoriesFiltered(final FilterTestCase filterTestCase) throws DotDataException { + + final String stringToFilterBy = filterTestCase.filter; + + final Category topLevelCategory_1 = new CategoryDataGen().setCategoryName("Top Level Category " + filterTestCase.filter) + .setKey("top_level_categoria") + .setCategoryVelocityVarName("top_level_categoria") + .nextPersisted(); + + final Category childCategory_1 = new CategoryDataGen().setCategoryName("Child Category 1") + .setKey("child_category_1 " + filterTestCase.filter) + .setCategoryVelocityVarName("child_category_1") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_2 = new CategoryDataGen().setCategoryName("Child Category 2") + .setKey("child_category_2") + .setCategoryVelocityVarName("child_category_2 " + filterTestCase.filter) + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_3 = new CategoryDataGen().setCategoryName("Child Category 3 " + filterTestCase.filter) + .setKey("child_category_3") + .setCategoryVelocityVarName("child_category_3") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_4 = new CategoryDataGen().setCategoryName("Child Category 4") + .setKey("child_category_4") + .setCategoryVelocityVarName("child_category_4") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_6 = new CategoryDataGen().setCategoryName(filterTestCase.filter + "Child Category 6") + .setKey("child_category_6") + .setCategoryVelocityVarName("child_category_6") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_7 = new CategoryDataGen().setCategoryName("Child " + filterTestCase.filter + "Category 7") + .setKey("child_category_7") + .setCategoryVelocityVarName("child_category_7") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category grandchildCategory_1 = new CategoryDataGen().setCategoryName("Grand Child Category 1 " + filterTestCase.filter) + .setKey("grand_child_category_1") + .setCategoryVelocityVarName("grand_child_category_1") + .parent(childCategory_4) + .nextPersisted(); + + final Category grandchildCategory_2 = new CategoryDataGen().setCategoryName("Grand Child Category 2 " + filterTestCase.filter) + .setKey("grand_child_category_2") + .setCategoryVelocityVarName("grand_child_category_2") + .parent(grandchildCategory_1) + .nextPersisted(); + + final Category topLevelCategory_2 = new CategoryDataGen().setCategoryName("Top Level Category " + filterTestCase.filter) + .setKey("top_level_category_2") + .setCategoryVelocityVarName("top_level_category_2") + .nextPersisted(); + + final Category childCategory_5 = new CategoryDataGen().setCategoryName("Child Category 5" + filterTestCase.filter) + .setKey("child_category_5 " + filterTestCase.filter) + .setCategoryVelocityVarName("child_category_5 " + filterTestCase.filter) + .parent(topLevelCategory_2) + .nextPersisted(); + + + List categoriesExpected = list(childCategory_1, childCategory_2, childCategory_3, grandchildCategory_1, + grandchildCategory_2, childCategory_6, childCategory_7).stream().map(Category::getInode).collect(Collectors.toList()); + + final CategoryFactory.CategorySearchCriteria categorySearchCriteria = new CategoryFactory.CategorySearchCriteria.Builder() + .filter(filterTestCase.transformToSearch()) + .rootInode(topLevelCategory_1.getInode()) + .build(); + + final List categories = FactoryLocator.getCategoryFactory().findAll(categorySearchCriteria) + .stream().map(Category::getInode).collect(Collectors.toList()); + assertEquals(categoriesExpected.size(), categories.size()); + assertTrue(categories.containsAll(categoriesExpected)); + } + + /** + * Method to test: {@link CategoryFactoryImpl#findAll(CategoryFactory.CategorySearchCriteria)} + * When: call the method with filter and inode equals to null + * Should: return all the Categories + * + * @throws DotDataException + */ + @Test + public void getAllCategoriesWithNullFilterAndInode() throws DotDataException { + final Category topLevelCategory_1 = new CategoryDataGen().setCategoryName("Top Level Category") + .setKey("top_level_categoria") + .setCategoryVelocityVarName("top_level_categoria") + .nextPersisted(); + + final Category childCategory_1 = new CategoryDataGen().setCategoryName("Child Category 1") + .setKey("child_category_1") + .setCategoryVelocityVarName("child_category_1") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_2 = new CategoryDataGen().setCategoryName("Child Category 2") + .setKey("child_category_2") + .setCategoryVelocityVarName("child_category_2") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category grandchildCategory_1 = new CategoryDataGen().setCategoryName("Grand Child Category 1") + .setKey("grand_child_category_1") + .setCategoryVelocityVarName("grand_child_category_1") + .parent(childCategory_2) + .nextPersisted(); + + final CategoryFactory.CategorySearchCriteria categorySearchCriteria = new CategoryFactory.CategorySearchCriteria.Builder() + .build(); + + final Collection categoriesWithFilter = FactoryLocator.getCategoryFactory().findAll(categorySearchCriteria); + final List categoriesWithoutFilter = FactoryLocator.getCategoryFactory().findAll(); + + assertTrue(Objects.deepEquals(categoriesWithoutFilter, categoriesWithFilter)); + } + + /** + * Method to test: {@link CategoryFactoryImpl#findAll(CategoryFactory.CategorySearchCriteria)} + * When: call the method with filter equals to null and inode not null + * Should: return all children Categories + * + * @throws DotDataException + */ + @Test + public void getAllCategoriesWithNullFilter() throws DotDataException { + final Category topLevelCategory_1 = new CategoryDataGen().setCategoryName("Top Level Category 1") + .setKey("top_level_categoria_1") + .setCategoryVelocityVarName("top_level_categoria_1") + .nextPersisted(); + + final Category childCategory_1 = new CategoryDataGen().setCategoryName("Child Category 1") + .setKey("child_category_1") + .setCategoryVelocityVarName("child_category_1") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_2 = new CategoryDataGen().setCategoryName("Child Category 2") + .setKey("child_category_2") + .setCategoryVelocityVarName("child_category_2") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_3 = new CategoryDataGen().setCategoryName("Child Category 3 ") + .setKey("child_category_3") + .setCategoryVelocityVarName("child_category_3") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_4 = new CategoryDataGen().setCategoryName("Child Category 4") + .setKey("child_category_4") + .setCategoryVelocityVarName("child_category_4") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category grandchildCategory_1 = new CategoryDataGen().setCategoryName("Grand Child Category 1") + .setKey("grand_child_category_1") + .setCategoryVelocityVarName("grand_child_category_1") + .parent(childCategory_4) + .nextPersisted(); + + final Category grandchildCategory_2 = new CategoryDataGen().setCategoryName("Grand Child Category 2") + .setKey("grand_child_category_2") + .setCategoryVelocityVarName("grand_child_category_2") + .parent(grandchildCategory_1) + .nextPersisted(); + + final Category topLevelCategory_2 = new CategoryDataGen().setCategoryName("Top Level Category 2") + .setKey("top_level_category_2") + .setCategoryVelocityVarName("top_level_category_2") + .nextPersisted(); + + final Category childCategory_5 = new CategoryDataGen().setCategoryName("Child Category 5") + .setKey("child_category_5") + .setCategoryVelocityVarName("child_category_5") + .parent(topLevelCategory_2) + .nextPersisted(); + + final CategoryFactory.CategorySearchCriteria categorySearchCriteria = new CategoryFactory.CategorySearchCriteria.Builder() + .rootInode(topLevelCategory_1.getInode()) + .build(); + + List categoriesExpected = list(childCategory_1, childCategory_2, childCategory_3, childCategory_4, + grandchildCategory_1, grandchildCategory_2).stream().map(Category::getInode).collect(Collectors.toList()); + + final List categories = FactoryLocator.getCategoryFactory().findAll(categorySearchCriteria) + .stream().map(Category::getInode).collect(Collectors.toList()); + assertEquals(categoriesExpected.size(), categories.size()); + assertTrue(categories.containsAll(categoriesExpected)); + } + + /** + * Method to test: {@link CategoryFactoryImpl#findAll(CategoryFactory.CategorySearchCriteria)} + * When: Create a set of {@link Category} and called the method ordering by key + * Should: return all children Categories ordered + * + * @throws DotDataException + */ + @Test + public void getAllCategoriesFilteredOrdered() throws DotDataException { + final Category topLevelCategory_1 = new CategoryDataGen().setCategoryName("Top Level Category 1") + .setKey("top_level") + .setCategoryVelocityVarName("top_level_categoria_1") + .nextPersisted(); + + final Category childCategory_1 = new CategoryDataGen().setCategoryName("Child Category 1") + .setKey("A") + .setCategoryVelocityVarName("child_category_1") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category childCategory_2 = new CategoryDataGen().setCategoryName("Child Category 2") + .setKey("C") + .setCategoryVelocityVarName("child_category_2") + .parent(topLevelCategory_1) + .nextPersisted(); + + final Category grandchildCategory_1 = new CategoryDataGen().setCategoryName("Grand Child Category 1") + .setKey("B") + .setCategoryVelocityVarName("grand_child_category_1") + .parent(childCategory_2) + .nextPersisted(); + + final CategoryFactory.CategorySearchCriteria categorySearchCriteria = new CategoryFactory.CategorySearchCriteria.Builder() + .orderBy("category_key") + .direction(OrderDirection.ASC) + .rootInode(topLevelCategory_1.getInode()) + .build(); + + final List categoriesInode = FactoryLocator.getCategoryFactory().findAll(categorySearchCriteria) + .stream().map(Category::getInode).collect(Collectors.toList()); + + List categoriesExpected = list(childCategory_1, grandchildCategory_1, childCategory_2).stream() + .map(Category::getInode).collect(Collectors.toList()); + + assertEquals(categoriesExpected.size(), categoriesInode.size()); + + for (int i =0; i < categoriesExpected.size(); i++){ + assertEquals(categoriesExpected.get(i), categoriesInode.get(i)); + } + + } + + private static class FilterTestCase { + private String filter; + private Function transformToSearch; + + public FilterTestCase(final String filter, final Function transformToSearch) { + this.filter = filter; + this.transformToSearch = transformToSearch; + } + + public String transformToSearch(){ + return transformToSearch.apply(filter); + } + } }