Skip to content

Nested Documents UI — Phase 2: Virtual Catalogue Config, SearchWrapper & CatalogueSearch #3662

@luis100

Description

@luis100

Overview

Add config-driven "virtual catalogue" entries to the search dropdown. A virtual catalogue is a named search context that queries Solr child documents using ChildOfFilterParameter, returning results as IndexedAIP (children carry their specific fields in indexedAip.getFields()). No new Java result type is needed — ConfigurableAsyncTableCell already reads object.getFields().get(c.getField()) for non-default columns.

Note

Key architectural insight: Child documents returned by ChildOfFilterParameter are deserialized as IndexedAIP objects. Mandatory AIP fields (permissions, ancestors, etc.) come from the parent or are empty/null. Child-specific dynamic Solr fields (e.g. subject_txt, sender_s) appear in indexedAip.getFields(). ConfigurableAsyncTableCell.configureDisplay() at line ~337 already calls object.getFields().get(c.getField()) for any non-default_ column — meaning the email-specific columns work through existing infrastructure without any new table class.

Warning

SearchWrapper.Components is currently keyed by Class<? extends IsIndexed>. Two virtual catalogues both using IndexedAIP would overwrite each other. The Components inner class must be extended to support a parallel string-keyed map for virtual catalogues (keyed by listId).

Part of: #3382
Depends on: #3660 (Phase 0 — content_type values referenced in config)


How Virtual Catalogues Work

The API call for a virtual catalogue uses ChildOfFilterParameter:

{
  "filter": {
    "parameters": [{
      "type": "ChildOfFilterParameter",
      "childrenOfFilter": {
        "type": "SimpleFilterParameter",
        "name": "content_type",
        "value": "emailarchive"
      }
    }]
  },
  "fieldsToReturn": ["uuid", "subject_txt", "sender_s", "sentDate_dt"]
}

Results are IndexedAIP objects where child-specific fields are in the fields map:

{
  "id": "5f28e162-.../emails#42",
  "fields": {
    "subject_txt": ["Re: Quarterly Report"],
    "sender_s": ["joao.silva@empresa.pt"],
    "sentDate_dt": ["2021-03-15T09:42:00Z"]
  }
}

Clicking a row navigates to browse/{parentUUID} — the parent UUID is extracted from the child UUID by splitting on / and taking the first segment.


Configuration Schema

File: roda-ui/roda-wui/src/main/resources/config/roda-wui.properties

Virtual catalogues are declared at a top-level key and configured under ui.lists.{listId}.* — the same mechanism as Search_AIPs:

# Declare virtual catalogue list IDs
ui.catalogue.virtual = Search_emails

# Dropdown metadata (new sub-keys under ui.lists.{listId})
ui.lists.Search_emails.catalogue.label.i18n     = ui.catalogue.Search_emails.label
ui.lists.Search_emails.catalogue.icon           = fa-envelope
ui.lists.Search_emails.catalogue.childOf.filter = content_type:emailarchive
ui.lists.Search_emails.catalogue.click_action   = browse

# Columns — same syntax as ui.lists.Search_AIPs
ui.lists.Search_emails.columns[] = subject_txt
ui.lists.Search_emails.columns[].subject_txt.field          = subject_txt
ui.lists.Search_emails.columns[].subject_txt.header         = ui.lists.Search_emails.columns.subject_txt
ui.lists.Search_emails.columns[].subject_txt.sortBy         = subject_txt

ui.lists.Search_emails.columns[] = sender_s
ui.lists.Search_emails.columns[].sender_s.field             = sender_s
ui.lists.Search_emails.columns[].sender_s.header            = ui.lists.Search_emails.columns.sender_s

ui.lists.Search_emails.columns[] = sentDate_dt
ui.lists.Search_emails.columns[].sentDate_dt.field          = sentDate_dt
ui.lists.Search_emails.columns[].sentDate_dt.header         = ui.lists.Search_emails.columns.sentDate_dt
ui.lists.Search_emails.columns[].sentDate_dt.renderingHint  = DATETIME_FORMAT_SIMPLE

ui.lists.Search_emails.defaultSortList.columnName = sentDate_dt
ui.lists.Search_emails.defaultSortList.ascending  = false

# Facets
ui.lists.Search_emails.facets.parameters = sender_s
ui.lists.Search_emails.facets.parameters.sender_s.type = SimpleFacetParameter

# Advanced search enabled
ui.lists.Search_emails.search.advanced.enabled = true

New RodaConstants entries

public static final String UI_CATALOGUE_VIRTUAL_PROPERTY          = "ui.catalogue.virtual";
public static final String UI_LISTS_CATALOGUE_LABEL_I18N_PROPERTY = "catalogue.label.i18n";
public static final String UI_LISTS_CATALOGUE_ICON_PROPERTY       = "catalogue.icon";
public static final String UI_LISTS_CATALOGUE_CHILDOF_FILTER      = "catalogue.childOf.filter";
public static final String UI_LISTS_CATALOGUE_CLICK_ACTION        = "catalogue.click_action";

SearchWrapper Changes

File: roda-ui/roda-wui/src/main/java/org/roda/wui/client/common/search/SearchWrapper.java

Extend Components inner class

The current Components class uses Map<Class<? extends IsIndexed>, ...>. Add parallel string-keyed maps for virtual catalogues:

Components changes
private class Components {
  // Existing maps (unchanged)
  private final Map<Class<? extends IsIndexed>, SearchPanel<?>> searchPanels = new LinkedHashMap<>();
  private final Map<Class<? extends IsIndexed>, AsyncTableCell<?>> lists = new LinkedHashMap<>();

  // NEW: string-keyed maps for virtual catalogues (multiple IndexedAIP lists)
  private final Map<String, SearchPanel<?>> virtualSearchPanels = new LinkedHashMap<>();
  private final Map<String, AsyncTableCell<?>> virtualLists = new LinkedHashMap<>();

  public <T extends IsIndexed> void putVirtual(String listId, SearchPanel<T> panel, AsyncTableCell<T> list) {
    virtualSearchPanels.put(listId, panel);
    virtualLists.put(listId, list);
  }

  // getList(String key): check virtual map first, then fall back to class-name lookup
  <T extends IsIndexed> AsyncTableCell<T> getList(String key) {
    if (virtualLists.containsKey(key)) {
      return (AsyncTableCell<T>) virtualLists.get(key);
    }
    return (AsyncTableCell<T>) lists.get(classForName(key));
  }

  // forEachList: include virtual lists
  <T extends IsIndexed> void forEachList(Consumer<AsyncTableCell<T>> action) {
    for (AsyncTableCell<?> list : lists.values()) action.accept((AsyncTableCell<T>) list);
    for (AsyncTableCell<?> list : virtualLists.values()) action.accept((AsyncTableCell<T>) list);
  }
}

New createListAndSearchPanel overload for virtual catalogues

Uses listId as the dropdownValue (not classSimpleName) so each virtual catalogue gets a unique dropdown slot:

public SearchWrapper createVirtualListAndSearchPanel(ListBuilder<IndexedAIP> listBuilder, String listId, ...) {
  // dropdownValue = listId  (e.g. "Search_emails")
  // components.putVirtual(listId, searchPanel, list)
  // searchPanelSelectionDropdown.addItem(label, listId, icon)
}

CatalogueSearch Changes

File: roda-ui/roda-wui/src/main/java/org/roda/wui/client/common/search/CatalogueSearch.java

Read virtual catalogue configs

List<String> virtualCatalogueIds = ConfigurationManager.getStringList(
  RodaConstants.UI_CATALOGUE_VIRTUAL_PROPERTY);

Build virtual list builders

For each virtual catalogue listId:

String childOfFilter = ConfigurationManager.getString(
  RodaConstants.UI_LISTS_PROPERTY, listId, RodaConstants.UI_LISTS_CATALOGUE_CHILDOF_FILTER);
// e.g. "content_type:emailarchive" → parsed as SimpleFilterParameter

Filter baseFilter = new Filter(new ChildOfFilterParameter(
  new SimpleFilterParameter(parseKey(childOfFilter), parseValue(childOfFilter))));

ListBuilder<IndexedAIP> lb = new ListBuilder<>(
  () -> new ConfigurableAsyncTableCell<>(),
  new AsyncTableCellOptions<>(IndexedAIP.class, listId)
    .withFilter(baseFilter)
    .withStartHidden(startHidden)
    // NO bindOpener() — custom click action below
);
searchWrapper.createVirtualListAndSearchPanel(lb, listId, true, true);

Row click — navigate to parent AIP

Child UUID format: {parentUUID}/{fieldName}#{index} (e.g. 5f28e162-.../emails#42).

// Registered as a click handler on each row in the virtual catalogue list:
String childUUID = object.getUUID();
String parentUUID = childUUID.contains("/") ? childUUID.split("/")[0] : childUUID;
History.newItem(HistoryUtils.getHistoryBrowse(parentUUID));

i18n Keys Required

ui.catalogue.Search_emails.label = Emails
ui.lists.Search_emails.columns.subject_txt = Subject
ui.lists.Search_emails.columns.sender_s    = From
ui.lists.Search_emails.columns.sentDate_dt = Sent Date

Files to Change

File Action
roda-core/roda-common-data/.../data/common/RodaConstants.java Edit — add new constants
roda-ui/.../resources/config/roda-wui.properties Edit — add virtual catalogue config
roda-ui/.../client/common/search/SearchWrapper.java Edit — extend Components, add virtual list support
roda-ui/.../client/common/search/CatalogueSearch.java Edit — read virtual configs, build list builders, row click handler
All locale .properties files Edit — add i18n keys

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions