Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Content Fragment List] Add "All Tag" match ability #2382

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -15,7 +15,15 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment;

import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
Expand All @@ -25,15 +33,12 @@
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -64,62 +69,60 @@
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ContentFragmentListImpl extends AbstractComponentImpl implements ContentFragmentList {

/**
* The default logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(ContentFragmentListImpl.class);

/**
* Resource type for V1 component.
*/
public static final String RESOURCE_TYPE_V1 = "core/wcm/components/contentfragmentlist/v1/contentfragmentlist";

/**
* Resource type for V2 component.
*/
public static final String RESOURCE_TYPE_V2 = "core/wcm/components/contentfragmentlist/v2/contentfragmentlist";

/**
* Root path of the DAM.
*/
public static final String DEFAULT_DAM_PARENT_PATH = "/content/dam";

/**
* Default maximum number of items to return (-1 means all).
*/
public static final int DEFAULT_MAX_ITEMS = -1;

/**
* Default tag match requirement.
*/
private static final String TAGS_MATCH_ANY_VALUE = "any";

/**
* The request.
*/
@Self(injectionStrategy = InjectionStrategy.REQUIRED)
private SlingHttpServletRequest slingHttpServletRequest;

/**
* Content type converter service.
*/
@Inject
private ContentTypeConverter contentTypeConverter;

@SlingObject
private ResourceResolver resourceResolver;

@ValueMapValue(name = ContentFragmentList.PN_MODEL_PATH, injectionStrategy = InjectionStrategy.OPTIONAL)
@Nullable
private String modelPath;

@ValueMapValue(name = ContentFragmentList.PN_ELEMENT_NAMES, injectionStrategy = InjectionStrategy.OPTIONAL)
@Nullable
private String[] elementNames;

@ValueMapValue(name = ContentFragmentList.PN_TAG_NAMES, injectionStrategy = InjectionStrategy.OPTIONAL)
@Nullable
private String[] tagNames;

@ValueMapValue(name = ContentFragmentList.PN_PARENT_PATH, injectionStrategy = InjectionStrategy.OPTIONAL)
@Nullable
private String parentPath;

@ValueMapValue(name = ContentFragmentList.PN_MAX_ITEMS, injectionStrategy = InjectionStrategy.OPTIONAL)
@Default(intValues = DEFAULT_MAX_ITEMS)
private int maxItems;

@ValueMapValue(name = ContentFragmentList.PN_ORDER_BY, injectionStrategy = InjectionStrategy.OPTIONAL)
@Default(values = JcrConstants.JCR_CREATED)
private String orderBy;

@ValueMapValue(name = ContentFragmentList.PN_SORT_ORDER, injectionStrategy = InjectionStrategy.OPTIONAL)
@Default(values = Predicate.SORT_ASCENDING)
private String sortOrder;

/**
* List of content fragment items.
*/
private final List<DAMContentFragment> items = new ArrayList<>();

@PostConstruct
private void initModel() {
// Default path limits search to DAM
if (StringUtils.isEmpty(parentPath)) {
parentPath = DEFAULT_DAM_PARENT_PATH;
}
ResourceResolver resourceResolver = this.request.getResourceResolver();
ValueMap properties = this.request.getResource().getValueMap();

if (StringUtils.isEmpty(modelPath)) {
String modelPath = properties.get(ContentFragmentList.PN_MODEL_PATH, String.class);
if (StringUtils.isBlank(modelPath)) {
LOG.warn("Please provide a model path");
return;
}
Expand All @@ -137,30 +140,33 @@ private void initModel() {
}

Map<String, String> queryParameterMap = new HashMap<>();
queryParameterMap.put("path", parentPath);
queryParameterMap.put("path", Optional.ofNullable(properties.get(ContentFragmentList.PN_PARENT_PATH, String.class))
.filter(StringUtils::isNotEmpty)
.orElse(DEFAULT_DAM_PARENT_PATH));
queryParameterMap.put("type", NT_DAM_ASSET);
queryParameterMap.put("p.limit", Integer.toString(maxItems));
queryParameterMap.put("p.limit", Integer.toString(properties.get(ContentFragmentList.PN_MAX_ITEMS, DEFAULT_MAX_ITEMS)));
queryParameterMap.put("1_property", JcrConstants.JCR_CONTENT + "/data/cq:model");
queryParameterMap.put("1_property.value", modelPath);

if (StringUtils.isNotEmpty(orderBy)) {
queryParameterMap.put("orderby", "@" + orderBy);
if (StringUtils.isNotEmpty(sortOrder)) {
queryParameterMap.put("orderby.sort", sortOrder);
}
}
queryParameterMap.put("orderby", "@" + Optional.ofNullable(properties.get(ContentFragmentList.PN_ORDER_BY, String.class))
.filter(StringUtils::isNotBlank)
.orElse(JcrConstants.JCR_CREATED));
queryParameterMap.put("orderby.sort", Optional.ofNullable(properties.get(ContentFragmentList.PN_SORT_ORDER, String.class))
.filter(StringUtils::isNotBlank)
.orElse(Predicate.SORT_ASCENDING));

ArrayList<String> allTags = new ArrayList<>();
if (tagNames != null && tagNames.length > 0) {
allTags.addAll(Arrays.asList(tagNames));
}

List<String> allTags = Optional.ofNullable(properties.get(ContentFragmentList.PN_TAG_NAMES, String[].class))
.filter(array -> array.length > 0)
.map(Arrays::asList)
.orElseGet(Collections::emptyList);

if (!allTags.isEmpty()) {
// Check for the taggable mixin
queryParameterMap.put("2_property", JcrConstants.JCR_CONTENT + "/metadata/" + JcrConstants.JCR_MIXINTYPES);
queryParameterMap.put("2_property.value", TagConstants.NT_TAGGABLE);
// Check for the actual tags (by default, tag are or'ed)
queryParameterMap.put("tagid.property", JcrConstants.JCR_CONTENT + "/metadata/cq:tags");
queryParameterMap.put("tagid.and", Boolean.toString(!properties.get(PN_TAGS_MATCH, TAGS_MATCH_ANY_VALUE).equals(TAGS_MATCH_ANY_VALUE)));
for (int i = 0; i < allTags.size(); i++) {
queryParameterMap.put(String.format("tagid.%d_value", i + 1), allTags.get(i));
}
Expand All @@ -176,6 +182,9 @@ private void initModel() {
// Query builder has a leaking resource resolver, so the following work around is required.
ResourceResolver leakingResourceResolver = null;
try {

String[] elementNames = properties.get(ContentFragmentList.PN_ELEMENT_NAMES, String[].class);

// Iterate over the hits if you need special information
Iterator<Resource> resourceIterator = searchResult.getResources();
while (resourceIterator.hasNext()) {
Expand All @@ -185,10 +194,14 @@ private void initModel() {
leakingResourceResolver = resource.getResourceResolver();
}

DAMContentFragment contentFragmentModel = new DAMContentFragmentImpl(
resource, contentTypeConverter, null, elementNames);

items.add(contentFragmentModel);
// re-resolve the resource so that no references to the leaking resource resolver are retained
Resource currentSessionResource = resourceResolver.getResource(resource.getPath());
if (currentSessionResource != null) {
DAMContentFragment contentFragmentModel = new DAMContentFragmentImpl(
resource, contentTypeConverter, null, elementNames);
items.add(contentFragmentModel);
}
}
} finally {
if (leakingResourceResolver != null) {
Expand Down
Expand Up @@ -61,6 +61,14 @@ public interface ContentFragmentList extends Component {
*/
String PN_TAG_NAMES = "tagNames";

/**
* Name of the resource property indicating if the matching against tags can accept any tag from the tag list. The accepted value is
* <code>any</code>.
*
* @since com.adobe.cq.wcm.core.components.models.contentfragment 1.6.0
*/
String PN_TAGS_MATCH = "tagsMatch";

/**
* Name of the optional resource property that stores the parent path of the content fragments.
*
Expand Down
Expand Up @@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
@Version("1.5.1")
@Version("1.6.0")
package com.adobe.cq.wcm.core.components.models.contentfragment;

import org.osgi.annotation.versioning.Version;
Expand Up @@ -17,15 +17,19 @@

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.jcr.Session;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.tagging.TagConstants;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatchers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import com.adobe.cq.wcm.core.components.Utils;
Expand All @@ -39,6 +43,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;

@ExtendWith(AemContextExtension.class)
Expand Down Expand Up @@ -88,7 +93,7 @@ void setUp() throws NoSuchFieldException, IllegalAccessException {
when(iterator.next()).thenReturn(resource);
when(resource.getResourceResolver()).thenReturn(leakingResourceResolverMock);
when(queryBuilderMock.createQuery(Mockito.any(PredicateGroup.class), Mockito.any(Session.class)))
.thenReturn(query);
.thenReturn(query);
}

@Test
Expand Down Expand Up @@ -145,8 +150,11 @@ void verifyQueryBuilderInteractionWhenNonExistingModelIsGiven() {
expectedPredicates.put("path", ImmutableMap.of("path", ContentFragmentListImpl.DEFAULT_DAM_PARENT_PATH));
expectedPredicates.put("type", ImmutableMap.of("type", "dam:Asset"));
expectedPredicates.put("1_property", ImmutableMap.of(
"property", "jcr:content/data/cq:model",
"value", "foobar"));
"property", "jcr:content/data/cq:model",
"value", "foobar"));
expectedPredicates.put("orderby", ImmutableMap.of(
"orderby", "@" + JcrConstants.JCR_CREATED,
"sort", Predicate.SORT_ASCENDING));

// WHEN
getModelInstanceUnderTest(NON_EXISTING_MODEL);
Expand All @@ -163,11 +171,11 @@ void verifyQueryBuilderInteractionWhenOrderByIsGiven() {
expectedPredicates.put("path", ImmutableMap.of("path", ContentFragmentListImpl.DEFAULT_DAM_PARENT_PATH));
expectedPredicates.put("type", ImmutableMap.of("type", "dam:Asset"));
expectedPredicates.put("1_property", ImmutableMap.of(
"property", "jcr:content/data/cq:model",
"value", "foobar"));
"property", "jcr:content/data/cq:model",
"value", "foobar"));
expectedPredicates.put("orderby", ImmutableMap.of(
"orderby", "@main",
"sort", "desc"));
"orderby", "@main",
"sort", "desc"));


// WHEN
Expand Down Expand Up @@ -195,11 +203,17 @@ void verifyQueryBuilderInteractionWhenPathParameterAndTagsAreGiven() {
expectedPredicates.put("path", ImmutableMap.of("path", "/content/dam/some-other-parent-path"));
expectedPredicates.put("type", ImmutableMap.of("type", "dam:Asset"));
expectedPredicates.put("1_property", ImmutableMap.of(
"property", "jcr:content/data/cq:model",
"value", "foobar"));
"property", "jcr:content/data/cq:model",
"value", "foobar"));
expectedPredicates.put("2_property", ImmutableMap.of(
"property", "jcr:content/metadata/jcr:mixinTypes",
"value", TagConstants.NT_TAGGABLE));
expectedPredicates.put("tagid", ImmutableMap.of(
"property", "jcr:content/metadata/cq:tags",
"1_value", "quux"));
"property", "jcr:content/metadata/cq:tags",
"1_value", "quux"));
expectedPredicates.put("orderby", ImmutableMap.of(
"orderby", "@" + JcrConstants.JCR_CREATED,
"sort", Predicate.SORT_ASCENDING));

// WHEN
getModelInstanceUnderTest(NON_EXISTING_MODEL_WITH_PATH_AND_TAGS);
Expand All @@ -216,8 +230,11 @@ void verifyQueryBuilderInteractionWhenMaxLimitIsGiven() {
expectedPredicates.put("path", ImmutableMap.of("path", ContentFragmentListImpl.DEFAULT_DAM_PARENT_PATH));
expectedPredicates.put("type", ImmutableMap.of("type", "dam:Asset"));
expectedPredicates.put("1_property", ImmutableMap.of(
"property", "jcr:content/data/cq:model",
"value", "foobar"));
"property", "jcr:content/data/cq:model",
"value", "foobar"));
expectedPredicates.put("orderby", ImmutableMap.of(
"orderby", "@" + JcrConstants.JCR_CREATED,
"sort", Predicate.SORT_ASCENDING));

//Expected Max Limit
String expectedLimit = "20";
Expand All @@ -234,26 +251,38 @@ void verifyQueryBuilderInteractionWhenMaxLimitIsGiven() {
* {@link com.day.cq.search.QueryBuilder#createQuery(PredicateGroup, Session)} call.
*/
private void verifyPredicateGroup(final Map<String, Map<String, String>> expectedPredicates, String expectedLimit) {
Mockito.verify(queryBuilderMock).createQuery(ArgumentMatchers.argThat(argument -> {
ArgumentCaptor<PredicateGroup> captor = ArgumentCaptor.forClass(PredicateGroup.class);
Mockito.verify(queryBuilderMock, times(1)).createQuery(captor.capture(), Mockito.any(Session.class));

//Check the result limit
String actualLimit = argument.get(PredicateGroup.PARAM_LIMIT);
if (actualLimit == null || !actualLimit.equals(expectedLimit)) {
return false;
}
PredicateGroup capturedPredicateGroup = captor.getValue();

//Check the result limit
String actualLimit = capturedPredicateGroup.get(PredicateGroup.PARAM_LIMIT);
assertNotNull(actualLimit, "Expected predicate group limit to be non-null.");
assertEquals(expectedLimit, actualLimit, "Predicate group limit is not the expected value.");

for (String predicateName : expectedPredicates.keySet()) {
Predicate predicate = argument.getByName(predicateName);
for (String predicateParameterName : expectedPredicates.get(predicateName).keySet()) {
String predicateParameterValue = predicate.getParameters().get(predicateParameterName);
String expectedPredicateParameterValue =
expectedPredicates.get(predicateName).get(predicateParameterName);
if (!predicateParameterValue.equals(expectedPredicateParameterValue)) {
return false;
}
}
// check all expected predicates exist
for (String predicateName : expectedPredicates.keySet()) {
Predicate predicate = capturedPredicateGroup.getByName(predicateName);
assertNotNull(predicate, "The captured predicate group does not include the predicate \"" + predicateName + "\"");

for (String predicateParameterName : expectedPredicates.get(predicateName).keySet()) {
String predicateParameterValue = predicate.getParameters().get(predicateParameterName);
assertNotNull(predicateParameterValue, "The predicate \"" + predicateName + "\" does not include the parameter \"" + predicateParameterName);

String expectedPredicateParameterValue = expectedPredicates.get(predicateName).get(predicateParameterName);
assertEquals(expectedPredicateParameterValue, predicateParameterValue, "predicate parameter is incorrect for \"" + predicateName + "[" + predicateParameterName + "]\"");
}
return true;
}), Mockito.any(Session.class));
}

// check for any unexpected predicates
List<String> extraPredicates = capturedPredicateGroup.subList(0, capturedPredicateGroup.size()).stream()
.map(Predicate::getName)
.filter(p -> !expectedPredicates.containsKey(p))
.collect(Collectors.toList());
assertEquals(
0,
extraPredicates.size(),
"The following predicates were found, but were not expected: [" + String.join(", ", extraPredicates) + "]");
}
}