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

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

30 Sep 2025 10:09AM UTC coverage: 78.134% (+5.4%) from 72.712%
#576

Pull #264

github

web-flow
Merge 269776c6c into ef6582172
Pull Request #264: Development branch for v3.0.0

661 of 796 new or added lines in 25 files covered. (83.04%)

3 existing lines in 2 files now uncovered.

1072 of 1372 relevant lines covered (78.13%)

0.78 hits per line

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

85.66
/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
            addPrefixToMapping(pidMappings, prefix);
×
NEW
126
            return ResponseEntity.status(HttpStatus.OK).body(new BatchRecordResponse(validatedRecords, pidMappings));
×
127
        }
128

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

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

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

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

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

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

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

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

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

199
    private static void addPrefixToMapping(Map<String, String> pidMappings, String prefix) {
200
        pidMappings.replaceAll((placeholder, realSuffix) -> prefix + realSuffix);
1✔
201
    }
1✔
202

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

224
            pidRecord.setPid(""); // clear the PID field in the record
1✔
225
            if (dryrun) { // if it is a dryrun, we set the PID to a temporary value
1✔
NEW
226
                pidRecord.setPid("dryrun_" + pidMappings.size());
×
227
            } else {
228
                setPid(pidRecord); // otherwise, we generate a real PID
1✔
229
            }
230
            pidMappings.put(internalPID, pidRecord.getPid()); // store the mapping between the internal and real PID
1✔
231
        }
1✔
232
        return pidMappings;
1✔
233
    }
234

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

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

257
            // validate the record
258
            this.typingService.validate(pidRecord);
1✔
259

260
            // store the record
261
            validatedRecords.add(pidRecord);
1✔
262
            LOG.debug("Record {} is valid.", pidRecord);
1✔
263
        }
1✔
264
        return validatedRecords;
1✔
265
    }
266

267
    @Override
268
    public ResponseEntity<PIDRecord> createPID(
269
            PIDRecord pidRecord,
270
            boolean dryrun,
271

272
            final WebRequest request,
273
            final HttpServletResponse response,
274
            final UriComponentsBuilder uriBuilder
275
    ) {
276
        LOG.info("Creating PID");
1✔
277

278
        if (dryrun) {
1✔
279
            pidRecord.setPid("dryrun");
1✔
280
        } else {
281
            setPid(pidRecord);
1✔
282
        }
283

284
        this.typingService.validate(pidRecord);
1✔
285

286
        if (dryrun) {
1✔
287
            // dryrun only does validation. Stop now and return as we would later on.
288
            return ResponseEntity.status(HttpStatus.OK).eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
289
        }
290

291
        String pid = this.typingService.registerPid(pidRecord);
1✔
292
        pidRecord.setPid(pid);
1✔
293

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

311
    private boolean hasPid(PIDRecord pidRecord) {
312
        return pidRecord.getPid() != null && !pidRecord.getPid().isBlank();
1✔
313
    }
314

315
    private void setPid(PIDRecord pidRecord) {
316
        boolean hasCustomPid = hasPid(pidRecord);
1✔
317
        boolean allowsCustomPids = pidGenerationProperties.isCustomClientPidsEnabled();
1✔
318

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

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

347
    @Override
348
    public ResponseEntity<PIDRecord> updatePID(
349
            PIDRecord pidRecord,
350
            boolean dryrun,
351

352
            final WebRequest request,
353
            final HttpServletResponse response,
354
            final UriComponentsBuilder uriBuilder
355
    ) {
356
        // PID validation
357
        String pid = getContentPathFromRequest("pid", request);
1✔
358
        String pidInternal = pidRecord.getPid();
1✔
359
        if (hasPid(pidRecord) && !pid.equals(pidInternal)) {
1✔
NEW
360
            throw new RecordValidationException(
×
361
                    pidRecord,
NEW
362
                    "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));
×
363
        }
364

365
        PIDRecord existingRecord = this.resolver.resolve(pid);
1✔
366
        if (existingRecord == null) {
1✔
NEW
367
            throw new PidNotFoundException(pid);
×
368
        }
369

370
        // record validation
371
        pidRecord.setPid(pid);
1✔
372
        this.typingService.validate(pidRecord);
1✔
373

374
        // throws exception (HTTP 412) if check fails.
375
        ControllerUtils.checkEtag(request, existingRecord);
1✔
376

377
        if (dryrun) {
1✔
378
            // dryrun only does validation. Stop now and return as we would later on.
379
            return ResponseEntity.ok().eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
380
        }
381

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

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

422
    private String getContentPathFromRequest(String lastPathElement, WebRequest request) {
423
        String requestedUri = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
1✔
424
                RequestAttributes.SCOPE_REQUEST);
425
        if (requestedUri == null) {
1✔
NEW
426
            throw new CustomInternalServerError("Unable to obtain request URI.");
×
427
        }
428
        return requestedUri.substring(requestedUri.indexOf(lastPathElement + "/") + (lastPathElement + "/").length());
1✔
429
    }
430

431
    @Override
432
    public ResponseEntity<PIDRecord> getRecord(
433
            boolean validation,
434

435
            final WebRequest request,
436
            final HttpServletResponse response,
437
            final UriComponentsBuilder uriBuilder
438
    ) {
439
        String pid = getContentPathFromRequest("pid", request);
1✔
440
        PIDRecord pidRecord = this.resolver.resolve(pid);
1✔
441
        if (applicationProps.getStorageStrategy().storesResolved()) {
1✔
442
            storeLocally(pid, false);
1✔
443
        }
444
        this.saveToElastic(pidRecord);
1✔
445
        if (validation) {
1✔
446
            typingService.validate(pidRecord);
1✔
447
        }
448
        return ResponseEntity.ok().eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
449
    }
450

451
    private void saveToElastic(PIDRecord rec) {
452
        this.elastic.ifPresent(
1✔
NEW
453
                database -> database.save(
×
NEW
454
                        new PidRecordElasticWrapper(rec, typingService.getOperations())
×
455
                )
456
        );
457
    }
1✔
458

459
    @Override
460
    public ResponseEntity<KnownPid> findByPid(
461
            WebRequest request,
462
            HttpServletResponse response,
463
            UriComponentsBuilder uriBuilder
464
    ) throws IOException {
465
        String pid = getContentPathFromRequest("known-pid", request);
1✔
466
        Optional<KnownPid> known = this.localPidStorage.findByPid(pid);
1✔
467
        return known
1✔
468
                .map(knownPid -> ResponseEntity.ok().body(knownPid))
1✔
469
                .orElseGet(() -> ResponseEntity.notFound().build());
1✔
470
    }
471

472
    public Page<KnownPid> findAllPage(
473
            Instant createdAfter,
474
            Instant createdBefore,
475
            Instant modifiedAfter,
476
            Instant modifiedBefore,
477
            Pageable pageable
478
    ) {
479
        final boolean queriesCreated = createdAfter != null || createdBefore != null;
1✔
480
        final boolean queriesModified = modifiedAfter != null || modifiedBefore != null;
1✔
481
        if (queriesCreated && createdAfter == null) {
1✔
482
            createdAfter = Instant.EPOCH;
1✔
483
        }
484
        if (queriesCreated && createdBefore == null) {
1✔
485
            createdBefore = Instant.now().plus(1, ChronoUnit.DAYS);
1✔
486
        }
487
        if (queriesModified && modifiedAfter == null) {
1✔
488
            modifiedAfter = Instant.EPOCH;
1✔
489
        }
490
        if (queriesModified && modifiedBefore == null) {
1✔
491
            modifiedBefore = Instant.now().plus(1, ChronoUnit.DAYS);
1✔
492
        }
493

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

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

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

557
    private String quotedEtag(PIDRecord pidRecord) {
558
        return String.format("\"%s\"", pidRecord.getEtag());
1✔
559
    }
560

561
}
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