• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kit-data-manager / pit-service / #552

29 Jul 2025 01:16PM UTC coverage: 77.908% (+5.2%) from 72.712%
#552

Pull #264

github

web-flow
Merge 0f68003b1 into 201395eb2
Pull Request #264: Development branch for v3.0.0

654 of 791 new or added lines in 25 files covered. (82.68%)

3 existing lines in 2 files now uncovered.

1065 of 1367 relevant lines covered (77.91%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.19
/src/main/java/edu/kit/datamanager/pit/web/impl/TypingRESTResourceImpl.java
1
/*
2
 * Copyright (c) 2025 Karlsruhe Institute of Technology.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package edu.kit.datamanager.pit.web.impl;
18

19
import edu.kit.datamanager.entities.messaging.PidRecordMessage;
20
import edu.kit.datamanager.exceptions.CustomInternalServerError;
21
import edu.kit.datamanager.pit.common.*;
22
import edu.kit.datamanager.pit.configuration.ApplicationProperties;
23
import edu.kit.datamanager.pit.configuration.PidGenerationProperties;
24
import edu.kit.datamanager.pit.domain.PIDRecord;
25
import edu.kit.datamanager.pit.elasticsearch.PidRecordElasticRepository;
26
import edu.kit.datamanager.pit.elasticsearch.PidRecordElasticWrapper;
27
import edu.kit.datamanager.pit.pidgeneration.PidSuffix;
28
import edu.kit.datamanager.pit.pidgeneration.PidSuffixGenerator;
29
import edu.kit.datamanager.pit.pidlog.KnownPid;
30
import edu.kit.datamanager.pit.pidlog.KnownPidsDao;
31
import edu.kit.datamanager.pit.pitservice.ITypingService;
32
import edu.kit.datamanager.pit.resolver.Resolver;
33
import edu.kit.datamanager.pit.web.BatchRecordResponse;
34
import edu.kit.datamanager.pit.web.ITypingRestResource;
35
import edu.kit.datamanager.pit.web.TabulatorPaginationFormat;
36
import edu.kit.datamanager.service.IMessagingService;
37
import edu.kit.datamanager.util.AuthenticationHelper;
38
import edu.kit.datamanager.util.ControllerUtils;
39
import jakarta.servlet.http.HttpServletResponse;
40
import org.apache.commons.lang3.stream.Streams;
41
import org.apache.http.client.cache.HeaderConstants;
42
import org.slf4j.Logger;
43
import org.slf4j.LoggerFactory;
44
import org.springframework.data.domain.Page;
45
import org.springframework.data.domain.PageImpl;
46
import org.springframework.data.domain.Pageable;
47
import org.springframework.http.HttpStatus;
48
import org.springframework.http.ResponseEntity;
49
import org.springframework.web.bind.annotation.RestController;
50
import org.springframework.web.context.request.RequestAttributes;
51
import org.springframework.web.context.request.WebRequest;
52
import org.springframework.web.servlet.HandlerMapping;
53
import org.springframework.web.util.UriComponentsBuilder;
54

55
import java.io.IOException;
56
import java.time.Instant;
57
import java.time.temporal.ChronoUnit;
58
import java.util.*;
59
import java.util.stream.Stream;
60

61
@RestController
62
public class TypingRESTResourceImpl implements ITypingRestResource {
63

64
    private static final Logger LOG = LoggerFactory.getLogger(TypingRESTResourceImpl.class);
1✔
65

66
    private final ITypingService typingService;
67
    private final Resolver resolver;
68
    private final ApplicationProperties applicationProps;
69
    private final IMessagingService messagingService;
70
    private final KnownPidsDao localPidStorage;
71
    private final Optional<PidRecordElasticRepository> elastic;
72
    private final PidSuffixGenerator suffixGenerator;
73
    private final PidGenerationProperties pidGenerationProperties;
74

75
    public TypingRESTResourceImpl(ITypingService typingService, Resolver resolver, ApplicationProperties applicationProps, IMessagingService messagingService, KnownPidsDao localPidStorage, Optional<PidRecordElasticRepository> elastic, PidSuffixGenerator suffixGenerator, PidGenerationProperties pidGenerationProperties) {
76
        super();
1✔
77
        this.typingService = typingService;
1✔
78
        this.resolver = resolver;
1✔
79
        this.applicationProps = applicationProps;
1✔
80
        this.messagingService = messagingService;
1✔
81
        this.localPidStorage = localPidStorage;
1✔
82
        this.elastic = elastic;
1✔
83
        this.suffixGenerator = suffixGenerator;
1✔
84
        this.pidGenerationProperties = pidGenerationProperties;
1✔
85
    }
1✔
86

87
    @Override
88
    public ResponseEntity<BatchRecordResponse> createPIDs(
89
            List<PIDRecord> rec,
90
            boolean dryrun,
91
            WebRequest request,
92
            HttpServletResponse response,
93
            UriComponentsBuilder uriBuilder
94
    ) throws IOException, RecordValidationException, ExternalServiceException {
95
        if (rec == null || rec.isEmpty()) {
1✔
96
            LOG.warn("No records provided for PID creation.");
1✔
97
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new BatchRecordResponse(Collections.emptyList(), Collections.emptyMap()));
1✔
98
        }
99
        if (rec.size() == 1) {
1✔
100
            // If only one record is provided, we can use the single record creation method.
101
            LOG.info("Only one record provided. Using single record creation method.");
1✔
102
            var result = createPID(rec.getFirst(), dryrun, request, response, uriBuilder);
1✔
103
            // Return the single record in a list
104
            assert result.getBody() != null;
1✔
105
            return ResponseEntity.status(result.getStatusCode()).headers(result.getHeaders()).body(new BatchRecordResponse(Collections.singletonList(result.getBody()), Collections.singletonMap(rec.getFirst().getPid(), result.getBody().getPid())));
1✔
106
        }
107
        Instant startTime = Instant.now();
1✔
108
        LOG.info("Creating PIDs for {} records.", rec.size());
1✔
109
        String prefix = this.typingService.getPrefix().orElseThrow(() -> new IOException("No prefix configured."));
1✔
110

111
        // Generate a map between temporary (user-defined) PIDs and final PIDs (generated)
112
        Map<String, String> pidMappings = generatePIDMapping(rec, dryrun);
1✔
113
        Instant mappingTime = Instant.now();
1✔
114

115
        // Apply the mappings to the records and validate them
116
        List<PIDRecord> validatedRecords = applyMappingsToRecordsAndValidate(rec, pidMappings, prefix);
1✔
117
        Instant validationTime = Instant.now();
1✔
118

119
        if (dryrun) {
1✔
120
            // dryrun only does validation. Stop now and return as we would later on.
NEW
121
            LOG.info("Time taken for dryrun: {} ms", ChronoUnit.MILLIS.between(startTime, validationTime));
×
NEW
122
            LOG.info("-- Time taken for mapping: {} ms", ChronoUnit.MILLIS.between(startTime, mappingTime));
×
NEW
123
            LOG.info("-- Time taken for validation: {} ms", ChronoUnit.MILLIS.between(mappingTime, validationTime));
×
NEW
124
            LOG.info("Dryrun finished. Returning validated records for {} records.", validatedRecords.size());
×
NEW
125
            return ResponseEntity.status(HttpStatus.OK).body(new BatchRecordResponse(validatedRecords, pidMappings));
×
126
        }
127

128
        List<PIDRecord> failedRecords = new ArrayList<>();
1✔
129
        List<PIDRecord> successfulRecords = new ArrayList<>();
1✔
130
        // register the records
131
        validatedRecords.forEach(pidRecord -> {
1✔
132
            try {
133
                // register the PID
134
                String pid = this.typingService.registerPid(pidRecord);
1✔
135
                pidRecord.setPid(pid);
1✔
136

137
                // store pid locally in accordance with the storage strategy
138
                if (applicationProps.getStorageStrategy().storesModified()) {
1✔
139
                    storeLocally(pid, true);
1✔
140
                }
141

142
                // distribute pid creation event to other services
143
                PidRecordMessage message = PidRecordMessage.creation(
1✔
144
                        pid,
145
                        "", // TODO parameter is deprecated and will be removed soon.
146
                        AuthenticationHelper.getPrincipal(),
1✔
147
                        ControllerUtils.getLocalHostname());
1✔
148
                try {
149
                    this.messagingService.send(message);
1✔
NEW
150
                } catch (Exception e) {
×
NEW
151
                    LOG.error("Could not notify messaging service about the following message: {}", message);
×
152
                }
1✔
153

154
                // save the record to elastic
155
                this.saveToElastic(pidRecord);
1✔
156
                successfulRecords.add(pidRecord);
1✔
157
                LOG.debug("Successfully registered PID for record: {}", pidRecord);
1✔
NEW
158
            } catch (Exception e) {
×
NEW
159
                LOG.error("Could not register PID for record {}. Error: {}", pidRecord, e.getMessage());
×
NEW
160
                failedRecords.add(pidRecord);
×
161
            }
1✔
162
        });
1✔
163

164
        Instant endTime = Instant.now();
1✔
165

166
        // return the created records
167
        LOG.info("Total time taken: {} ms", ChronoUnit.MILLIS.between(startTime, endTime));
1✔
168
        LOG.info("-- Time taken for mapping: {} ms", ChronoUnit.MILLIS.between(startTime, mappingTime));
1✔
169
        LOG.info("-- Time taken for validation: {} ms", ChronoUnit.MILLIS.between(mappingTime, validationTime));
1✔
170
        LOG.info("-- Time taken for registration: {} ms", ChronoUnit.MILLIS.between(validationTime, endTime));
1✔
171

172
        if (!failedRecords.isEmpty()) {
1✔
NEW
173
            List<String> rollbackFailures = new ArrayList<>();
×
NEW
174
            for (PIDRecord successfulRecord : successfulRecords) { // rollback the successful records
×
175
                try {
NEW
176
                    LOG.debug("Rolling back PID creation for record with PID {}.", successfulRecord.getPid());
×
NEW
177
                    this.typingService.deletePid(successfulRecord.getPid());
×
NEW
178
                } catch (Exception e) {
×
NEW
179
                    rollbackFailures.add(successfulRecord.getPid());
×
NEW
180
                    LOG.error("Could not rollback PID creation for record with PID {}. Error: {}", successfulRecord.getPid(), e.getMessage());
×
NEW
181
                }
×
NEW
182
            }
×
183

NEW
184
            if (!rollbackFailures.isEmpty()) {
×
NEW
185
                LOG.error("Failed to rollback {} PIDs: {}", rollbackFailures.size(), rollbackFailures);
×
186
            }
187

NEW
188
            LOG.info("Creation finished. Returning validated records for {} records. {} records failed to be created.", validatedRecords.size(), failedRecords.size());
×
NEW
189
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new BatchRecordResponse(failedRecords, pidMappings));
×
190
        } else {
191
            LOG.info("Creation finished. Returning successfully validated and created records for {} records of {}.", successfulRecords.size(), validatedRecords.size());
1✔
192
            return ResponseEntity.status(HttpStatus.CREATED).body(new BatchRecordResponse(successfulRecords, pidMappings));
1✔
193
        }
194
    }
195

196
    /**
197
     * This method generates a mapping between user-provided "fantasy" PIDs and real PIDs.
198
     *
199
     * @param rec    the list of records produced by the user
200
     * @param dryrun whether the operation is a dryrun or not
201
     * @return a map between the user-provided PIDs (key) and the real PIDs (values)
202
     * @throws RecordValidationException if the same internal PID is used for multiple records
203
     * @throws ExternalServiceException  if the PID generation fails
204
     */
205
    private Map<String, String> generatePIDMapping(List<PIDRecord> rec, boolean dryrun) throws RecordValidationException, ExternalServiceException {
206
        Map<String, String> pidMappings = new HashMap<>();
1✔
207
        for (PIDRecord pidRecord : rec) {
1✔
208
            String internalPID = pidRecord.getPid(); // the internal PID is the one given by the user
1✔
209
            if (internalPID == null) {
1✔
210
                internalPID = ""; // if no PID was given, we set it to an empty string
1✔
211
            }
212
            if (!internalPID.isBlank() && pidMappings.containsKey(internalPID)) { // check if the internal PID was already used
1✔
213
                // This internal PID was already used by some other record in the same request.
214
                throw new RecordValidationException(pidRecord, "The PID " + internalPID + " was used for multiple records in the same request.");
1✔
215
            }
216

217
            pidRecord.setPid(""); // clear the PID field in the record
1✔
218
            if (dryrun) { // if it is a dryrun, we set the PID to a temporary value
1✔
NEW
219
                pidRecord.setPid("dryrun_" + pidMappings.size());
×
220
            } else {
221
                setPid(pidRecord); // otherwise, we generate a real PID
1✔
222
            }
223
            pidMappings.put(internalPID, pidRecord.getPid()); // store the mapping between the internal and real PID
1✔
224
        }
1✔
225
        return pidMappings;
1✔
226
    }
227

228
    /**
229
     * This method applies the mappings between temporary PIDs and real PIDs to the records and validates them.
230
     *
231
     * @param rec         the list of records produced by the user
232
     * @param pidMappings the map between the user-provided PIDs (key) and the real PIDs (values)
233
     * @param prefix      the prefix to be used for the real PIDs
234
     * @return the list of validated records
235
     * @throws RecordValidationException as a possible validation outcome
236
     * @throws ExternalServiceException  as a possible validation outcome
237
     */
238
    private List<PIDRecord> applyMappingsToRecordsAndValidate(List<PIDRecord> rec, Map<String, String> pidMappings, String prefix) throws RecordValidationException, ExternalServiceException {
239
        List<PIDRecord> validatedRecords = new ArrayList<>();
1✔
240
        for (PIDRecord pidRecord : rec) {
1✔
241

242
            // use this map to replace all temporary PIDs in the record values with their corresponding real PIDs
243
            pidRecord.getEntries().values().stream() // get all values of the record
1✔
244
                    .flatMap(List::stream) // flatten the list of values
1✔
245
                    .filter(entry -> entry.getValue() != null) // Filter out null values
1✔
246
                    .filter(entry -> pidMappings.containsKey(entry.getValue())) // replace only if the value (aka. "fantasy PID") is a key in the map
1✔
247
                    .peek(entry -> LOG.debug("Found reference. Replacing {} with {}.", entry.getValue(), prefix + pidMappings.get(entry.getValue()))) // log the replacement
1✔
248
                    .forEach(entry -> entry.setValue(prefix + pidMappings.get(entry.getValue()))); // replace the value with the real PID according to the map
1✔
249

250
            // validate the record
251
            this.typingService.validate(pidRecord);
1✔
252

253
            // store the record
254
            validatedRecords.add(pidRecord);
1✔
255
            LOG.debug("Record {} is valid.", pidRecord);
1✔
256
        }
1✔
257
        return validatedRecords;
1✔
258
    }
259

260
    @Override
261
    public ResponseEntity<PIDRecord> createPID(
262
            PIDRecord pidRecord,
263
            boolean dryrun,
264

265
            final WebRequest request,
266
            final HttpServletResponse response,
267
            final UriComponentsBuilder uriBuilder
268
    ) {
269
        LOG.info("Creating PID");
1✔
270

271
        if (dryrun) {
1✔
272
            pidRecord.setPid("dryrun");
1✔
273
        } else {
274
            setPid(pidRecord);
1✔
275
        }
276

277
        this.typingService.validate(pidRecord);
1✔
278

279
        if (dryrun) {
1✔
280
            // dryrun only does validation. Stop now and return as we would later on.
281
            return ResponseEntity.status(HttpStatus.OK).eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
282
        }
283

284
        String pid = this.typingService.registerPid(pidRecord);
1✔
285
        pidRecord.setPid(pid);
1✔
286

287
        if (applicationProps.getStorageStrategy().storesModified()) {
1✔
288
            storeLocally(pid, true);
1✔
289
        }
290
        PidRecordMessage message = PidRecordMessage.creation(
1✔
291
                pid,
292
                "", // TODO parameter is deprecated and will be removed soon.
293
                AuthenticationHelper.getPrincipal(),
1✔
294
                ControllerUtils.getLocalHostname());
1✔
295
        try {
296
            this.messagingService.send(message);
1✔
NEW
297
        } catch (Exception e) {
×
NEW
298
            LOG.error("Could not notify messaging service about the following message: {}", message);
×
299
        }
1✔
300
        this.saveToElastic(pidRecord);
1✔
301
        return ResponseEntity.status(HttpStatus.CREATED).eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
302
    }
303

304
    private boolean hasPid(PIDRecord pidRecord) {
305
        return pidRecord.getPid() != null && !pidRecord.getPid().isBlank();
1✔
306
    }
307

308
    private void setPid(PIDRecord pidRecord) {
309
        boolean hasCustomPid = hasPid(pidRecord);
1✔
310
        boolean allowsCustomPids = pidGenerationProperties.isCustomClientPidsEnabled();
1✔
311

312
        if (allowsCustomPids && hasCustomPid) {
1✔
313
            // in this only case, we do not have to generate a PID
314
            // but we have to check if the PID is already registered and return an error if so
315
            String prefix = this.typingService.getPrefix()
1✔
316
                    .orElseThrow(() -> new InvalidConfigException("No prefix configured."));
1✔
317
            String maybeSuffix = pidRecord.getPid();
1✔
318
            String pid = PidSuffix.asPrefixedChecked(maybeSuffix, prefix);
1✔
319
            boolean isRegisteredPid = this.typingService.isPidRegistered(pid);
1✔
320
            if (isRegisteredPid) {
1✔
321
                throw new PidAlreadyExistsException(pidRecord.getPid());
1✔
322
            }
323
        } else {
1✔
324
            // In all other (usual) cases, we have to generate a PID.
325
            // We store only the suffix in the pid field.
326
            // The registration at the PID service will preprend the prefix.
327

328
            Stream<PidSuffix> suffixStream = suffixGenerator.infiniteStream();
1✔
329
            Optional<PidSuffix> maybeSuffix = Streams.failableStream(suffixStream)
1✔
330
                    // With failable streams, we can throw exceptions.
331
                    .filter(suffix -> !this.typingService.isPidRegistered(suffix))
1✔
332
                    .stream()  // back to normal java streams
1✔
333
                    .findFirst();  // as the stream is infinite, we should always find a prefix.
1✔
334
            PidSuffix suffix = maybeSuffix
1✔
335
                    .orElseThrow(() -> new ExternalServiceException("Could not generate PID suffix which did not exist yet."));
1✔
336
            pidRecord.setPid(suffix.get());
1✔
337
        }
338
    }
1✔
339

340
    @Override
341
    public ResponseEntity<PIDRecord> updatePID(
342
            PIDRecord pidRecord,
343
            boolean dryrun,
344

345
            final WebRequest request,
346
            final HttpServletResponse response,
347
            final UriComponentsBuilder uriBuilder
348
    ) {
349
        // PID validation
350
        String pid = getContentPathFromRequest("pid", request);
1✔
351
        String pidInternal = pidRecord.getPid();
1✔
352
        if (hasPid(pidRecord) && !pid.equals(pidInternal)) {
1✔
NEW
353
            throw new RecordValidationException(
×
354
                    pidRecord,
NEW
355
                    "Optional PID in record is given (%s), but it was not the same as the PID in the URL (%s). Ignore request, assuming this was not intended.".formatted(pidInternal, pid));
×
356
        }
357

358
        PIDRecord existingRecord = this.resolver.resolve(pid);
1✔
359
        if (existingRecord == null) {
1✔
NEW
360
            throw new PidNotFoundException(pid);
×
361
        }
362

363
        // record validation
364
        pidRecord.setPid(pid);
1✔
365
        this.typingService.validate(pidRecord);
1✔
366

367
        // throws exception (HTTP 412) if check fails.
368
        ControllerUtils.checkEtag(request, existingRecord);
1✔
369

370
        if (dryrun) {
1✔
371
            // dryrun only does validation. Stop now and return as we would later on.
372
            return ResponseEntity.ok().eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
373
        }
374

375
        // update and send message
376
        if (this.typingService.updatePid(pidRecord)) {
1✔
377
            // store pid locally
378
            if (applicationProps.getStorageStrategy().storesModified()) {
1✔
379
                storeLocally(pidRecord.getPid(), true);
1✔
380
            }
381
            // distribute pid to other services
382
            PidRecordMessage message = PidRecordMessage.update(
1✔
383
                    pid,
384
                    "", // TODO parameter is depricated and will be removed soon.
385
                    AuthenticationHelper.getPrincipal(),
1✔
386
                    ControllerUtils.getLocalHostname());
1✔
387
            this.messagingService.send(message);
1✔
388
            this.saveToElastic(pidRecord);
1✔
389
            return ResponseEntity.ok().eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
390
        } else {
NEW
391
            throw new PidNotFoundException(pid);
×
392
        }
393
    }
394

395
    /**
396
     * Stores the PID in a local database.
397
     *
398
     * @param pid    the PID
399
     * @param update if true, updates the modified timestamp if it already exists.
400
     *               If it does not exist, it will be created with both timestamps
401
     *               (created and modified) being the same.
402
     */
403
    private void storeLocally(String pid, boolean update) {
404
        Instant now = Instant.now();
1✔
405
        Optional<KnownPid> oldPid = localPidStorage.findByPid(pid);
1✔
406
        if (oldPid.isEmpty()) {
1✔
407
            localPidStorage.saveAndFlush(new KnownPid(pid, now, now));
1✔
408
        } else if (update) {
1✔
409
            KnownPid newPid = oldPid.get();
1✔
410
            newPid.setModified(now);
1✔
411
            localPidStorage.saveAndFlush(newPid);
1✔
412
        }
413
    }
1✔
414

415
    private String getContentPathFromRequest(String lastPathElement, WebRequest request) {
416
        String requestedUri = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
1✔
417
                RequestAttributes.SCOPE_REQUEST);
418
        if (requestedUri == null) {
1✔
NEW
419
            throw new CustomInternalServerError("Unable to obtain request URI.");
×
420
        }
421
        return requestedUri.substring(requestedUri.indexOf(lastPathElement + "/") + (lastPathElement + "/").length());
1✔
422
    }
