Hi @jcarter ,
I have a solution for you! Ideally for your problem here we would have used a query-time join on documents in the content index to search by the container attributes, however there is currently no support for that (hopefully we can add this in the future). I went ahead with the approach of duplicating the container macro value in the child documents in the content index.
Click here to see the full source code integrated into the confluence-devrel-plugin.
Some explanation has been provided below:
Implementing the Extractor2 API:
public class ContainerMacroExtractor implements Extractor2 {
private final static String SCROLL_PLUGIN_KEY = "com.k15t.scroll.scroll-platform:scroll-search-proxy-content-type";
public final static String CONTAINER_MACROS_FIELD_NAME = "containerMacros";
private final Logger log = LoggerFactory.getLogger(ContainerMacroFieldHandler.class);
private XhtmlContent xhtmlContent;
private MacroManager macroManager;
public ContainerMacroExtractor(@ComponentImport XhtmlContent xhtmlContent, @ComponentImport MacroManager macroManager) {
this.xhtmlContent = checkNotNull(xhtmlContent);
this.macroManager = checkNotNull(macroManager);
}
I made sure to import the XhtmlContent
and MacroManager
components so I could get information on what macros existed within a body of text. Then since I wanted to add a new field to the content index for each of the child content (which would be the scroll-search-proxy-content-type), I provided an implementation for extractFields
:
@Override
public Collection<FieldDescriptor> extractFields(Object searchable) {
ImmutableList.Builder<FieldDescriptor> resultBuilder = ImmutableList.builder();
if (searchable instanceof CustomContentEntityObject) {
CustomContentEntityObject customEntity = (CustomContentEntityObject) searchable;
if (customEntity.getPluginModuleKey().equals(SCROLL_PLUGIN_KEY)) {
ContentEntityObject container = customEntity.getContainer();
MacroCollector collector = new MacroCollector(macroManager);
BodyType bodyType = container.getBodyContent().getBodyType();
if (bodyType.equals(XHTML)) {
processXhtml(container, collector);
} else if (bodyType.equals(WIKI)) {
collector.processPotentialWikiMacro(container.getBodyAsString());
}
Function<String, FieldDescriptor> toContainerMacroFieldDescriptor = macroName -> new StringFieldDescriptor(CONTAINER_MACROS_FIELD_NAME, macroName, FieldDescriptor.Store.NO);
collector.getMacroNames().stream().map(toContainerMacroFieldDescriptor).forEach(resultBuilder::add);
}
}
return resultBuilder.build();
}
private void processXhtml(final ContentEntityObject searchableCeo, final MacroDefinitionHandler macroUsageCollector) {
DefaultConversionContext context = new DefaultConversionContext(searchableCeo.toPageContext());
try {
xhtmlContent.handleMacroDefinitions(searchableCeo.getBodyAsString(), context, macroUsageCollector);
} catch (XhtmlException ex) {
log.warn("Failed to extracting macro usages on entity [{}] : {}", searchableCeo.getId(), ex.getMessage());
log.debug("Failed to extracting macro usages on entity [{}] : {}", searchableCeo.getId(), ex);
}
}
The MacroCollector
implementation here:
class MacroCollector implements MacroDefinitionHandler, WikiContentHandler {
private Set<String> macroNames;
private MacroManager macroManager;
public MacroCollector(MacroManager macroManager) {
this.macroNames = new HashSet<>();
this.macroManager = checkNotNull(macroManager);
}
@Override
public void handle(MacroDefinition macroDefinition) {
macroNames.add(macroDefinition.getName());
if (macroDefinition.getName().equals(UnmigratedBlockWikiMarkupMacro.MACRO_NAME)) {
processPotentialWikiMacro(macroDefinition.getBodyText());
}
}
@Override
public void handleMacro(StringBuffer stringBuffer, MacroTag macroTag, String body) {
macroNames.add(macroTag.command);
processPotentialWikiMacro(body);
}
@Override
public void handleText(StringBuffer stringBuffer, String s) {
//Do Nothing
}
public Set<String> getMacroNames() {
return macroNames;
}
protected void processPotentialWikiMacro(String wiki) {
WikiMarkupParser parser = new WikiMarkupParser(macroManager, this);
parser.parse(wiki);
}
}
Yes, the process of gathering the macro information is quite messy. We do not store this information explicitly in the database so we can’t get it immediately from ContentEntityObject
. Nor do we provide an abstraction of this macro collection process in a component which can be reused. This is in fact a duplication of logic from MacroExtractor
which we have identified as behaviour we might need to refactor in the future.
The only step left is to provide a BaseFieldHandler
implementation to be able to use CQL to search on that newly added field in the content index.
See: Adding a field to CQL to learn how to hook up a BaseFieldHandler
to a field.
For the majority of container attributes you may ever want to duplicate in the index and search by for the children, its generally a lot simpler than this macro example.
If you have any further questions, please do not hesitate to reply.
Kind Regards,
Richard