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