423

424
    @Override
425
    public ResponseEntity<PIDRecord> getRecord(
426
            boolean validation,
427

428
            final WebRequest request,
429
            final HttpServletResponse response,
430
            final UriComponentsBuilder uriBuilder
431
    ) {
432
        String pid = getContentPathFromRequest("pid", request);
1✔
433
        PIDRecord pidRecord = this.resolver.resolve(pid);
1✔
434
        if (applicationProps.getStorageStrategy().storesResolved()) {
1✔
435
            storeLocally(pid, false);
1✔
436
        }
437
        this.saveToElastic(pidRecord);
1✔
438
        if (validation) {
1✔
439
            typingService.validate(pidRecord);
1✔
440
        }
441
        return ResponseEntity.ok().eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
442
    }
443

444
    private void saveToElastic(PIDRecord rec) {
445
        this.elastic.ifPresent(
1✔
NEW
446
                database -> database.save(
×
NEW
447
                        new PidRecordElasticWrapper(rec, typingService.getOperations())
×
448
                )
449
        );
450
    }
1✔
451

452
    @Override
453
    public ResponseEntity<KnownPid> findByPid(
454
            WebRequest request,
455
            HttpServletResponse response,
456
            UriComponentsBuilder uriBuilder
457
    ) throws IOException {
458
        String pid = getContentPathFromRequest("known-pid", request);
1✔
459
        Optional<KnownPid> known = this.localPidStorage.findByPid(pid);
1✔
460
        return known
1✔
461
                .map(knownPid -> ResponseEntity.ok().body(knownPid))
1✔
462
                .orElseGet(() -> ResponseEntity.notFound().build());
1✔
463
    }
