001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.search.impl; 007 008import static java.time.format.DateTimeFormatter.ISO_INSTANT; 009import static java.util.stream.Collectors.toList; 010import static org.fcrepo.common.db.DbPlatform.POSTGRESQL; 011import static org.fcrepo.search.api.Condition.Field.CONTENT_SIZE; 012import static org.fcrepo.search.api.Condition.Field.FEDORA_ID; 013import static org.fcrepo.search.api.Condition.Field.MIME_TYPE; 014import static org.fcrepo.search.api.Condition.Field.RDF_TYPE; 015 016import java.net.URI; 017import java.sql.ResultSet; 018import java.sql.SQLException; 019import java.sql.Timestamp; 020import java.sql.Types; 021import java.time.Instant; 022import java.time.temporal.ChronoUnit; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Set; 031import java.util.concurrent.ConcurrentHashMap; 032 033import javax.annotation.PostConstruct; 034import javax.inject.Inject; 035import javax.sql.DataSource; 036 037import com.google.common.collect.Sets; 038import org.fcrepo.common.db.DbPlatform; 039import org.fcrepo.kernel.api.Transaction; 040import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 041import org.fcrepo.kernel.api.identifiers.FedoraId; 042import org.fcrepo.kernel.api.models.ResourceFactory; 043import org.fcrepo.kernel.api.models.ResourceHeaders; 044import org.fcrepo.search.api.Condition; 045import org.fcrepo.search.api.InvalidQueryException; 046import org.fcrepo.search.api.PaginationInfo; 047import org.fcrepo.search.api.SearchIndex; 048import org.fcrepo.search.api.SearchParameters; 049import org.fcrepo.search.api.SearchResult; 050import org.slf4j.Logger; 051import org.slf4j.LoggerFactory; 052import org.springframework.dao.DuplicateKeyException; 053import org.springframework.jdbc.core.RowMapper; 054import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; 055import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 056import org.springframework.stereotype.Component; 057import org.springframework.transaction.annotation.Propagation; 058import org.springframework.transaction.annotation.Transactional; 059 060/** 061 * An implementation of the {@link SearchIndex} 062 * 063 * @author dbernstein 064 * @author whikloj 065 */ 066@Component("searchIndexImpl") 067public class DbSearchIndexImpl implements SearchIndex { 068 private static final Logger LOGGER = LoggerFactory.getLogger(DbSearchIndexImpl.class); 069 070 private static final String TRANSACTION_ID_COLUMN = "transaction_id"; 071 private static final String SIMPLE_SEARCH_TABLE = "simple_search"; 072 private static final String SIMPLE_SEARCH_TRANSACTIONS_TABLE = "simple_search_transactions"; 073 private static final String SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE = "search_resource_rdf_type_transactions"; 074 public static final String SEARCH_RESOURCE_RDF_TYPE_TABLE = "search_resource_rdf_type"; 075 public static final String SEARCH_RDF_TYPE_TABLE = "search_rdf_type"; 076 077 private static final String FEDORA_ID_COLUMN = "fedora_id"; 078 private static final String MODIFIED_COLUMN = "modified"; 079 private static final String CREATED_COLUMN = "created"; 080 private static final String CONTENT_SIZE_COLUMN = "content_size"; 081 private static final String MIME_TYPE_COLUMN = "mime_type"; 082 private static final String RESOURCE_ID_COLUMN = "resource_id"; 083 public static final String RDF_TYPE_ID_COLUMN = "rdf_type_id"; 084 public static final String ID_COLUMN = "id"; 085 private static final String OPERATION_COLUMN = "operation"; 086 private static final String RDF_TYPE_URI_COLUMN = "rdf_type_uri"; 087 088 private static final String FEDORA_ID_PARAM = "fedora_id"; 089 private static final String RESOURCE_ID_PARAM = "resource_id"; 090 private static final String RDF_TYPE_ID_PARAM = "rdf_type_id"; 091 private static final String MODIFIED_PARAM = "modified"; 092 private static final String CONTENT_SIZE_PARAM = "content_size"; 093 private static final String MIME_TYPE_PARAM = "mime_type"; 094 private static final String CREATED_PARAM = "created"; 095 public static final String RDF_TYPE_URI_PARAM = "rdf_type_uri"; 096 public static final String RESOURCE_SEARCH_ID_PARAM = "resource_search_id"; 097 098 public static final String TRANSACTION_ID_PARAM = "transaction_id"; 099 private static final String OPERATION_PARAM = "operation"; 100 101 private static final String RDF_TYPE_FILTER_SUB_TABLE = ", (SELECT rrt." + RESOURCE_ID_COLUMN + " from " + 102 SEARCH_RESOURCE_RDF_TYPE_TABLE + " rrt, " + 103 SEARCH_RDF_TYPE_TABLE + " rt, " + SIMPLE_SEARCH_TABLE + " s WHERE rrt.rdf_type_id = rt.id and s.id = " + 104 "rrt.resource_id and rt." + RDF_TYPE_URI_COLUMN + " like :" + RDF_TYPE_URI_PARAM + 105 " group by rrt." + RESOURCE_ID_COLUMN + ") r_filter"; 106 private static final String RDF_TYPES_SUB_TABLE = ", (SELECT rrt.resource_id, group_concat_function as rdf_type " + 107 " from " + SEARCH_RESOURCE_RDF_TYPE_TABLE + " rrt, " + 108 "search_rdf_type rt ," + SIMPLE_SEARCH_TABLE + " s " + 109 "WHERE rrt.rdf_type_id = rt.id group by rrt.resource_id) r "; 110 111 private static final String POSTGRES_GROUP_CONCAT_FUNCTION = "STRING_AGG(b.rdf_type_uri, ',')"; 112 private static final String DEFAULT_GROUP_CONCAT_FUNCTION = "GROUP_CONCAT(distinct b.rdf_type_uri " + 113 "ORDER BY b.rdf_type_uri ASC SEPARATOR ',')"; 114 115 private static final String UPSERT_SIMPLE_SEARCH_TRANSACTION_H2 = 116 "MERGE INTO " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + 117 CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," + 118 FEDORA_ID_COLUMN + "," + OPERATION_COLUMN + ", " + TRANSACTION_ID_COLUMN + 119 ") KEY (" + FEDORA_ID_COLUMN + ", " + TRANSACTION_ID_COLUMN + ") VALUES ( :" + MODIFIED_PARAM + 120 ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," + 121 ":" + FEDORA_ID_PARAM + ", :" + OPERATION_PARAM + ", :" + TRANSACTION_ID_PARAM + ")"; 122 123 private static final String UPSERT_SIMPLE_SEARCH_H2 = 124 "MERGE INTO " + SIMPLE_SEARCH_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + 125 CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," + 126 FEDORA_ID_COLUMN + ") KEY (" + FEDORA_ID_COLUMN + ") VALUES ( :" + MODIFIED_PARAM + 127 ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," + 128 ":" + FEDORA_ID_PARAM + ")"; 129 130 private static final String UPSERT_SIMPLE_SEARCH_TRANSACTION_MYSQL_MARIA = 131 "INSERT INTO " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + 132 CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," + 133 FEDORA_ID_COLUMN + "," + OPERATION_COLUMN + ", " + TRANSACTION_ID_COLUMN + 134 ") VALUES ( :" + MODIFIED_PARAM + ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + 135 ", :" + MIME_TYPE_PARAM + "," + ":" + FEDORA_ID_PARAM + ", :" + OPERATION_PARAM + 136 ", :" + TRANSACTION_ID_PARAM + ") ON DUPLICATE KEY " + 137 "UPDATE " + MODIFIED_COLUMN + " = VALUES(" + MODIFIED_COLUMN + "), " + 138 CREATED_COLUMN + "= VALUES(" + CREATED_COLUMN + ")," + 139 CONTENT_SIZE_COLUMN + "= VALUES(" + CONTENT_SIZE_COLUMN + ")," + 140 MIME_TYPE_COLUMN + "= VALUES(" + MIME_TYPE_COLUMN + ")," + 141 OPERATION_COLUMN + "= VALUES(" + OPERATION_COLUMN + ")"; 142 143 private static final String UPSERT_SIMPLE_SEARCH_MYSQL_MARIA = 144 "INSERT INTO " + SIMPLE_SEARCH_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + 145 CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," + 146 FEDORA_ID_COLUMN + ") VALUES ( :" + MODIFIED_PARAM + ", :" + CREATED_PARAM + 147 ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," + ":" + FEDORA_ID_PARAM + ") " + 148 "ON DUPLICATE KEY UPDATE " + MODIFIED_COLUMN + " = VALUES(" + MODIFIED_COLUMN + "), " + 149 CREATED_COLUMN + "= VALUES(" + CREATED_COLUMN + ")," + 150 CONTENT_SIZE_COLUMN + "= VALUES(" + CONTENT_SIZE_COLUMN + ")," + 151 MIME_TYPE_COLUMN + "= VALUES(" + MIME_TYPE_COLUMN + ")"; 152 153 private static final String UPSERT_SIMPLE_SEARCH_TRANSACTION_POSTGRESQL = 154 "INSERT INTO " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + 155 CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," + 156 FEDORA_ID_COLUMN + "," + OPERATION_COLUMN + ", " + TRANSACTION_ID_COLUMN + 157 ") VALUES ( :" + MODIFIED_PARAM + 158 ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," + 159 ":" + FEDORA_ID_PARAM + ", :" + OPERATION_PARAM + ", :" + TRANSACTION_ID_PARAM + ") ON CONFLICT " + 160 "( " + FEDORA_ID_COLUMN + ", " + TRANSACTION_ID_COLUMN + ") " + 161 "DO UPDATE SET " + MODIFIED_COLUMN + " = EXCLUDED." + MODIFIED_COLUMN + ", " + 162 CREATED_COLUMN + " = EXCLUDED." + CREATED_COLUMN + ", " + 163 CONTENT_SIZE_COLUMN + " = EXCLUDED." + CONTENT_SIZE_COLUMN + ", " + 164 MIME_TYPE_COLUMN + " = EXCLUDED." + MIME_TYPE_COLUMN + ", " + 165 OPERATION_COLUMN + " = EXCLUDED." + OPERATION_COLUMN; 166 167 private static final String UPSERT_SIMPLE_SEARCH_POSTGRESQL = 168 "INSERT INTO " + SIMPLE_SEARCH_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + 169 CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," + 170 FEDORA_ID_COLUMN + ") VALUES ( :" + MODIFIED_PARAM + 171 ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," + 172 ":" + FEDORA_ID_PARAM + ") ON CONFLICT ( " + FEDORA_ID_COLUMN + ") " + 173 "DO UPDATE SET " + MODIFIED_COLUMN + " = EXCLUDED." + MODIFIED_COLUMN + ", " + 174 CREATED_COLUMN + " = EXCLUDED." + CREATED_COLUMN + ", " + 175 CONTENT_SIZE_COLUMN + " = EXCLUDED." + CONTENT_SIZE_COLUMN + ", " + 176 MIME_TYPE_COLUMN + " = EXCLUDED." + MIME_TYPE_COLUMN; 177 178 private static final String UPSERT_COMMIT_SIMPLE_SEARCH_H2 = 179 "MERGE INTO " + SIMPLE_SEARCH_TABLE + 180 " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + CONTENT_SIZE_COLUMN + "," + 181 MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + 182 ") KEY (" + FEDORA_ID_COLUMN + ") SELECT " + MODIFIED_COLUMN + ", " + CREATED_COLUMN + 183 ", " + CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + " FROM " + 184 SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + "= :" + 185 TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + "='add'"; 186 187 private static final String UPSERT_COMMIT_SIMPLE_SEARCH_MYSQL_MARIA = "INSERT INTO " + SIMPLE_SEARCH_TABLE + 188 " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + CONTENT_SIZE_COLUMN + "," + 189 MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + 190 ") SELECT " + MODIFIED_COLUMN + ", " + CREATED_COLUMN + 191 ", " + CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + " FROM " + 192 SIMPLE_SEARCH_TRANSACTIONS_TABLE + " a WHERE " + TRANSACTION_ID_COLUMN + "= :" + 193 TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + "='add' " + 194 "ON DUPLICATE KEY UPDATE " + MODIFIED_COLUMN + " = a." + MODIFIED_COLUMN + ", " + 195 CREATED_COLUMN + " = a." + CREATED_COLUMN + ", " + 196 CONTENT_SIZE_COLUMN + " = a." + CONTENT_SIZE_COLUMN + ", " + 197 MIME_TYPE_COLUMN + " = a." + MIME_TYPE_COLUMN; 198 199 private static final String UPSERT_COMMIT_SIMPLE_SEARCH_POSTGRESQL = "INSERT INTO " + SIMPLE_SEARCH_TABLE + 200 " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + CONTENT_SIZE_COLUMN + "," + 201 MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + 202 ") SELECT " + MODIFIED_COLUMN + ", " + CREATED_COLUMN + 203 ", " + CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + " FROM " + 204 SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + "= :" + 205 TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + "='add' ON CONFLICT (" + FEDORA_ID_COLUMN + ") " + 206 "DO UPDATE SET " + MODIFIED_COLUMN + " = EXCLUDED." + MODIFIED_COLUMN + ", " + 207 CREATED_COLUMN + " = EXCLUDED." + CREATED_COLUMN + ", " + 208 CONTENT_SIZE_COLUMN + " = EXCLUDED." + CONTENT_SIZE_COLUMN + ", " + 209 MIME_TYPE_COLUMN + " = EXCLUDED." + MIME_TYPE_COLUMN; 210 211 private static final String COMMIT_RDF_TYPES = 212 "INSERT INTO " + SEARCH_RDF_TYPE_TABLE + " (" + RDF_TYPE_URI_COLUMN + ")" + 213 " SELECT distinct " + RDF_TYPE_URI_COLUMN + " FROM " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + 214 " WHERE " + TRANSACTION_ID_COLUMN + "= :" + TRANSACTION_ID_PARAM + " AND " + RDF_TYPE_URI_COLUMN + 215 " NOT IN (SELECT " + RDF_TYPE_URI_PARAM + " FROM " + SEARCH_RDF_TYPE_TABLE + ")"; 216 217 private static final String INSERT_RDF_TYPE = 218 "INSERT INTO " + SEARCH_RDF_TYPE_TABLE + " (" + RDF_TYPE_URI_COLUMN + ")" + 219 " VALUES (:" + RDF_TYPE_URI_PARAM + ")"; 220 221 private static final String INSERT_RDF_TYPE_POSTGRES = 222 "INSERT INTO " + SEARCH_RDF_TYPE_TABLE + " (" + RDF_TYPE_URI_COLUMN + ")" + 223 " VALUES (:" + RDF_TYPE_URI_PARAM + ")" + 224 " ON CONFLICT (" + RDF_TYPE_URI_COLUMN + ") DO NOTHING"; 225 226 private static final String COMMIT_RDF_TYPE_ASSOCIATIONS = 227 "INSERT INTO " + SEARCH_RESOURCE_RDF_TYPE_TABLE + 228 " (" + RESOURCE_ID_COLUMN + "," + RDF_TYPE_ID_COLUMN + ")" + 229 " SELECT a." + ID_COLUMN + ", b." + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE + " a, " + 230 SEARCH_RDF_TYPE_TABLE + " b, " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " c WHERE c." + 231 TRANSACTION_ID_COLUMN + "= :" + TRANSACTION_ID_PARAM + " AND b." + RDF_TYPE_URI_COLUMN + 232 "= c." + RDF_TYPE_URI_COLUMN + " AND c." + FEDORA_ID_COLUMN + " = a." + FEDORA_ID_COLUMN + 233 " GROUP BY a." + ID_COLUMN + ", b." + ID_COLUMN; 234 235 private static final String COMMIT_DELETE_RESOURCES_IN_TRANSACTION = 236 "DELETE FROM " + SIMPLE_SEARCH_TABLE + " WHERE " + FEDORA_ID_COLUMN + " IN (SELECT " + FEDORA_ID_COLUMN + 237 " FROM " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = " + 238 ":" + TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + " = 'delete')"; 239 240 private static final String COMMIT_DELETE_RDF_TYPE_ASSOCIATIONS = 241 "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TABLE + " where " + 242 RESOURCE_ID_COLUMN + " in (SELECT a." + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE + " a, " + 243 SIMPLE_SEARCH_TRANSACTIONS_TABLE + " b " + 244 " WHERE a." + FEDORA_ID_COLUMN + "= b." + FEDORA_ID_COLUMN + " AND b." + TRANSACTION_ID_COLUMN + 245 "= :" + TRANSACTION_ID_PARAM + ")"; 246 247 private static final String DELETE_TRANSACTION = 248 "DELETE FROM " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :" + 249 TRANSACTION_ID_PARAM; 250 251 private static final String DELETE_RESOURCE_FROM_SEARCH = 252 "DELETE FROM " + SIMPLE_SEARCH_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :" + 253 FEDORA_ID_PARAM; 254 255 private static final String DELETE_RDF_TYPE_ASSOCIATIONS_IN_TRANSACTION = 256 "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :" + 257 TRANSACTION_ID_PARAM; 258 259 260 private static final Map<DbPlatform, String> DIRECT_UPSERT_MAPPING = Map.of( 261 DbPlatform.H2, UPSERT_SIMPLE_SEARCH_H2, 262 DbPlatform.MYSQL, UPSERT_SIMPLE_SEARCH_MYSQL_MARIA, 263 DbPlatform.MARIADB, UPSERT_SIMPLE_SEARCH_MYSQL_MARIA, 264 DbPlatform.POSTGRESQL, UPSERT_SIMPLE_SEARCH_POSTGRESQL 265 ); 266 267 private static final Map<DbPlatform, String> TRANSACTION_UPSERT_MAPPING = Map.of( 268 DbPlatform.H2, UPSERT_SIMPLE_SEARCH_TRANSACTION_H2, 269 DbPlatform.MYSQL, UPSERT_SIMPLE_SEARCH_TRANSACTION_MYSQL_MARIA, 270 DbPlatform.MARIADB, UPSERT_SIMPLE_SEARCH_TRANSACTION_MYSQL_MARIA, 271 DbPlatform.POSTGRESQL, UPSERT_SIMPLE_SEARCH_TRANSACTION_POSTGRESQL 272 ); 273 274 private static final Map<DbPlatform, String> UPSERT_COMMIT_MAPPING = Map.of( 275 DbPlatform.H2, UPSERT_COMMIT_SIMPLE_SEARCH_H2, 276 DbPlatform.MYSQL, UPSERT_COMMIT_SIMPLE_SEARCH_MYSQL_MARIA, 277 DbPlatform.MARIADB, UPSERT_COMMIT_SIMPLE_SEARCH_MYSQL_MARIA, 278 DbPlatform.POSTGRESQL, UPSERT_COMMIT_SIMPLE_SEARCH_POSTGRESQL 279 ); 280 281 /* 282 * Insert an association between a RDF type and a resource. 283 */ 284 private static final String INSERT_RDF_TYPE_ASSOC_IN_TRANSACTION = "INSERT INTO " + 285 SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " (" + FEDORA_ID_COLUMN + ", " + RDF_TYPE_URI_COLUMN + ", " + 286 TRANSACTION_ID_COLUMN + ") VALUES (:" + FEDORA_ID_PARAM + ", :" + RDF_TYPE_URI_PARAM + ", :" + 287 TRANSACTION_ID_PARAM + ")"; 288 289 private static final String SELECT_RESOURCE_SEARCH_ID = "SELECT " + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE + 290 " WHERE " + FEDORA_ID_COLUMN + " = :" + FEDORA_ID_PARAM; 291 292 private static final String SELECT_RDF_TYPE_ID = "SELECT " + ID_COLUMN + " FROM " + SEARCH_RDF_TYPE_TABLE + 293 " WHERE " + RDF_TYPE_URI_COLUMN + "= :" + RDF_TYPE_URI_PARAM; 294 295 private static final String INSERT_RDF_TYPE_ASSOC = "INSERT INTO " + SEARCH_RESOURCE_RDF_TYPE_TABLE + 296 " (" + RESOURCE_ID_COLUMN + ", " + RDF_TYPE_ID_COLUMN + ")" + 297 " VALUES (:" + RESOURCE_SEARCH_ID_PARAM + ", :" + RDF_TYPE_ID_PARAM + ")"; 298 299 private static final String DELETE_RESOURCE_TYPE_ASSOCIATIONS_IN_TRANSACTION = 300 "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " WHERE " + 301 FEDORA_ID_COLUMN + "= :" + FEDORA_ID_PARAM + " AND " + TRANSACTION_ID_COLUMN + "= :" + 302 TRANSACTION_ID_PARAM; 303 304 private static final String DELETE_RDF_TYPE_ASSOCIATIONS = 305 "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TABLE + " WHERE " + RESOURCE_ID_COLUMN + 306 " = (SELECT " + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE + " WHERE " + 307 FEDORA_ID_COLUMN + " = :" + FEDORA_ID_PARAM + ")"; 308 309 private static final List<String> COUNT_QUERY_COLUMNS = Arrays.asList("count(0) as count"); 310 311 @Inject 312 private DataSource dataSource; 313 314 private NamedParameterJdbcTemplate jdbcTemplate; 315 316 @Inject 317 private ResourceFactory resourceFactory; 318 319 private DbPlatform dbPlatForm; 320 321 private final Map<URI, Long> rdfTypeIdCache; 322 323 /** 324 * Setup database table and connection 325 */ 326 @PostConstruct 327 public void setup() { 328 this.dbPlatForm = DbPlatform.fromDataSource(this.dataSource); 329 this.jdbcTemplate = new NamedParameterJdbcTemplate(this.dataSource); 330 } 331 332 public DbSearchIndexImpl() { 333 this.rdfTypeIdCache = new ConcurrentHashMap<>(); 334 } 335 336 @Override 337 public SearchResult doSearch(final SearchParameters parameters) throws InvalidQueryException { 338 //translate parameters into a SQL query 339 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 340 final var fields = parameters.getFields().stream().map(Condition.Field::toString).collect(toList()); 341 final var selectQuery = createSearchQuery(parameters, parameterSource, fields, false); 342 final RowMapper<Map<String, Object>> rowMapper = createRowMapper(fields); 343 344 Integer totalResults = -1; 345 if (parameters.isIncludeTotalResultCount()) { 346 final var countQuery = createSearchQuery(parameters, parameterSource, Collections.emptyList(), true); 347 LOGGER.debug("countQuery={}, parameterSource={}", countQuery, parameterSource); 348 totalResults = jdbcTemplate.queryForObject(countQuery.toString(), parameterSource, Integer.class); 349 } 350 351 final var selectQueryStr = selectQuery.toString(); 352 LOGGER.debug("selectQueryStr={}, parameterSource={}", selectQueryStr, parameterSource); 353 354 final List<Map<String, Object>> items = jdbcTemplate.query(selectQueryStr, parameterSource, rowMapper); 355 final var pagination = new PaginationInfo(parameters.getMaxResults(), parameters.getOffset(), 356 (totalResults != null ? totalResults : 0)); 357 LOGGER.debug("Search query with parameters: {} - {}", selectQuery.toString(), parameters); 358 return new SearchResult(items, pagination); 359 } 360 361 private RowMapper<Map<String, Object>> createRowMapper(final List<String> fields) { 362 return new RowMapper<Map<String, Object>>() { 363 @Override 364 public Map<String, Object> mapRow(final ResultSet rs, final int rowNum) throws SQLException { 365 final Map<String, Object> map = new HashMap<>(); 366 for (final String fieldStr : fields) { 367 var value = rs.getObject(fieldStr); 368 if (value instanceof Timestamp) { 369 //format as iso instant if timestamp 370 value = ISO_INSTANT.format(Instant.ofEpochMilli(((Timestamp) value).getTime())); 371 } else if (fieldStr.equals(RDF_TYPE.toString())) { 372 //convert the comma-separate string to an array for rdf_type 373 value = value.toString().split(","); 374 } 375 map.put(fieldStr, value); 376 } 377 return map; 378 } 379 }; 380 } 381 382 private StringBuilder createSearchQuery(final SearchParameters parameters, 383 final MapSqlParameterSource parameterSource, 384 final List<String> selectedFields, final boolean isCountQuery) 385 throws InvalidQueryException { 386 387 final var queryFields = new ArrayList<>(selectedFields); 388 final var fedoraIdStr = FEDORA_ID.toString(); 389 390 if (!isCountQuery) { 391 if (!queryFields.contains(fedoraIdStr)) { 392 queryFields.add(0,fedoraIdStr); 393 } 394 queryFields.add(0,"id"); 395 } else { 396 queryFields.add("count(0)"); 397 } 398 399 final var whereClauses = new ArrayList<String>(); 400 final var conditions = parameters.getConditions(); 401 final var fields = new ArrayList<String>(queryFields); 402 final var rdfTypeConditionValue = 403 conditions.stream().filter(c -> c.getField().equals(RDF_TYPE)).findFirst().orElse(null); 404 final var returnRdfType = fields.stream().anyMatch(x -> x.equals(RDF_TYPE.toString())); 405 final var returnFields = fields.stream().filter(x -> !x.equals(RDF_TYPE.toString())).collect(toList()); 406 407 final var sql = new StringBuilder("") 408 .append("SELECT ") 409 .append(String.join(",", returnFields)); 410 sql.append(" FROM ") 411 .append(SIMPLE_SEARCH_TABLE).append(" s "); 412 413 if (rdfTypeConditionValue != null) { 414 final var rdfTypeOperator = rdfTypeConditionValue.getObject().contains("*") ? " LIKE " : " = "; 415 sql.append(", (SELECT ").append(RESOURCE_ID_COLUMN).append(" FROM ") 416 .append(SEARCH_RESOURCE_RDF_TYPE_TABLE).append(" WHERE ") 417 .append(RDF_TYPE_ID_COLUMN).append(" IN (").append("SELECT ID FROM ").append(SEARCH_RDF_TYPE_TABLE) 418 .append(" WHERE ").append(RDF_TYPE_URI_COLUMN).append(rdfTypeOperator) 419 .append(":").append(RDF_TYPE_URI_PARAM).append(")) rdf_type_filter "); 420 whereClauses.add("rdf_type_filter.resource_id = s.id"); 421 addRdfTypeParam(parameterSource, conditions); 422 } 423 424 for (int i = 0; i < conditions.size(); i++) { 425 addWhereClause(i, parameterSource, whereClauses, conditions.get(i)); 426 } 427 428 if (!whereClauses.isEmpty()) { 429 sql.append(" WHERE "); 430 for (final var it = whereClauses.iterator(); it.hasNext(); ) { 431 sql.append(it.next()); 432 if (it.hasNext()) { 433 sql.append(" AND "); 434 } 435 } 436 } 437 438 if (isCountQuery) { 439 return sql; 440 } 441 442 if (parameters.getOrderBy() != null) { 443 //add order by limit and offset to selectquery. 444 sql.append(" ORDER BY ").append(parameters.getOrderBy()).append(" ").append(parameters.getOrder()); 445 } 446 447 sql.append(" LIMIT :limit OFFSET :offset"); 448 parameterSource.addValue("limit", parameters.getMaxResults()); 449 parameterSource.addValue("offset", parameters.getOffset()); 450 451 if (!returnRdfType) { 452 return sql; 453 } else { 454 final var rdfTypeWrapperSql = new StringBuilder(); 455 rdfTypeWrapperSql.append("SELECT a.*, ") 456 .append(isPostgres() ? POSTGRES_GROUP_CONCAT_FUNCTION : DEFAULT_GROUP_CONCAT_FUNCTION) 457 .append(" as rdf_type") 458 .append(" FROM ") 459 .append("(").append(sql).append(") a, ") 460 .append("(SELECT rrt.resource_id , rt.rdf_type_uri FROM search_resource_rdf_type rrt, " + 461 "search_rdf_type rt WHERE rrt.rdf_type_id = rt.id) b ") 462 .append("WHERE a.id = b.resource_id GROUP BY ").append(String.join(",", returnFields)); 463 464 if (parameters.getOrderBy() != null) { 465 //add order by limit and offset to selectquery. 466 rdfTypeWrapperSql.append(" ORDER BY ").append(parameters.getOrderBy()).append(" ") 467 .append(parameters.getOrder()); 468 } 469 470 return rdfTypeWrapperSql; 471 } 472 } 473 474 private void addRdfTypeParam(final MapSqlParameterSource parameterSource, final List<Condition> conditions) { 475 var rdfTypeUriParamValue = "*"; 476 for (final Condition condition : conditions) { 477 if (condition.getField().equals(RDF_TYPE)) { 478 rdfTypeUriParamValue = condition.getObject(); 479 break; 480 } 481 } 482 parameterSource.addValue(RDF_TYPE_URI_PARAM, convertToSqlLikeWildcard(rdfTypeUriParamValue)); 483 } 484 485 private void addWhereClause(final int paramCount, final MapSqlParameterSource parameterSource, 486 final List<String> whereClauses, 487 final Condition condition) throws InvalidQueryException { 488 final var field = condition.getField(); 489 final var operation = condition.getOperator(); 490 var object = condition.getObject(); 491 final var paramName = "param" + paramCount; 492 if ((field.equals(FEDORA_ID) || field.equals(MIME_TYPE)) && 493 condition.getOperator().equals(Condition.Operator.EQ)) { 494 if (!object.equals("*")) { 495 final String whereClause; 496 if (object.contains("*")) { 497 object = convertToSqlLikeWildcard(object); 498 if (object.contains("_")) { 499 object = object.replaceAll("_", "\\\\_"); 500 } 501 whereClause = field + " like :" + paramName; 502 } else { 503 whereClause = field + " = :" + paramName; 504 } 505 506 whereClauses.add("s." + whereClause); 507 parameterSource.addValue(paramName, object); 508 } 509 } else if (field.equals(Condition.Field.CREATED) || field.equals(Condition.Field.MODIFIED)) { 510 //parse date 511 try { 512 final var instant = InstantParser.parse(object); 513 whereClauses.add("s." + field + " " + operation.getStringValue() + " :" + paramName); 514 parameterSource.addValue(paramName, new Timestamp(instant.toEpochMilli()), Types.TIMESTAMP); 515 } catch (final Exception ex) { 516 throw new InvalidQueryException(ex.getMessage()); 517 } 518 } else if (field.equals(CONTENT_SIZE)) { 519 try { 520 whereClauses.add(field + " " + operation.getStringValue() + 521 " :" + paramName); 522 parameterSource.addValue(paramName, Long.parseLong(object), Types.INTEGER); 523 } catch (final Exception ex) { 524 throw new InvalidQueryException(ex.getMessage()); 525 } 526 } else if (field.equals(RDF_TYPE) && condition.getOperator().equals(Condition.Operator.EQ) ) { 527 //allowed but no where clause added here. 528 } else { 529 throw new InvalidQueryException("Condition not supported: \"" + condition + "\""); 530 } 531 } 532 533 private String convertToSqlLikeWildcard(final String value) { 534 return value.replace("*", "%"); 535 } 536 537 @Override 538 public void addUpdateIndex(final Transaction transaction, final ResourceHeaders resourceHeaders) { 539 final var fedoraId = resourceHeaders.getId(); 540 if (fedoraId.isAcl() || fedoraId.isMemento()) { 541 LOGGER.debug("The search index does not include acls or mementos. Ignoring resource {}", 542 fedoraId.getFullId()); 543 return; 544 } 545 LOGGER.debug("Updating search index for {}", fedoraId); 546 transaction.doInTx(() -> { 547 if (!transaction.isShortLived()) { 548 doUpsertWithTransaction(transaction, resourceHeaders, fedoraId); 549 } else { 550 doDirectUpsert(transaction, resourceHeaders, fedoraId); 551 } 552 }); 553 554 } 555 556 private void doDirectUpsert(final Transaction transaction, final ResourceHeaders resourceHeaders, 557 final FedoraId fedoraId) { 558 final var fullId = fedoraId.getFullId(); 559 try { 560 final var fedoraResource = resourceFactory.getResource(transaction, fedoraId); 561 doUpsertIntoSimpleSearch(fedoraId, resourceHeaders); 562 final var rdfTypes = new ArrayList<>(Sets.newHashSet(fedoraResource.getTypes())); 563 final var newTypes = insertRdfTypes(rdfTypes); 564 deleteRdfTypeAssociations(fedoraId); 565 insertRdfTypeAssociations(rdfTypes, newTypes, fedoraId); 566 } catch (final Exception e) { 567 throw new RepositoryRuntimeException("Failed add/updated the search index for : " + fullId, e); 568 } 569 } 570 571 /** 572 * Adds the list of RDF types to the db, if they aren't already there, and returns a set of types that were 573 * actually added. 574 * 575 * @param rdfTypes the types to attempt to add 576 * @return the types that were added 577 */ 578 private Set<URI> insertRdfTypes(final List<URI> rdfTypes) { 579 final var addTypes = new HashSet<URI>(); 580 581 rdfTypes.stream() 582 .filter(rdfType -> !rdfTypeIdCache.containsKey(rdfType)) 583 .forEach(rdfType -> { 584 try { 585 final var params = new MapSqlParameterSource(); 586 params.addValue(RDF_TYPE_URI_PARAM, rdfType.toString()); 587 if (isPostgres()) { 588 // weirdly, postgres spoils the entire tx on duplicate keys and must be handled differently 589 jdbcTemplate.update(INSERT_RDF_TYPE_POSTGRES, params); 590 } else { 591 jdbcTemplate.update(INSERT_RDF_TYPE, params); 592 } 593 594 addTypes.add(rdfType); 595 } catch (DuplicateKeyException e) { 596 // ignore duplicate keys 597 } 598 }); 599 600 return addTypes; 601 } 602 603 private void doUpsertWithTransaction(final Transaction transaction, final ResourceHeaders resourceHeaders, 604 final FedoraId fedoraId) { 605 final var fullId = fedoraId.getFullId(); 606 try { 607 final var txId = transaction.getId(); 608 final var fedoraResource = resourceFactory.getResource(transaction, fedoraId); 609 doUpsertIntoTransactionTables(txId, fedoraId, resourceHeaders, "add"); 610 // add rdf type associations to the rdf type association table 611 final var rdfTypes = Sets.newHashSet(fedoraResource.getTypes()); 612 insertRdfTypeAssociationsInTransaction(rdfTypes, txId, fedoraId); 613 } catch (final Exception e) { 614 throw new RepositoryRuntimeException("Failed add/updated the search index for : " + fullId, e); 615 } 616 } 617 618 /** 619 * Do the upsert action to the transaction table. 620 * 621 * @param txId the transaction id 622 * @param fedoraId the resourceId 623 * @param resourceHeaders the resources headers 624 * @param operation the operation to perform. 625 */ 626 private void doUpsertIntoTransactionTables(final String txId, final FedoraId fedoraId, 627 final ResourceHeaders resourceHeaders, final String operation) { 628 var mimetype = ""; 629 long contentSize = 0; 630 var modified = Instant.now(); 631 var created = Instant.now(); 632 if (resourceHeaders != null) { 633 contentSize = resourceHeaders.getContentSize(); 634 mimetype = resourceHeaders.getMimeType(); 635 modified = resourceHeaders.getLastModifiedDate(); 636 created = resourceHeaders.getCreatedDate(); 637 } 638 639 final var params = new MapSqlParameterSource(); 640 params.addValue(FEDORA_ID_PARAM, fedoraId.getFullId()); 641 params.addValue(MIME_TYPE_PARAM, mimetype); 642 params.addValue(CONTENT_SIZE_PARAM, contentSize); 643 params.addValue(CREATED_PARAM, formatInstant(created)); 644 params.addValue(MODIFIED_PARAM, formatInstant(modified)); 645 params.addValue(OPERATION_PARAM, operation); 646 params.addValue(TRANSACTION_ID_PARAM, txId); 647 jdbcTemplate.update(TRANSACTION_UPSERT_MAPPING.get(dbPlatForm), params); 648 } 649 650 /** 651 * Do direct upsert into simpl search table. 652 * 653 * @param fedoraId the resourceId 654 * @param resourceHeaders the resources headers 655 */ 656 private void doUpsertIntoSimpleSearch(final FedoraId fedoraId, 657 final ResourceHeaders resourceHeaders) { 658 var mimetype = ""; 659 long contentSize = 0; 660 var modified = Instant.now(); 661 var created = Instant.now(); 662 if (resourceHeaders != null) { 663 contentSize = resourceHeaders.getContentSize(); 664 mimetype = resourceHeaders.getMimeType(); 665 modified = resourceHeaders.getLastModifiedDate(); 666 created = resourceHeaders.getCreatedDate(); 667 } 668 669 final var params = new MapSqlParameterSource(); 670 params.addValue(FEDORA_ID_PARAM, fedoraId.getFullId()); 671 params.addValue(MIME_TYPE_PARAM, mimetype); 672 params.addValue(CONTENT_SIZE_PARAM, contentSize); 673 params.addValue(CREATED_PARAM, formatInstant(created)); 674 params.addValue(MODIFIED_PARAM, formatInstant(modified)); 675 jdbcTemplate.update(DIRECT_UPSERT_MAPPING.get(dbPlatForm), params); 676 } 677 678 private Timestamp formatInstant(final Instant instant) { 679 if (instant == null) { 680 return null; 681 } 682 return Timestamp.from(instant.truncatedTo(ChronoUnit.MILLIS)); 683 } 684 685 private void insertRdfTypeAssociationsInTransaction(final Set<URI> rdfTypes, 686 final String txId, 687 final FedoraId fedoraId) { 688 //remove and add type associations for the fedora id. 689 final List<MapSqlParameterSource> parameterSourcesList = new ArrayList<>(rdfTypes.size()); 690 final var parameterSource = new MapSqlParameterSource(); 691 parameterSource.addValue(TRANSACTION_ID_PARAM, txId); 692 parameterSource.addValue(FEDORA_ID_PARAM, fedoraId.getFullId()); 693 jdbcTemplate.update(DELETE_RESOURCE_TYPE_ASSOCIATIONS_IN_TRANSACTION, parameterSource); 694 695 for (final var rdfType : rdfTypes) { 696 final var assocParams = new MapSqlParameterSource(); 697 assocParams.addValue(TRANSACTION_ID_PARAM, txId); 698 assocParams.addValue(FEDORA_ID_PARAM, fedoraId.getFullId()); 699 assocParams.addValue(RDF_TYPE_URI_PARAM, rdfType.toString()); 700 parameterSourcesList.add(assocParams); 701 } 702 final MapSqlParameterSource[] psArray = parameterSourcesList.toArray(new MapSqlParameterSource[0]); 703 jdbcTemplate.batchUpdate(INSERT_RDF_TYPE_ASSOC_IN_TRANSACTION, psArray); 704 } 705 706 private void deleteRdfTypeAssociations(final FedoraId fedoraId) { 707 final var deleteParams = new MapSqlParameterSource(); 708 deleteParams.addValue(FEDORA_ID_PARAM, fedoraId.getFullId()); 709 jdbcTemplate.update(DELETE_RDF_TYPE_ASSOCIATIONS, 710 deleteParams); 711 } 712 713 private void insertRdfTypeAssociations(final List<URI> rdfTypes, 714 final Set<URI> newTypes, 715 final FedoraId fedoraId) { 716 //add rdf type associations 717 718 final var resourceSearchId = jdbcTemplate.queryForObject( 719 SELECT_RESOURCE_SEARCH_ID, 720 Map.of(FEDORA_ID_PARAM, fedoraId.getFullId()), Long.class); 721 722 final List<MapSqlParameterSource> parameterSourcesList = new ArrayList<>(); 723 for (final var rdfType : rdfTypes) { 724 final Long rdfTypeId; 725 if (newTypes.contains(rdfType)) { 726 // The cache MUST NOT be used when the current TX created the record as it will not be committed yet 727 // and it will break other transactions. 728 rdfTypeId = getRdfTypeIdDirect(rdfType); 729 } else { 730 rdfTypeId = getRdfTypeIdCached(rdfType); 731 } 732 733 final var assocParams = new MapSqlParameterSource(); 734 assocParams.addValue(RESOURCE_SEARCH_ID_PARAM, resourceSearchId); 735 assocParams.addValue(RDF_TYPE_ID_PARAM, rdfTypeId); 736 parameterSourcesList.add(assocParams); 737 } 738 739 final MapSqlParameterSource[] psArray = parameterSourcesList.toArray(new MapSqlParameterSource[0]); 740 jdbcTemplate.batchUpdate(INSERT_RDF_TYPE_ASSOC, psArray); 741 } 742 743 private Long getRdfTypeIdCached(final URI rdfType) { 744 return rdfTypeIdCache.computeIfAbsent(rdfType, this::getRdfTypeIdDirect); 745 } 746 747 private Long getRdfTypeIdDirect(final URI rdfType) { 748 return jdbcTemplate.queryForObject( 749 SELECT_RDF_TYPE_ID, 750 Map.of(RDF_TYPE_URI_PARAM, rdfType.toString()), Long.class); 751 } 752 753 @Override 754 public void removeFromIndex(final Transaction transaction, final FedoraId fedoraId) { 755 transaction.doInTx(() -> { 756 if (!transaction.isShortLived()) { 757 try { 758 doUpsertIntoTransactionTables(transaction.getId(), fedoraId, null, "delete"); 759 } catch (final Exception e) { 760 throw new RepositoryRuntimeException("Failed to remove " + fedoraId + " from search index", e); 761 } 762 } else { 763 doDirectRemove(fedoraId); 764 } 765 }); 766 } 767 768 private void doDirectRemove(final FedoraId fedoraId) { 769 deleteRdfTypeAssociations(fedoraId); 770 deleteResource(fedoraId); 771 } 772 773 private void deleteResource(final FedoraId fedoraId) { 774 final var params = new MapSqlParameterSource(); 775 params.addValue(FEDORA_ID_PARAM, fedoraId.getFullId()); 776 jdbcTemplate.update(DELETE_RESOURCE_FROM_SEARCH, params); 777 } 778 779 @Override 780 public void reset() { 781 rdfTypeIdCache.clear(); 782 783 try (final var conn = this.dataSource.getConnection(); 784 final var statement = conn.createStatement()) { 785 for (final var sql : toggleForeignKeyChecks(false)) { 786 statement.addBatch(sql); 787 } 788 statement.addBatch(truncateTable(SEARCH_RESOURCE_RDF_TYPE_TABLE)); 789 statement.addBatch(truncateTable(SEARCH_RDF_TYPE_TABLE)); 790 statement.addBatch(truncateTable(SIMPLE_SEARCH_TABLE)); 791 statement.addBatch(truncateTable(SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE)); 792 statement.addBatch(truncateTable(SIMPLE_SEARCH_TRANSACTIONS_TABLE)); 793 for (final var sql : toggleForeignKeyChecks(true)) { 794 statement.addBatch(sql); 795 } 796 statement.executeBatch(); 797 } catch (final SQLException e) { 798 throw new RepositoryRuntimeException("Failed to truncate search index tables", e); 799 } 800 } 801 802 @Override 803 public void commitTransaction(final Transaction tx) { 804 if (!tx.isShortLived()) { 805 tx.ensureCommitting(); 806 final var txId = tx.getId(); 807 try { 808 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 809 parameterSource.addValue(TRANSACTION_ID_PARAM, txId); 810 final int deletedAssociations = jdbcTemplate.update(COMMIT_DELETE_RDF_TYPE_ASSOCIATIONS, 811 parameterSource); 812 final int deletedResources = jdbcTemplate.update(COMMIT_DELETE_RESOURCES_IN_TRANSACTION, 813 parameterSource); 814 final int addedRdfTypes = jdbcTemplate.update(COMMIT_RDF_TYPES, parameterSource); 815 final int addedResources = jdbcTemplate.update(UPSERT_COMMIT_MAPPING.get(dbPlatForm), 816 parameterSource); 817 final int addRdfTypeAssociations = jdbcTemplate.update(COMMIT_RDF_TYPE_ASSOCIATIONS, parameterSource); 818 cleanupTransaction(txId); 819 LOGGER.debug("Commit of tx {} complete with {} resource adds, {} resource associations adds, " + 820 "{} rdf types adds{}, resource deletes, {} resource/rdf type associations deletes", 821 txId, addedResources, addRdfTypeAssociations, addedRdfTypes, deletedResources, 822 deletedAssociations); 823 } catch (final Exception e) { 824 LOGGER.warn("Unable to commit search index transaction {}: {}", txId, e.getMessage()); 825 throw new RepositoryRuntimeException("Unable to commit search index transaction", e); 826 } 827 } 828 } 829 830 @Transactional(propagation = Propagation.NOT_SUPPORTED) 831 @Override 832 public void rollbackTransaction(final Transaction tx) { 833 if (!tx.isShortLived()) { 834 cleanupTransaction(tx.getId()); 835 } 836 } 837 838 private void cleanupTransaction(final String txId) { 839 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 840 parameterSource.addValue(TRANSACTION_ID_PARAM, txId); 841 jdbcTemplate.update(DELETE_TRANSACTION, parameterSource); 842 jdbcTemplate.update(DELETE_RDF_TYPE_ASSOCIATIONS_IN_TRANSACTION, parameterSource); 843 LOGGER.debug("Transaction data has been removed from the search transaction tables for txId={} ", txId); 844 } 845 846 private List<String> toggleForeignKeyChecks(final boolean enable) { 847 848 if (isPostgres()) { 849 return Collections.emptyList(); 850 } else { 851 return List.of("SET FOREIGN_KEY_CHECKS = " + (enable ? 1 : 0) + ";"); 852 } 853 } 854 855 private boolean isPostgres() { 856 return dbPlatForm.equals(POSTGRESQL); 857 } 858 859 private String truncateTable(final String tableName) { 860 final var addCascade = isPostgres(); 861 return "TRUNCATE TABLE " + tableName + (addCascade ? " CASCADE" : "") + ";"; 862 } 863 864}