From fc49f58f1678d3eef70a9ac22280385d6d03a959 Mon Sep 17 00:00:00 2001 From: Bindu Gajria <37914059+bgajria@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:44:15 -0400 Subject: [PATCH] Add getDefaultLinkCategory hook and load-time macro substitution for default hyperlinks DatasetInjector subclasses can now name a default hyperlink category (as declared in datasetLinks.xml) that auto-loads when no explicit categoryOverride is set, without touching categoryOverride itself (which also drives DatasetDatasource.category / search facets, and must stay untouched here). The category-default hyperlink loading path in DatasetPresenterSetLoader.loadDatasetInjector previously inserted link text/url verbatim with no macro substitution, so any template containing DEFAULT_PROJECT/DEFAULT_DATASET_NAME/DEFAULT_ORG_ABBREV/ DEFAULT_SEQUENCE placeholders would load broken literal text. Added loadSubstitutedLink to resolve these at load time via new organism and default-top-level-sequence lookups (small per-taxon caches). Companion fix in ApiCommonModel: ChIPSeq.java overrides the new hook to restore a working JBrowse link in the dataset References table for ChIP-seq datasets, a mechanism that regressed when datasetLinks.xml migrated its selector scheme from type/subtype to category in 2025 without anyone wiring up the corresponding categoryOverride. --- .../datasetPresenter/DatasetInjector.java | 11 ++ .../DatasetPresenterSetLoader.java | 110 +++++++++++++++++- 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetInjector.java b/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetInjector.java index 838ecc03..5ad3d9d8 100644 --- a/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetInjector.java +++ b/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetInjector.java @@ -565,4 +565,15 @@ public String getCategoryOverride() { public void setCategoryOverride(String categoryOverride) { this._categoryOverride = categoryOverride; } + + /** + * Subclasses may override to name a default-hyperlink category (as declared + * in datasetLinks.xml) whose links should auto-load for this injector's + * dataset(s) when no explicit categoryOverride is set. Unlike + * getCategoryOverride(), this affects ONLY which default hyperlinks load — + * it has no effect on DatasetDatasource.category (search/facet behavior). + */ + public String getDefaultLinkCategory() { + return null; + } } diff --git a/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetPresenterSetLoader.java b/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetPresenterSetLoader.java index d8b4ee71..bf94b69b 100644 --- a/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetPresenterSetLoader.java +++ b/DatasetPresenter/src/main/java/org/apidb/apicommon/datasetPresenter/DatasetPresenterSetLoader.java @@ -12,6 +12,8 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -37,6 +39,8 @@ public class DatasetPresenterSetLoader { private Contacts allContacts; private HyperLinks defaultHyperLinks; + private final Map orgAbbrevCache = new HashMap<>(); + private final Map defaultSequenceCache = new HashMap<>(); private Connection dbConnection; private Configuration config; @@ -383,9 +387,21 @@ void loadDatasetInjector(DatasetPresenter datasetPresenter, DatasetInjector data loadInjectorPropValue(datasetPresenterId, name, pv.getKey(), dataValue, injectorPropertiesStmt); } - if (datasetInjector.getCategoryOverride() != null) { - for (HyperLink link : defaultHyperLinks.getHyperLinksFromCategory(datasetInjector.getCategoryOverride())) - loadLink(datasetPresenterId, link, linkStmt); + String linkCategory = datasetInjector.getCategoryOverride() != null + ? datasetInjector.getCategoryOverride() + : datasetInjector.getDefaultLinkCategory(); + + if (linkCategory != null) { + Collection targetDatasources = datasetInjector.getDatasourceName() != null + ? Collections.singleton(datasetPresenter.getDatasource(datasetInjector.getDatasourceName())) + : datasetPresenter.getDatasources(); + + for (HyperLink link : defaultHyperLinks.getHyperLinksFromCategory(linkCategory)) { + for (Datasource datasource : targetDatasources) { + if (datasource == null) continue; + loadSubstitutedLink(datasetPresenterId, link, datasource, datasetInjector.getDatasetName(), linkStmt); + } + } } for (ModelReference ref : datasetInjector.getModelReferences()) { @@ -668,6 +684,94 @@ private void loadLink(String datasetPresenterId, HyperLink link, stmt.execute(); } + /** + * Loads a default (category-templated) hyperlink for one datasource, substituting + * DEFAULT_PROJECT/DEFAULT_DATASET_NAME/DEFAULT_ORG_ABBREV/DEFAULT_SEQUENCE placeholders + * with real values before insertion. The HyperLink instance passed in is shared (parsed + * once from datasetLinks.xml and reused across every dataset in its category), so it is + * never mutated here — only fresh, substituted copies of its text/url are built. + */ + private void loadSubstitutedLink(String datasetPresenterId, HyperLink link, Datasource datasource, + String datasetName, PreparedStatement stmt) throws SQLException { + String text = link.getText(); + String description = link.getDescription(); + String url = link.getUrl(); + + String projectId = datasource.getProjectId(); + text = replaceIfPresent(text, "DEFAULT_PROJECT", projectId); + url = replaceIfPresent(url, "DEFAULT_PROJECT", projectId); + + String linkDatasetName = datasource.getName() != null ? datasource.getName() : datasetName; + text = replaceIfPresent(text, "DEFAULT_DATASET_NAME", linkDatasetName); + url = replaceIfPresent(url, "DEFAULT_DATASET_NAME", linkDatasetName); + + if (contains(url, "DEFAULT_ORG_ABBREV") || contains(text, "DEFAULT_ORG_ABBREV")) { + String orgAbbrev = lookupOrgAbbrev(datasource.getTaxonId()); + text = replaceIfPresent(text, "DEFAULT_ORG_ABBREV", orgAbbrev); + url = replaceIfPresent(url, "DEFAULT_ORG_ABBREV", orgAbbrev); + } + + if (contains(url, "DEFAULT_SEQUENCE") || contains(text, "DEFAULT_SEQUENCE")) { + String defaultSeq = lookupDefaultTopLevelSequence(datasource.getTaxonId()); + text = replaceIfPresent(text, "DEFAULT_SEQUENCE", defaultSeq); + url = replaceIfPresent(url, "DEFAULT_SEQUENCE", defaultSeq); + } + + stmt.setString(1, datasetPresenterId); + stmt.setString(2, text); + stmt.setString(3, description); + stmt.setString(4, url); + stmt.setString(5, link.getIsPublication()); + stmt.execute(); + } + + private static boolean contains(String s, String needle) { + return s != null && s.contains(needle); + } + + private static String replaceIfPresent(String s, String placeholder, String value) { + return (s == null || value == null) ? s : s.replace(placeholder, value); + } + + PreparedStatement getOrganismAbbrevStmt() throws SQLException { + return dbConnection.prepareStatement("SELECT public_abbrev FROM apidb.Organism WHERE taxon_id = ?"); + } + + PreparedStatement getDefaultTopLevelSequenceStmt() throws SQLException { + return dbConnection.prepareStatement( + "SELECT source_id FROM webready.GenomicSeqAttributes_p" + + " WHERE taxon_id = ? AND is_top_level = 1" + + " ORDER BY chromosome_order_num, length DESC LIMIT 1"); + } + + private String lookupOrgAbbrev(Integer taxonId) throws SQLException { + if (taxonId == null) return null; + if (orgAbbrevCache.containsKey(taxonId)) return orgAbbrevCache.get(taxonId); + String abbrev = null; + try (PreparedStatement stmt = getOrganismAbbrevStmt()) { + stmt.setInt(1, taxonId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) abbrev = rs.getString(1); + } + } + orgAbbrevCache.put(taxonId, abbrev); + return abbrev; + } + + private String lookupDefaultTopLevelSequence(Integer taxonId) throws SQLException { + if (taxonId == null) return null; + if (defaultSequenceCache.containsKey(taxonId)) return defaultSequenceCache.get(taxonId); + String seq = null; + try (PreparedStatement stmt = getDefaultTopLevelSequenceStmt()) { + stmt.setInt(1, taxonId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) seq = rs.getString(1); + } + } + defaultSequenceCache.put(taxonId, seq); + return seq; + } + // ///////////// Static methods //////////////////////////////