464

465
    public Page<KnownPid> findAllPage(
466
            Instant createdAfter,
467
            Instant createdBefore,
468
            Instant modifiedAfter,
469
            Instant modifiedBefore,
470
            Pageable pageable
471
    ) {
472
        final boolean queriesCreated = createdAfter != null || createdBefore != null;
1✔
473
        final boolean queriesModified = modifiedAfter != null || modifiedBefore != null;
1✔
474
        if (queriesCreated && createdAfter == null) {
1✔
475
            createdAfter = Instant.EPOCH;
1✔
476
        }
477
        if (queriesCreated && createdBefore == null) {
1✔
478
            createdBefore = Instant.now().plus(1, ChronoUnit.DAYS);
1✔
479
        }
480
        if (queriesModified && modifiedAfter == null) {
1✔
481
            modifiedAfter = Instant.EPOCH;
1✔
482
        }
483
        if (queriesModified && modifiedBefore == null) {
1✔
484
            modifiedBefore = Instant.now().plus(1, ChronoUnit.DAYS);
1✔
485
        }
486

487
        Page<KnownPid> resultCreatedTimestamp = Page.empty();
1✔
488
        Page<KnownPid> resultModifiedTimestamp = Page.empty();
1✔
489
        if (queriesCreated) {
1✔
490
            resultCreatedTimestamp = this.localPidStorage
1✔
491
                    .findDistinctPidsByCreatedBetween(createdAfter, createdBefore, pageable);
1✔
492
        }
493
        if (queriesModified) {
1✔
494
            resultModifiedTimestamp = this.localPidStorage
1✔
495
                    .findDistinctPidsByModifiedBetween(modifiedAfter, modifiedBefore, pageable);
1✔
496
        }
497
        if (queriesCreated && queriesModified) {
1✔
498
            final Page<KnownPid> tmp = resultModifiedTimestamp;
1✔
499
            final List<KnownPid> intersection = resultCreatedTimestamp.filter((x) -> tmp.getContent().contains(x)).toList();
1✔
500
            return new PageImpl<>(intersection);
1✔
501
        } else if (queriesCreated) {
1✔
502
            return resultCreatedTimestamp;
1✔
503
        } else if (queriesModified) {
1✔
504
            return resultModifiedTimestamp;
1✔
505
        }
506
        return new PageImpl<>(this.localPidStorage.findAll());
1✔
507
    }
