Content by Label - label attribute logic
Asked by Charles Reinecke:

When using the "label" attribute as follows

label=AA,BB,+CC

It appears to behave as follows.

* must match CC  
  AND
* can match AA, BB or neither

"Neither" is not really what I wanted. I want (AA or BB) AND CC


Answers:
author: David Chan
body: The only way I've found to do this is to actually split the macro up into 2 macros. Although it's a bit more work, it should display as normal. For example, to achieve what you want do:

label=+AA,+CC

label=+BB,+CC,-AA

The reason for the -AA is to prevent duplicate labels.


author: Kitt Hirasaki
body: This is a little out of context, but hopefully these two files will give you a pretty good idea of what I did.

First file, "ORLabelParameter.java":

package com.pixar.confluence.plugins;

import com.atlassian.confluence.macro.query.SearchQueryInterpreter;

import com.atlassian.confluence.macro.query.SearchQueryInterpreterException;

import com.atlassian.confluence.macro.query.params.BooleanQueryFactoryParameter;

import com.atlassian.confluence.macro.MacroExecutionContext;

import com.atlassian.confluence.search.v2.SearchQuery;

import com.atlassian.confluence.search.v2.query.LabelQuery;

import com.atlassian.confluence.labels.LabelManager;

import com.atlassian.confluence.labels.LabelParser;

import com.atlassian.spring.container.ContainerManager;

/\*\*

\* Presents label parameters as {@link com.atlassian.confluence.macro.query.BooleanQueryFactory} implementations.

\*

\* @since 2.9

\*/

public class ORLabelParameter extends BooleanQueryFactoryParameter

{

private static final String\[\] DEFAULT_PARAM_NAMES = { "orlabel", "orlabels" };

private LabelManager labelManager;

public ORLabelParameter()

{

this(null);

}

public ORLabelParameter(String defaultValue)

{

super(DEFAULT_PARAM_NAMES, defaultValue);

ContainerManager.autowireComponent(this);

}

static class Interpreter implements SearchQueryInterpreter

{

private boolean shouldValidate;

private LabelManager labelManager;

public void setShouldValidate(boolean shouldValidate)

{

this.shouldValidate = shouldValidate;

}

public void setLabelManager(LabelManager labelManager)

{

this.labelManager = labelManager;

}

public SearchQuery createSearchQuery(String value) throws SearchQueryInterpreterException

{

if (shouldValidate)

{

if (labelManager.getLabel(value) == null)

{

throw new SearchQueryInterpreterException("'" + value + "' is not an existing label");

}

}

/\*\*

\* CONF-15398. We want to report invalid labels even if validation is suppressed using shouldValidate = false,

\* as you cannot make a valid search query out of an invalid label.

\*

\* Ideally this validation should performed on the value passed to createSearchQuery() before it is invoked.

\*/

if (LabelParser.parse(value) == null)

throw new SearchQueryInterpreterException("'" + value + "' is an invalid label.");

return new LabelQuery(value);

}

}

@Override

protected SearchQueryInterpreter createSearchQueryInterpreter(MacroExecutionContext ctx)

{

Interpreter interpreter = new Interpreter();

interpreter.setShouldValidate(shouldValidate);

interpreter.setLabelManager(labelManager);

return interpreter;

}

public void setLabelManager(LabelManager labelManager)

{

this.labelManager = labelManager;

}

}  
Second file, LabelledContentMacro.java:  
package com.pixar.confluence.plugins;  
import com.atlassian.bonnie.Searchable;  
import com.atlassian.confluence.core.ConfluenceActionSupport;  
import com.atlassian.confluence.core.SpaceContentEntityObject;  
import com.atlassian.confluence.macro.ContentFilteringMacro;  
import com.atlassian.confluence.macro.MacroExecutionContext;  
import com.atlassian.confluence.macro.params.ParameterException;  
import com.atlassian.confluence.macro.query.BooleanQueryFactory;  
import com.atlassian.confluence.macro.query.InclusionCriteria;  
import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;  
import com.atlassian.confluence.search.v2.\*;  
import com.atlassian.confluence.search.v2.filter.SubsetResultFilter;  
import com.atlassian.confluence.search.v2.query.BooleanQuery;  
import com.atlassian.confluence.search.v2.query.LabelQuery;  
import com.atlassian.confluence.search.v2.query.CreatorQuery;  
import com.atlassian.confluence.search.v2.query.InSpaceQuery;  
import com.atlassian.confluence.search.v2.searchfilter.ContentPermissionsSearchFilter;  
import com.atlassian.confluence.search.v2.searchfilter.SpacePermissionsSearchFilter;  
import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;  
import com.atlassian.confluence.util.ContentMacrosHelper;  
import com.atlassian.confluence.util.GeneralUtil;  
import com.atlassian.confluence.util.velocity.VelocityUtils;  
import com.atlassian.renderer.v2.RenderMode;  
import com.atlassian.renderer.v2.macro.MacroException;  
import com.atlassian.spring.container.ContainerManager;  
import org.apache.commons.lang.StringEscapeUtils;  
import org.apache.commons.lang.StringUtils;  
import org.apache.log4j.Logger;  
import com.atlassian.confluence.pages.AbstractPage;  
import com.atlassian.confluence.pages.DefaultPageManager;  
import com.atlassian.confluence.pages.PageManager;  
import java.util.ArrayList;  
import java.util.List;  
import java.util.Map;  
import java.util.Set;  
import java.util.Vector;  
import java.util.Collections;  
public class LabelledContentMacro extends ContentFilteringMacro  
{  
private static final Logger log = Logger.getLogger(LabelledContentMacro.class);  
private static final String TEMPLATE_NAME = "com/pixar/confluence/plugins/docfilters/labelledcontent.vm";  
private static final String SPACE_KEY_BC_PARAM = "key";  
private static final String SPACE_CURRENT = "@self";  
private static final String SPACE_ALL = "@all";  
private static final String OPERATOR = "operator";  
private static final String OPERATOR_AND = "and";  
private static final String DEPT_PREFIX = "dept=";  
private static final String DEPT_ALL = DEPT_PREFIX + "all";  
private static final String SHOW_PREFIX = "show=";  
private static final String SHOW_ALL = SHOW_PREFIX + "all";  
private ContentMacrosHelper contentMacrosHelper;  
private ConfluenceActionSupport confluenceActionSupport;  
protected final ORLabelParameter orlabelParam;  
public class DocTableOutput  
{  
public int type;  
public Searchable content;  
public String spaceKey;  
public int getType() {  
return type;  
}  
public Searchable getContent() {  
return content;  
}  
public String getSpaceKey() {  
return spaceKey;  
}  
}  
public LabelledContentMacro()  
{  
super();  
labelParam.setValidate(false); // CONF-14235 - this macro shouldn't try to validate labels  
orlabelParam = new ORLabelParameter();  
}  
public boolean isInline()  
{  
return false;  
}  
public boolean hasBody()  
{  
return false;  
}  
public RenderMode getBodyRenderMode()  
{  
return RenderMode.NO_RENDER;  
}  
@Override  
public String execute(MacroExecutionContext ctx) throws MacroException  
{  
return executeSearch(ctx);  
}  
public String executeSearch(MacroExecutionContext ctx) throws MacroException  
{  
Map\<String, Object\> contextMap = getMacroVelocityContext();  
Map\<String, String\> parameters = ctx.getParams();  
String title = parameters.get("title");  
BooleanQueryFactory queryFactory = new BooleanQueryFactory();  
// CONF-12749 Limit the label links to a single space if we only search a single space  
boolean limitLabelLinksToSpace = false;  
// parse the space(s) parameter  
spaceKeyParam.addParameterAlias("key");  
// CONF-14023 - default to searching all spaces  
spaceKeyParam.setDefaultValue(SPACE_ALL);  
BooleanQueryFactory spaceKeyQuery = spaceKeyParam.findValue(ctx);  
if (spaceKeyQuery != null)  
{  
queryFactory.addMust(spaceKeyQuery.toBooleanQuery());  
limitLabelLinksToSpace = isSingleSpace(spaceKeyQuery);  
}  
// use the "labels" parameter from ContentFilteringMacro  
labelParam.addParameterAlias("0");  
if (parameters.containsKey(OPERATOR) \&\& parameters.get(OPERATOR).equalsIgnoreCase(OPERATOR_AND))  
{  
labelParam.setDefaultInclusionCriteria(InclusionCriteria.ALL);  
}  
else  
{  
labelParam.setDefaultInclusionCriteria(InclusionCriteria.ANY);  
}  
BooleanQueryFactory labelQuery = labelParam.findValue(ctx);  
if (labelQuery != null)  
{  
queryFactory.addMust(labelQuery.toBooleanQuery());  
}  
else  
{  
throw new MacroException(  
getConfluenceActionSupport().getText("contentbylabel.error.label-parameter-required")  
);  
}  
List\<String\> labelNames = new ArrayList\<String\>();  
// we need a list of label names to display in case no content comes back.  
// For now, we're going to just stick the label string into a single-element list.  
labelNames.add(labelParam.getParameterValue(parameters));  
contextMap.put("labelNames", labelNames);  
String orlabelString = orlabelParam.getParameterValue(parameters);  
if (orlabelString != null \&\& !orlabelString.isEmpty()) {  
String\[\] orlabelNames = StringUtils.split(orlabelString, ',');  
if (orlabelNames.length \> 0) {  
Vector\<LabelQuery\> orqueries = new Vector\<LabelQuery\>();  
for (String orlName : orlabelNames) {  
orqueries.add(new LabelQuery(orlName));  
}  
SearchQuery orQuery;  
if (orqueries.size() \> 1) {  
orQuery = orqueries.remove(0);  
while (orqueries.size() \> 0) {  
orQuery = BooleanQuery.composeOrQuery(orQuery, orqueries.remove(0));  
}  
}  
else {  
orQuery = orqueries.get(0);  
}  
queryFactory.addMust(orQuery);  
}  
}  
String myDept = FilterUtils.getDept();  
SearchQuery deptQuery;  
LabelQuery myDeptQuery;  
LabelQuery allDeptQuery = new LabelQuery(DEPT_ALL);  
if (myDept != null \&\& myDept != "")  
{  
myDeptQuery = new LabelQuery(DEPT_PREFIX + myDept);  
deptQuery = BooleanQuery.composeOrQuery(myDeptQuery, allDeptQuery);  
}  
else  
{  
deptQuery = allDeptQuery;  
}  
if (deptQuery != null)  
{  
if (!myDept.equals("all")) {  
queryFactory.addMust(deptQuery);  
}  
}  
else  
{  
throw new MacroException(  
getConfluenceActionSupport().getText("contentbylabel.error.no-dept")  
);  
}  
String myShow = FilterUtils.getShow();  
SearchQuery showQuery;  
LabelQuery myShowQuery;  
LabelQuery allShowQuery = new LabelQuery(SHOW_ALL);  
if (myShow != null \&\& myShow != "")  
{  
myShowQuery = new LabelQuery(SHOW_PREFIX + myShow);  
showQuery = BooleanQuery.composeOrQuery(myShowQuery, allShowQuery);  
}  
else  
{  
showQuery = allShowQuery;  
}  
if (showQuery != null)  
{  
if (!myShow.equals("all")) {  
queryFactory.addMust(showQuery);  
}  
}  
else  
{  
throw new MacroException(  
getConfluenceActionSupport().getText("contentbylabel.error.no-show")  
);  
}  
// handle the other parameters.  
SubsetResultFilter subsetResultFilter;  
maxResultsParam.addParameterAlias("maxResults");  
maxResultsParam.setDefaultValue(ContentFilteringMacro.DEFAULT_MAX_RESULTS);  
try  
{  
Integer maxResults = maxResultsParam.findValue(ctx);  
contextMap.put("maxResults", maxResults);  
subsetResultFilter = new SubsetResultFilter(maxResults);  
}  
catch (ParameterException pe)  
{  
throw new MacroException(  
getConfluenceActionSupport().getText("contentbylabel.error.parse-max-labels-param"),  
pe);  
}  
SearchSort searchSort;  
try  
{  
searchSort = sortParam.findValue(ctx);  
}  
catch (ParameterException pe)  
{  
throw new MacroException(  
getConfluenceActionSupport().getText("contentbylabel.error.parse-reverse-or-sort-param"),  
pe);  
}  
try  
{  
BooleanQueryFactory contentTypeQuery = contentTypeParam.findValue(ctx);  
if (contentTypeQuery != null)  
{  
queryFactory.addMust(contentTypeQuery.toBooleanQuery());  
}  
}  
catch (ParameterException pe)  
{  
throw new MacroException(  
getConfluenceActionSupport().getText(  
"contentbylabel.error.parse-types-param",  
new String\[\] { StringEscapeUtils.escapeHtml(pe.getMessage()) }  
),  
pe  
);  
}  
// parse author parameter  
Set\<String\> authors = authorParam.findValue(ctx);  
if (!authors.isEmpty())  
{  
BooleanQueryFactory authorQueryFactory = new BooleanQueryFactory();  
for (String author : authors)  
{  
authorQueryFactory.addShould(new CreatorQuery(author));  
}  
queryFactory.addMust(authorQueryFactory.toBooleanQuery());  
}  
BooleanQuery query = queryFactory.toBooleanQuery();  
final SearchFilter searchFilter = ContentPermissionsSearchFilter.getInstance().and(SpacePermissionsSearchFilter.getInstance());  
ISearch search = new ContentSearch(query, searchSort, searchFilter, subsetResultFilter);  
SearchResults searchResults;  
try  
{  
searchResults = searchManager.search(search);  
}  
catch (InvalidSearchException ise)  
{  
throw new MacroException(getConfluenceActionSupport().getText("contentbylabel.error.run-search"), ise);  
}  
List\<Searchable\> rawcontents = searchManager.convertToEntities(searchResults, true);  
// Do not include our enclosing page in the list of results.  
long pageId = ctx.getContent().getId();  
Vector\<DocTableOutput\> contents = new Vector\<DocTableOutput\>();  
/\*  
Vector\<DocTableOutput\> contentsPixdoc = new Vector\<DocTableOutput\>();  
Vector\<DocTableOutput\> contentsProd = new Vector\<DocTableOutput\>();  
\*/  
/\*  
DocTableOutput headerCell = new DocTableOutput();  
headerCell.content = null;  
headerCell.spaceKey = "";  
headerCell.type = 0;  
contentsProd.add(headerCell);  
\*/  
PageManager pageManager = (PageManager) ContainerManager.getComponent("pageManager");  
for( Searchable content: rawcontents) {  
if (content.getId() != pageId) {  
DocTableOutput output = new DocTableOutput();  
output.content = content;  
String spaceKey = pageManager.getAbstractPage(content.getId()).getSpaceKey();  
output.spaceKey = spaceKey;  
if (spaceKey.equals("pixdoc")) {  
output.type = 2;  
//contentsPixdoc.add(output);  
} else {  
output.type = 1;  
//contentsProd.add(output);  
}  
contents.add(output);  
}  
}  
/\*  
if(contentsProd.size() \> 0) {  
contents.addAll(contentsProd);  
}  
contents.addAll(contentsPixdoc);  
\*/  
// If our results contain more than ten items, split them between two columns.  
List\<DocTableOutput\> contentsA;  
List\<DocTableOutput\> contentsB;  
int lenA = (contents.size() / 2) + (contents.size() % 2);  
contentsA = contents.subList(0, lenA);  
contentsB = contents.subList(lenA, contents.size());  
/\*  
if (contents.size() \> 10) {  
int lenA = (contents.size() / 2) + (contents.size() % 2);  
contentsA = contents.subList(0, lenA);  
contentsB = contents.subList(lenA, contents.size());  
}  
else  
{  
contentsA = contents;  
contentsB = contents.subList(0,0);  
contentsB.clear();  
}  
\*/  
contextMap.put("title", title);  
contextMap.put("contentsA", contentsA);  
contextMap.put("contentsB", contentsB);  
contextMap.put("unfilteredResultsCnt", searchResults.getUnfilteredResultsCount());  
contextMap.put("showLabels", getBooleanParameter(parameters.get("showLabels"), true));  
contextMap.put("showSpace", getBooleanParameter(parameters.get("showSpace"), true));  
contextMap.put("limitLabelLinksToSpace", limitLabelLinksToSpace);  
contextMap.put("contentMacrosHelper", contentMacrosHelper);  
contextMap.put("pageContext", ctx.getPageContext());  
contextMap.put("showExcerpt", Boolean.valueOf(parameters.get("excerpt")));  
contextMap.put("embedTable", getBooleanParameter(parameters.get("embedTable"), false));  
return render(contextMap);  
}  
/\* We assume that the query applies to a single space if there's  
\* only one component and it is an InSpace query type.  
\*  
\* TODO This probably belongs in the SpaceQuery class  
\*/  
private boolean isSingleSpace(BooleanQueryFactory spaceKeyQuery)  
{  
boolean limitLabelLinksToSpace = false;  
List params = spaceKeyQuery.toBooleanQuery().getParameters();  
if(params.size() == 1 \&\& (params.get(0) instanceof InSpaceQuery) )  
{  
limitLabelLinksToSpace = true;  
}  
return limitLabelLinksToSpace;  
}  
/// CLOVER:OFF  
@SuppressWarnings("unchecked")  
protected Map\<String, Object\> getMacroVelocityContext()  
{  
// current MacroUtils returns untyped map, but its keys are always  
// strings and we don't want to restrict what it can hold  
return MacroUtils.defaultVelocityContext();  
}  
protected String render(Map\<String, Object\> contextMap)  
{  
return VelocityUtils.getRenderedTemplate(TEMPLATE_NAME, contextMap);  
}  
///CLOVER:ON  
private Boolean getBooleanParameter(String booleanValue, boolean defaultValue)  
{  
if (StringUtils.isNotBlank(booleanValue))  
return Boolean.valueOf(booleanValue);  
else  
return defaultValue;  
}  
public void setContentMacrosHelper(ContentMacrosHelper contentMacrosHelper)  
{  
this.contentMacrosHelper = contentMacrosHelper;  
}  
protected ConfluenceActionSupport getConfluenceActionSupport()  
{  
if (null == confluenceActionSupport)  
confluenceActionSupport = GeneralUtil.newWiredConfluenceActionSupport();  
return confluenceActionSupport;  
}  
}

comments: author: Joe Clark
body: Thanks so much for sharing your code - I'll see if we can get these enhancements contributed back to Confluence - if you don't have any objections?


author: Joerg Bencke
body: Has anybody extended this to filter "pagetreeonly" vs. "global search" ?


author: Kitt Hirasaki
body: Sure, if they're helpful, go for it!


author: Kitt Hirasaki
body: I wanted to do the same thing and ended up creating my own hacked-up version of the macro.

comments: author: Andreas van Rienen
body: Kitt - is there a chance that you share the code of your modified macro. In my opinion more advanced AND/OR "queries" are a useful feature for more advanced requirements ... Thanks for a brief reply ...