001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.impl.services;
019
020import static org.slf4j.LoggerFactory.getLogger;
021
022import java.sql.ResultSet;
023import java.sql.SQLException;
024import java.sql.Timestamp;
025import java.time.Instant;
026import java.util.List;
027import java.util.Map;
028import java.util.stream.Stream;
029
030import javax.annotation.PostConstruct;
031import javax.inject.Inject;
032import javax.sql.DataSource;
033import javax.transaction.Transactional;
034import org.apache.jena.graph.Node;
035import org.apache.jena.graph.NodeFactory;
036import org.apache.jena.graph.Triple;
037import org.fcrepo.common.db.DbPlatform;
038import org.fcrepo.kernel.api.identifiers.FedoraId;
039import org.slf4j.Logger;
040import org.springframework.core.io.DefaultResourceLoader;
041import org.springframework.jdbc.core.RowCallbackHandler;
042import org.springframework.jdbc.core.RowMapper;
043import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
044import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
045import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
046import org.springframework.stereotype.Component;
047
048import com.google.common.base.Preconditions;
049
050/**
051 * Manager for the membership index
052 *
053 * @author bbpennel
054 */
055@Component
056public class MembershipIndexManager {
057    private static final Logger log = getLogger(MembershipIndexManager.class);
058
059    private static final Timestamp NO_END_TIMESTAMP = Timestamp.from(MembershipServiceImpl.NO_END_INSTANT);
060    private static final Timestamp NO_START_TIMESTAMP = Timestamp.from(Instant.parse("1000-01-01T00:00:00.000Z"));
061
062    private static final String ADD_OPERATION = "add";
063    private static final String DELETE_OPERATION = "delete";
064    private static final String FORCE_FLAG = "force";
065
066    private static final String TX_ID_PARAM = "txId";
067    private static final String SUBJECT_ID_PARAM = "subjectId";
068    private static final String NO_END_TIME_PARAM = "noEndTime";
069    private static final String ADD_OP_PARAM = "addOp";
070    private static final String DELETE_OP_PARAM = "deleteOp";
071    private static final String MEMENTO_TIME_PARAM = "mementoTime";
072    private static final String PROPERTY_PARAM = "property";
073    private static final String TARGET_ID_PARAM = "targetId";
074    private static final String SOURCE_ID_PARAM = "sourceId";
075    private static final String START_TIME_PARAM = "startTime";
076    private static final String END_TIME_PARAM = "endTime";
077    private static final String OPERATION_PARAM = "operation";
078    private static final String FORCE_PARAM = "forceFlag";
079    private static final String OBJECT_ID_PARAM = "objectId";
080
081    private static final String SELECT_ALL_MEMBERSHIP = "SELECT * FROM membership";
082
083    private static final String SELECT_ALL_OPERATIONS = "SELECT * FROM membership_tx_operations";
084
085    private static final String SELECT_MEMBERSHIP_IN_TX =
086            "SELECT m.property, m.object_id" +
087            " FROM membership m" +
088            " WHERE subject_id = :subjectId" +
089                " AND end_time = :noEndTime" +
090                " AND NOT EXISTS (" +
091                    " SELECT 1" +
092                    " FROM membership_tx_operations mto" +
093                    " WHERE mto.subject_id = :subjectId" +
094                        " AND mto.source_id = m.source_id" +
095                        " AND mto.object_id = m.object_id" +
096                        " AND mto.tx_id = :txId" +
097                        " AND mto.operation = :deleteOp)" +
098            " UNION" +
099            " SELECT property, object_id" +
100            " FROM membership_tx_operations" +
101            " WHERE subject_id = :subjectId" +
102                " AND tx_id = :txId" +
103                " AND end_time = :noEndTime" +
104                " AND operation = :addOp";
105
106    private static final String SELECT_MEMBERSHIP_MEMENTO_IN_TX =
107            "SELECT property, object_id" +
108            " FROM membership m" +
109            " WHERE m.subject_id = :subjectId" +
110                " AND m.start_time <= :mementoTime" +
111                " AND m.end_time > :mementoTime" +
112                " AND NOT EXISTS (" +
113                    " SELECT 1" +
114                    " FROM membership_tx_operations mto" +
115                    " WHERE mto.subject_id = :subjectId" +
116                        " AND mto.source_id = m.source_id" +
117                        " AND mto.property = m.property" +
118                        " AND mto.object_id = m.object_id" +
119                        " AND mto.end_time <= :mementoTime" +
120                        " AND mto.tx_id = :txId" +
121                        " AND mto.operation = :deleteOp)" +
122            " UNION" +
123            " SELECT property, object_id" +
124            " FROM membership_tx_operations" +
125            " WHERE subject_id = :subjectId" +
126                " AND tx_id = :txId" +
127                " AND start_time <= :mementoTime" +
128                " AND end_time > :mementoTime" +
129                " AND operation = :addOp";
130
131    private static final String INSERT_MEMBERSHIP_IN_TX =
132            "INSERT INTO membership_tx_operations" +
133            " (subject_id, property, object_id, source_id, start_time, end_time, tx_id, operation)" +
134            " VALUES (:subjectId, :property, :targetId, :sourceId, :startTime, :endTime, :txId, :operation)";
135
136    private static final String END_EXISTING_MEMBERSHIP =
137            "INSERT INTO membership_tx_operations" +
138            " (subject_id, property, object_id, source_id, start_time, end_time, tx_id, operation)" +
139            " SELECT m.subject_id, m.property, m.object_id, m.source_id, m.start_time, :endTime, :txId, :deleteOp" +
140            " FROM membership m" +
141            " WHERE m.source_id = :sourceId" +
142                " AND m.end_time = :noEndTime" +
143                " AND m.subject_id = :subjectId" +
144                " AND m.property = :property" +
145                " AND m.object_id = :objectId";
146
147    private static final String CLEAR_ENTRY_IN_TX =
148            "DELETE FROM membership_tx_operations" +
149            " WHERE source_id = :sourceId" +
150                " AND tx_id = :txId" +
151                " AND subject_id = :subjectId" +
152                " AND property = :property" +
153                " AND object_id = :objectId" +
154                " AND operation = :operation" +
155                " AND force_flag IS NULL";
156
157    private static final String CLEAR_ALL_ADDED_FOR_SOURCE_IN_TX =
158            "DELETE FROM membership_tx_operations" +
159            " WHERE source_id = :sourceId" +
160                " AND tx_id = :txId" +
161                " AND operation = :addOp";
162
163    // Add "delete" entries for all existing membership from the given source, if not already deleted
164    private static final String END_EXISTING_FOR_SOURCE =
165            "INSERT INTO membership_tx_operations" +
166            " (subject_id, property, object_id, source_id, start_time, end_time, tx_id, operation)" +
167            " SELECT subject_id, property, object_id, source_id, start_time, :endTime, :txId, :deleteOp" +
168            " FROM membership m" +
169            " WHERE source_id = :sourceId" +
170                " AND end_time = :noEndTime" +
171                " AND NOT EXISTS (" +
172                    " SELECT TRUE" +
173                    " FROM membership_tx_operations mtx" +
174                    " WHERE mtx.subject_id = m.subject_id" +
175                        " AND mtx.property = m.property" +
176                        " AND mtx.object_id = m.object_id" +
177                        " AND mtx.source_id = m.source_id" +
178                        " AND mtx.operation = :deleteOp" +
179                    ")";
180
181    private static final String DELETE_EXISTING_FOR_SOURCE_AFTER =
182            "INSERT INTO membership_tx_operations" +
183            " (subject_id, property, object_id, source_id, start_time, end_time, tx_id, operation, force_flag)" +
184            " SELECT subject_id, property, object_id, source_id, start_time, end_time, :txId, :deleteOp, :forceFlag" +
185            " FROM membership m" +
186            " WHERE m.source_id = :sourceId" +
187                " AND (m.start_time >= :startTime" +
188                " OR m.end_time >= :startTime)";
189
190    private static final String PURGE_ALL_REFERENCES_MEMBERSHIP =
191            "DELETE from membership" +
192            " where source_id = :targetId" +
193                " OR subject_id = :targetId" +
194                " OR object_id = :targetId";
195
196    private static final String PURGE_ALL_REFERENCES_TRANSACTION =
197            "DELETE from membership_tx_operations" +
198            " WHERE tx_id = :txId" +
199                " AND (source_id = :targetId" +
200                " OR subject_id = :targetId" +
201                " OR object_id = :targetId)";
202
203    private static final String COMMIT_DELETES =
204            "DELETE from membership" +
205            " WHERE EXISTS (" +
206                " SELECT TRUE" +
207                " FROM membership_tx_operations mto" +
208                " WHERE mto.tx_id = :txId" +
209                    " AND mto.operation = :deleteOp" +
210                    " AND mto.force_flag = :forceFlag" +
211                    " AND membership.source_id = mto.source_id" +
212                    " AND membership.subject_id = mto.subject_id" +
213                    " AND membership.property = mto.property" +
214                    " AND membership.object_id = mto.object_id" +
215                " )";
216
217    private static final String COMMIT_ENDS_H2 =
218            "UPDATE membership m" +
219            " SET end_time = (" +
220                " SELECT mto.end_time" +
221                " FROM membership_tx_operations mto" +
222                " WHERE mto.tx_id = :txId" +
223                    " AND m.source_id = mto.source_id" +
224                    " AND m.subject_id = mto.subject_id" +
225                    " AND m.property = mto.property" +
226                    " AND m.object_id = mto.object_id" +
227                    " AND mto.operation = :deleteOp" +
228                " )" +
229            " WHERE EXISTS (" +
230                "SELECT TRUE" +
231                " FROM membership_tx_operations mto" +
232                " WHERE mto.tx_id = :txId" +
233                    " AND mto.operation = :deleteOp" +
234                    " AND m.source_id = mto.source_id" +
235                    " AND m.subject_id = mto.subject_id" +
236                    " AND m.property = mto.property" +
237                    " AND m.object_id = mto.object_id" +
238                " )";
239
240    private static final String COMMIT_ENDS_POSTGRES =
241            "UPDATE membership" +
242            " SET end_time = mto.end_time" +
243            " FROM membership_tx_operations mto" +
244            " WHERE mto.tx_id = :txId" +
245                " AND mto.operation = :deleteOp" +
246                " AND membership.source_id = mto.source_id" +
247                " AND membership.subject_id = mto.subject_id" +
248                " AND membership.property = mto.property" +
249                " AND membership.object_id = mto.object_id";
250
251    private static final String COMMIT_ENDS_MYSQL =
252            "UPDATE membership m" +
253            " INNER JOIN membership_tx_operations mto ON" +
254                " m.source_id = mto.source_id" +
255                " AND m.subject_id = mto.subject_id" +
256                " AND m.subject_id = mto.subject_id" +
257                " AND m.property = mto.property" +
258                " AND m.object_id = mto.object_id" +
259            " SET m.end_time = mto.end_time" +
260            " WHERE mto.tx_id = :txId" +
261                " AND mto.operation = :deleteOp";
262
263    private static final Map<DbPlatform, String> COMMIT_ENDS_MAP = Map.of(
264            DbPlatform.MYSQL, COMMIT_ENDS_MYSQL,
265            DbPlatform.MARIADB, COMMIT_ENDS_MYSQL,
266            DbPlatform.POSTGRESQL, COMMIT_ENDS_POSTGRES,
267            DbPlatform.H2, COMMIT_ENDS_H2
268    );
269
270    // Transfer all "add" operations from tx to committed membership, unless the entry already exists
271    private static final String COMMIT_ADDS =
272            "INSERT INTO membership" +
273            " (subject_id, property, object_id, source_id, start_time, end_time)" +
274            " SELECT subject_id, property, object_id, source_id, start_time, end_time" +
275            " FROM membership_tx_operations mto" +
276            " WHERE mto.tx_id = :txId" +
277                " AND mto.operation = :addOp" +
278                " AND NOT EXISTS (" +
279                    " SELECT TRUE" +
280                    " FROM membership m" +
281                    " WHERE m.source_id = mto.source_id" +
282                        " AND m.subject_id = mto.subject_id" +
283                        " AND m.property = mto.property" +
284                        " AND m.object_id = mto.object_id" +
285                        " AND m.start_time = mto.start_time" +
286                        " AND m.end_time = mto.end_time" +
287                " )";
288
289    private static final String DELETE_TRANSACTION =
290            "DELETE FROM membership_tx_operations" +
291            " WHERE tx_id = :txId";
292
293    private static final String TRUNCATE_MEMBERSHIP = "TRUNCATE TABLE membership";
294
295    private static final String TRUNCATE_MEMBERSHIP_TX = "TRUNCATE TABLE membership_tx_operations";
296
297    @Inject
298    private DataSource dataSource;
299
300    private NamedParameterJdbcTemplate jdbcTemplate;
301
302    private DbPlatform dbPlatform;
303
304    private static final Map<DbPlatform, String> DDL_MAP = Map.of(
305            DbPlatform.MYSQL, "sql/mysql-membership.sql",
306            DbPlatform.H2, "sql/default-membership.sql",
307            DbPlatform.POSTGRESQL, "sql/default-membership.sql",
308            DbPlatform.MARIADB, "sql/mariadb-membership.sql"
309    );
310
311    @PostConstruct
312    public void setUp() {
313        jdbcTemplate = new NamedParameterJdbcTemplate(getDataSource());
314
315        dbPlatform = DbPlatform.fromDataSource(dataSource);
316
317        Preconditions.checkArgument(DDL_MAP.containsKey(dbPlatform),
318                "Missing DDL mapping for %s", dbPlatform);
319
320        final var ddl = DDL_MAP.get(dbPlatform);
321        log.debug("Applying ddl: {}", ddl);
322        DatabasePopulatorUtils.execute(
323                new ResourceDatabasePopulator(new DefaultResourceLoader().getResource("classpath:" + ddl)),
324                dataSource);
325    }
326
327    /**
328     * End a membership entry, setting an end time if committed, or clearing from the current tx
329     * if it was newly added.
330     *
331     * @param txId transaction id
332     * @param sourceId ID of the direct/indirect container whose membership should be ended
333     * @param membership membership triple to end
334     * @param endTime the time the resource was deleted, generally its last modified
335     */
336    @Transactional
337    public void endMembership(final String txId,  final FedoraId sourceId, final Triple membership,
338            final Instant endTime) {
339        final Map<String, Object> parameterSource = Map.of(
340                TX_ID_PARAM, txId,
341                SOURCE_ID_PARAM, sourceId.getFullId(),
342                SUBJECT_ID_PARAM, membership.getSubject().getURI(),
343                PROPERTY_PARAM, membership.getPredicate().getURI(),
344                OBJECT_ID_PARAM, membership.getObject().getURI(),
345                OPERATION_PARAM, ADD_OPERATION);
346
347        final int affected = jdbcTemplate.update(CLEAR_ENTRY_IN_TX, parameterSource);
348
349        // If no rows were deleted, then assume we need to delete permanent entry
350        if (affected == 0) {
351            final Map<String, Object> parameterSource2 = Map.of(
352                    TX_ID_PARAM, txId,
353                    SOURCE_ID_PARAM, sourceId.getFullId(),
354                    SUBJECT_ID_PARAM, membership.getSubject().getURI(),
355                    PROPERTY_PARAM, membership.getPredicate().getURI(),
356                    OBJECT_ID_PARAM, membership.getObject().getURI(),
357                    END_TIME_PARAM, formatInstant(endTime),
358                    NO_END_TIME_PARAM, NO_END_TIMESTAMP,
359                    DELETE_OP_PARAM, DELETE_OPERATION);
360            jdbcTemplate.update(END_EXISTING_MEMBERSHIP, parameterSource2);
361        }
362    }
363
364    /**
365     * End all membership properties resulting from the specified source container
366     * @param txId transaction id
367     * @param sourceId ID of the direct/indirect container whose membership should be ended
368     * @param endTime the time the resource was deleted, generally its last modified
369     */
370    @Transactional
371    public void endMembershipForSource(final String txId, final FedoraId sourceId, final Instant endTime) {
372        final Map<String, Object> parameterSource = Map.of(
373                TX_ID_PARAM, txId,
374                SOURCE_ID_PARAM, sourceId.getFullId(),
375                ADD_OP_PARAM, ADD_OPERATION);
376
377        jdbcTemplate.update(CLEAR_ALL_ADDED_FOR_SOURCE_IN_TX, parameterSource);
378
379        final Map<String, Object> parameterSource2 = Map.of(
380                TX_ID_PARAM, txId,
381                SOURCE_ID_PARAM, sourceId.getFullId(),
382                END_TIME_PARAM, formatInstant(endTime),
383                NO_END_TIME_PARAM, NO_END_TIMESTAMP,
384                DELETE_OP_PARAM, DELETE_OPERATION);
385        jdbcTemplate.update(END_EXISTING_FOR_SOURCE, parameterSource2);
386    }
387
388    /**
389     * Delete membership entries that are active at or after the given timestamp for the specified source
390     * @param txId transaction id
391     * @param sourceId ID of the direct/indirect container
392     * @param afterTime time at or after which membership should be removed
393     */
394    @Transactional
395    public void deleteMembershipForSourceAfter(final String txId, final FedoraId sourceId, final Instant afterTime) {
396        // Clear all membership added in this transaction
397        final Map<String, Object> parameterSource = Map.of(
398                TX_ID_PARAM, txId,
399                SOURCE_ID_PARAM, sourceId.getFullId(),
400                ADD_OP_PARAM, ADD_OPERATION);
401
402        jdbcTemplate.update(CLEAR_ALL_ADDED_FOR_SOURCE_IN_TX, parameterSource);
403
404        final var afterTimestamp = afterTime == null ? NO_START_TIMESTAMP : formatInstant(afterTime);
405
406        // Delete all existing membership entries that start after or end after the given timestamp
407        final Map<String, Object> parameterSource2 = Map.of(
408                TX_ID_PARAM, txId,
409                SOURCE_ID_PARAM, sourceId.getFullId(),
410                START_TIME_PARAM, afterTimestamp,
411                FORCE_PARAM, FORCE_FLAG,
412                DELETE_OP_PARAM, DELETE_OPERATION);
413        jdbcTemplate.update(DELETE_EXISTING_FOR_SOURCE_AFTER, parameterSource2);
414    }
415
416    /**
417     * Clean up any references to the target id, in transactions and outside
418     * @param txId transaction id
419     * @param targetId identifier of the resource to cleanup membership references for
420     */
421    @Transactional
422    public void deleteMembershipReferences(final String txId, final FedoraId targetId) {
423        final Map<String, Object> parameterSource = Map.of(
424                TARGET_ID_PARAM, targetId.getFullId(),
425                TX_ID_PARAM, txId);
426
427        jdbcTemplate.update(PURGE_ALL_REFERENCES_TRANSACTION, parameterSource);
428        jdbcTemplate.update(PURGE_ALL_REFERENCES_MEMBERSHIP, parameterSource);
429    }
430
431    /**
432     * Add new membership property to the index, clearing any delete
433     * operations for the property if necessary.
434     * @param txId transaction id
435     * @param sourceId ID of the direct/indirect container which produced the membership
436     * @param membership membership triple
437     * @param startTime time the membership triple was added
438     */
439    @Transactional
440    public void addMembership(final String txId, final FedoraId sourceId, final Triple membership,
441            final Instant startTime) {
442        // Clear any existing delete operation for this membership
443        final Map<String, Object> parametersDelete = Map.of(
444                TX_ID_PARAM, txId,
445                SOURCE_ID_PARAM, sourceId.getFullId(),
446                SUBJECT_ID_PARAM, membership.getSubject().getURI(),
447                PROPERTY_PARAM, membership.getPredicate().getURI(),
448                OBJECT_ID_PARAM, membership.getObject().getURI(),
449                OPERATION_PARAM, DELETE_OPERATION);
450
451        jdbcTemplate.update(CLEAR_ENTRY_IN_TX, parametersDelete);
452
453        // Add the new membership operation
454        addMembership(txId, sourceId, membership, startTime, null);
455    }
456
457    /**
458     * Add new membership property to the index
459     * @param txId transaction id
460     * @param sourceId ID of the direct/indirect container which produced the membership
461     * @param membership membership triple
462     * @param startTime time the membership triple was added
463     * @param endTime time the membership triple ends, or never if not provided
464     */
465    public void addMembership(final String txId, final FedoraId sourceId, final Triple membership,
466            final Instant startTime, final Instant endTime) {
467        final var endTimestamp = endTime == null ? NO_END_TIMESTAMP : formatInstant(endTime);
468        // Add the new membership operation
469        final Map<String, Object> parameterSource = Map.of(
470                SUBJECT_ID_PARAM, membership.getSubject().getURI(),
471                PROPERTY_PARAM, membership.getPredicate().getURI(),
472                TARGET_ID_PARAM, membership.getObject().getURI(),
473                SOURCE_ID_PARAM, sourceId.getFullId(),
474                START_TIME_PARAM, formatInstant(startTime),
475                END_TIME_PARAM, endTimestamp,
476                TX_ID_PARAM, txId,
477                OPERATION_PARAM, ADD_OPERATION);
478
479        jdbcTemplate.update(INSERT_MEMBERSHIP_IN_TX, parameterSource);
480    }
481
482    /**
483     * Get a stream of membership triples with
484     * @param txId transaction from which membership will be retrieved, or null for no transaction
485     * @param subjectId ID of the subject
486     * @return Stream of membership triples
487     */
488    public Stream<Triple> getMembership(final String txId, final FedoraId subjectId) {
489        final Node subjectNode = NodeFactory.createURI(subjectId.getBaseId());
490
491        final RowMapper<Triple> membershipMapper = (rs, rowNum) ->
492                Triple.create(subjectNode,
493                              NodeFactory.createURI(rs.getString("property")),
494                              NodeFactory.createURI(rs.getString("object_id")));
495
496        List<Triple> membership = null;
497        if (subjectId.isMemento()) {
498            final Map<String, Object> parameterSource = Map.of(
499                    SUBJECT_ID_PARAM, subjectId.getBaseId(),
500                    MEMENTO_TIME_PARAM, formatInstant(subjectId.getMementoInstant()),
501                    TX_ID_PARAM, txId,
502                    ADD_OP_PARAM, ADD_OPERATION,
503                    DELETE_OP_PARAM, DELETE_OPERATION);
504
505            membership = jdbcTemplate.query(SELECT_MEMBERSHIP_MEMENTO_IN_TX, parameterSource, membershipMapper);
506        } else {
507            final Map<String, Object> parameterSource = Map.of(
508                    SUBJECT_ID_PARAM, subjectId.getFullId(),
509                    NO_END_TIME_PARAM, NO_END_TIMESTAMP,
510                    TX_ID_PARAM, txId,
511                    ADD_OP_PARAM, ADD_OPERATION,
512                    DELETE_OP_PARAM, DELETE_OPERATION);
513
514            membership = jdbcTemplate.query(SELECT_MEMBERSHIP_IN_TX, parameterSource, membershipMapper);
515        }
516
517        return membership.stream();
518    }
519
520    /**
521     * Perform a commit of operations stored in the specified transaction
522     * @param txId transaction id
523     */
524    @Transactional
525    public void commitTransaction(final String txId) {
526        final Map<String, String> parameterSource = Map.of(TX_ID_PARAM, txId,
527                ADD_OP_PARAM, ADD_OPERATION,
528                DELETE_OP_PARAM, DELETE_OPERATION,
529                FORCE_PARAM, FORCE_FLAG);
530
531        jdbcTemplate.update(COMMIT_DELETES, parameterSource);
532        final int ends = jdbcTemplate.update(COMMIT_ENDS_MAP.get(this.dbPlatform), parameterSource);
533        final int adds = jdbcTemplate.update(COMMIT_ADDS, parameterSource);
534        final int cleaned = jdbcTemplate.update(DELETE_TRANSACTION, parameterSource);
535
536        log.debug("Completed commit, {} ended, {} adds, {} operations", ends, adds, cleaned);
537    }
538
539    /**
540     * Delete all entries related to a transaction
541     * @param txId transaction id
542     */
543    public void deleteTransaction(final String txId) {
544        final Map<String, String> parameterSource = Map.of(TX_ID_PARAM, txId);
545        jdbcTemplate.update(DELETE_TRANSACTION, parameterSource);
546    }
547
548    /**
549     * Format an instant to a timestamp without milliseconds, due to precision
550     * issues with memento datetimes.
551     * @param instant
552     * @return
553     */
554    private Timestamp formatInstant(final Instant instant) {
555        final var timestamp = Timestamp.from(instant);
556        timestamp.setNanos(0);
557        return timestamp;
558    }
559
560    /**
561     * Clear all entries from the index
562     */
563    @Transactional
564    public void clearIndex() {
565        jdbcTemplate.update(TRUNCATE_MEMBERSHIP, Map.of());
566        jdbcTemplate.update(TRUNCATE_MEMBERSHIP_TX, Map.of());
567    }
568
569    /**
570     * Log all membership entries, for debugging usage only
571     */
572    public void logMembership() {
573        log.info("source_id, subject_id, property, object_id, start_time, end_time");
574        jdbcTemplate.query(SELECT_ALL_MEMBERSHIP, new RowCallbackHandler() {
575            @Override
576            public void processRow(final ResultSet rs) throws SQLException {
577                log.info("{}, {}, {}, {}, {}, {}", rs.getString("source_id"), rs.getString("subject_id"),
578                        rs.getString("property"), rs.getString("object_id"), rs.getTimestamp("start_time"),
579                        rs.getTimestamp("end_time"));
580            }
581        });
582    }
583
584    /**
585     * Log all membership operations, for debugging usage only
586     */
587    public void logOperations() {
588        log.info("source_id, subject_id, property, object_id, start_time, end_time, tx_id, operation, force_flag");
589        jdbcTemplate.query(SELECT_ALL_OPERATIONS, new RowCallbackHandler() {
590            @Override
591            public void processRow(final ResultSet rs) throws SQLException {
592                log.info("{}, {}, {}, {}, {}, {}, {}, {}, {}",
593                        rs.getString("source_id"), rs.getString("subject_id"), rs.getString("property"),
594                        rs.getString("object_id"), rs.getTimestamp("start_time"), rs.getTimestamp("end_time"),
595                        rs.getString("tx_id"), rs.getString("operation"), rs.getString("force_flag"));
596            }
597        });
598    }
599
600    /**
601     * Set the JDBC datastore.
602     * @param dataSource the dataStore.
603     */
604    public void setDataSource(final DataSource dataSource) {
605        this.dataSource = dataSource;
606    }
607
608    /**
609     * Get the JDBC datastore.
610     * @return the dataStore.
611     */
612    public DataSource getDataSource() {
613        return dataSource;
614    }
615}