508

509
    @Override
510
    public ResponseEntity<List<KnownPid>> findAll(
511
            Instant createdAfter,
512
            Instant createdBefore,
513
            Instant modifiedAfter,
514
            Instant modifiedBefore,
515
            Pageable pageable,
516
            WebRequest request,
517
            HttpServletResponse response,
518
            UriComponentsBuilder uriBuilder) {
519
        Page<KnownPid> page = this.findAllPage(createdAfter, createdBefore, modifiedAfter, modifiedBefore, pageable);
1✔
520
        response.addHeader(
1✔
521
                HeaderConstants.CONTENT_RANGE,
522
                ControllerUtils.getContentRangeHeader(
1✔
523
                        page.getNumber(),
1✔
524
                        page.getSize(),
1✔
525
                        page.getTotalElements()));
1✔
526
        return ResponseEntity.ok().body(page.getContent());
1✔
527
    }
528

529
    @Override
530
    public ResponseEntity<TabulatorPaginationFormat<KnownPid>> findAllForTabular(
531
            Instant createdAfter,
532
            Instant createdBefore,
533
            Instant modifiedAfter,
534
            Instant modifiedBefore,
535
            Pageable pageable,
536
            WebRequest request,
537
            HttpServletResponse response,
538
            UriComponentsBuilder uriBuilder) {
539
        Page<KnownPid> page = this.findAllPage(createdAfter, createdBefore, modifiedAfter, modifiedBefore, pageable);
1✔
540
        response.addHeader(
1✔
541
                HeaderConstants.CONTENT_RANGE,
542
                ControllerUtils.getContentRangeHeader(
1✔
543
                        page.getNumber(),
1✔
544
                        page.getSize(),
1✔
545
                        page.getTotalElements()));
1✔
546
        TabulatorPaginationFormat<KnownPid> tabPage = new TabulatorPaginationFormat<>(page);
1✔
547
        return ResponseEntity.ok().body(tabPage);
1✔
548
    }
549

550
    private String quotedEtag(PIDRecord pidRecord) {
551
        return String.format("\"%s\"", pidRecord.getEtag());
1✔
552
    }
553

554
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc