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

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

13 Nov 2023 05:22PM UTC coverage: 72.606% (+11.8%) from 60.79%
#324

push

github

web-flow
Merge pull request #174 from kit-data-manager/dev-v2

Development branch for v2.0.0

136 of 205 new or added lines in 14 files covered. (66.34%)

4 existing lines in 3 files now uncovered.

872 of 1201 relevant lines covered (72.61%)

0.73 hits per line

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

94.07
/src/main/java/edu/kit/datamanager/pit/web/impl/TypingRESTResourceImpl.java
1
package edu.kit.datamanager.pit.web.impl;
2

3
import edu.kit.datamanager.exceptions.CustomInternalServerError;
4
import java.io.IOException;
5
import java.time.Instant;
6
import java.time.temporal.ChronoUnit;
7
import java.util.List;
8
import java.util.Optional;
9
import java.util.stream.Stream;
10

11
import edu.kit.datamanager.pit.common.PidAlreadyExistsException;
12
import edu.kit.datamanager.pit.common.PidNotFoundException;
13
import edu.kit.datamanager.pit.configuration.ApplicationProperties;
14
import edu.kit.datamanager.pit.configuration.PidGenerationProperties;
15
import edu.kit.datamanager.pit.common.RecordValidationException;
16
import edu.kit.datamanager.pit.domain.PIDRecord;
17
import edu.kit.datamanager.pit.elasticsearch.PidRecordElasticRepository;
18
import edu.kit.datamanager.pit.elasticsearch.PidRecordElasticWrapper;
19
import edu.kit.datamanager.pit.pidgeneration.PidSuffix;
20
import edu.kit.datamanager.pit.pidgeneration.PidSuffixGenerator;
21
import edu.kit.datamanager.pit.pidlog.KnownPid;
22
import edu.kit.datamanager.pit.pidlog.KnownPidsDao;
23
import edu.kit.datamanager.pit.pitservice.ITypingService;
24
import edu.kit.datamanager.pit.web.ITypingRestResource;
25
import edu.kit.datamanager.pit.web.TabulatorPaginationFormat;
26
import edu.kit.datamanager.service.IMessagingService;
27
import edu.kit.datamanager.entities.messaging.PidRecordMessage;
28
import edu.kit.datamanager.util.AuthenticationHelper;
29
import edu.kit.datamanager.util.ControllerUtils;
30
import io.swagger.v3.oas.annotations.media.Schema;
31

32
import jakarta.servlet.http.HttpServletResponse;
33

34
import org.apache.commons.lang3.stream.Streams;
35
import org.apache.http.client.cache.HeaderConstants;
36
import org.slf4j.Logger;
37
import org.slf4j.LoggerFactory;
38
import org.springframework.beans.factory.annotation.Autowired;
39
import org.springframework.data.domain.Page;
40
import org.springframework.data.domain.PageImpl;
41
import org.springframework.data.domain.Pageable;
42
import org.springframework.http.HttpStatus;
43
import org.springframework.http.ResponseEntity;
44
import org.springframework.web.bind.annotation.RequestMapping;
45
import org.springframework.web.bind.annotation.RestController;
46
import org.springframework.web.context.request.RequestAttributes;
47
import org.springframework.web.context.request.WebRequest;
48
import org.springframework.web.servlet.HandlerMapping;
49
import org.springframework.web.util.UriComponentsBuilder;
50

51
@RestController
52
@RequestMapping(value = "/api/v1/pit")
53
@Schema(description = "PID Information Types API")
54
public class TypingRESTResourceImpl implements ITypingRestResource {
55

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

58
    @Autowired
59
    private ApplicationProperties applicationProps;
60

61
    @Autowired
62
    protected ITypingService typingService;
63

64
    @Autowired
65
    private IMessagingService messagingService;
66

67
    @Autowired
68
    private KnownPidsDao localPidStorage;
69

70
    @Autowired
71
    private Optional<PidRecordElasticRepository> elastic;
72

73
    @Autowired
74
    private PidSuffixGenerator suffixGenerator;
75

76
    @Autowired
77
    private PidGenerationProperties pidGenerationProperties;
78

79
    public TypingRESTResourceImpl() {
80
        super();
1✔
81
    }
1✔
82

83
    @Override
84
    public ResponseEntity<PIDRecord> createPID(
85
            PIDRecord pidRecord,
86
            boolean dryrun,
87

88
            final WebRequest request,
89
            final HttpServletResponse response,
90
            final UriComponentsBuilder uriBuilder
91
    ) throws IOException {
92
        LOG.info("Creating PID");
1✔
93

94
        if (dryrun) {
1✔
95
            pidRecord.setPid("dryrun");
1✔
96
        } else {
97
            setPid(pidRecord);
1✔
98
        }
99

100
        this.typingService.validate(pidRecord);
1✔
101

102
        if (dryrun) {
1✔
103
            // dryrun only does validation. Stop now and return as we would later on.
104
            return ResponseEntity.status(HttpStatus.OK).eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
105
        }
106

107
        String pid = this.typingService.registerPID(pidRecord);
1✔
108
        pidRecord.setPid(pid);
1✔
109

110
        if (applicationProps.getStorageStrategy().storesModified()) {
1✔
111
            storeLocally(pid, true);
1✔
112
        }
113
        PidRecordMessage message = PidRecordMessage.creation(
1✔
114
                pid,
115
                "", // TODO parameter is depricated and will be removed soon.
116
                AuthenticationHelper.getPrincipal(),
1✔
117
                ControllerUtils.getLocalHostname());
1✔
118
        try {
119
            this.messagingService.send(message);
1✔
NEW
120
        } catch (Exception e) {
×
NEW
121
            LOG.error("Could not notify messaging service about the following message: {}", message);
×
122
        }
1✔
123
        this.saveToElastic(pidRecord);
1✔
124
        return ResponseEntity.status(HttpStatus.CREATED).eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
125
    }
126

127
    private boolean hasPid(PIDRecord pidRecord) {
128
        return pidRecord.getPid() != null && !pidRecord.getPid().isBlank();
1✔
129
    }
130

131
    private void setPid(PIDRecord pidRecord) throws IOException {
132
        boolean hasCustomPid = hasPid(pidRecord);
1✔
133
        boolean allowsCustomPids = pidGenerationProperties.isCustomClientPidsEnabled();
1✔
134

135
        if (allowsCustomPids && hasCustomPid) {
1✔
136
            // in this only case, we do not have to generate a PID
137
            // but we have to check if the PID is already registered and return an error if so
138
            String prefix = this.typingService.getPrefix().orElseThrow(() -> new IOException("No prefix configured."));
1✔
139
            String maybeSuffix = pidRecord.getPid();
1✔
140
            String pid = PidSuffix.asPrefixedChecked(maybeSuffix, prefix);
1✔
141
            boolean isRegisteredPid = this.typingService.isIdentifierRegistered(pid);
1✔
142
            if (isRegisteredPid) {
1✔
143
                throw new PidAlreadyExistsException(pidRecord.getPid());
1✔
144
            }
145
        } else {
1✔
146
            // In all other (usual) cases, we have to generate a PID.
147
            // We store only the suffix in the pid field.
148
            // The registration at the PID service will preprend the prefix.
149

150
            Stream<PidSuffix> suffixStream = suffixGenerator.infiniteStream();
1✔
151
            Optional<PidSuffix> maybeSuffix = Streams.stream(suffixStream)
1✔
152
                    // The Streams.stream gives us a failible stream, so we can throw an exception
153
                    .filter(suffix -> !this.typingService.isIdentifierRegistered(suffix))
1✔
154
                    .stream()  // back to normal java streams
1✔
155
                    .findFirst();  // as the stream is infinite, we should always find a prefix.
1✔
156
            PidSuffix suffix = maybeSuffix.orElseThrow(() -> new IOException("Could not generate PID suffix."));
1✔
157
            pidRecord.setPid(suffix.get());
1✔
158
        }
159
    }
1✔
160

161
    @Override
162
    public ResponseEntity<PIDRecord> updatePID(
163
            PIDRecord pidRecord,
164
            final WebRequest request,
165
            final HttpServletResponse response,
166
            final UriComponentsBuilder uriBuilder) throws IOException {
167
        // PID validation
168
        String pid = getContentPathFromRequest("pid", request);
1✔
169
        String pidInternal = pidRecord.getPid();
1✔
170
        if (hasPid(pidRecord) && !pid.equals(pidInternal)) {
1✔
NEW
171
            throw new RecordValidationException(
×
172
                pidRecord,
173
                "PID in record was given, but it was not the same as the PID in the URL. Ignore request, assuming this was not intended.");
174
        }
175
        
176
        PIDRecord existingRecord = this.typingService.queryAllProperties(pid);
1✔
177
        if (existingRecord == null) {
1✔
UNCOV
178
            throw new PidNotFoundException(pid);
×
179
        }
180
        // throws exception (HTTP 412) if check fails.
181
        ControllerUtils.checkEtag(request, existingRecord);
1✔
182

183
        // record validation
184
        pidRecord.setPid(pid);
1✔
185
        this.typingService.validate(pidRecord);
1✔
186

187
        // update and send message
188
        if (this.typingService.updatePID(pidRecord)) {
1✔
189
            // store pid locally
190
            if (applicationProps.getStorageStrategy().storesModified()) {
1✔
191
                storeLocally(pidRecord.getPid(), true);
1✔
192
            }
193
            // distribute pid to other services
194
            PidRecordMessage message = PidRecordMessage.update(
1✔
195
                    pid,
196
                    "", // TODO parameter is depricated and will be removed soon.
197
                    AuthenticationHelper.getPrincipal(),
1✔
198
                    ControllerUtils.getLocalHostname());
1✔
199
            this.messagingService.send(message);
1✔
200
            this.saveToElastic(pidRecord);
1✔
201
            return ResponseEntity.ok().eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
202
        } else {
NEW
203
            throw new PidNotFoundException(pid);
×
204
        }
205
    }
206

207
    /**
208
     * Stores the PID in a local database.
209
     * 
210
     * @param pid    the PID
211
     * @param update if true, updates the modified timestamp if it already exists.
212
     *               If it does not exist, it will be created with both timestamps
213
     *               (created and modified) being the same.
214
     */
215
    private void storeLocally(String pid, boolean update) {
216
        Instant now = Instant.now();
1✔
217
        Optional<KnownPid> oldPid = localPidStorage.findByPid(pid);
1✔
218
        if (oldPid.isEmpty()) {
1✔
219
            localPidStorage.saveAndFlush(new KnownPid(pid, now, now));
1✔
220
        } else if (update) {
1✔
221
            KnownPid newPid = oldPid.get();
1✔
222
            newPid.setModified(now);
1✔
223
            localPidStorage.saveAndFlush(newPid);
1✔
224
        }
225
    }
1✔
226

227
    private String getContentPathFromRequest(String lastPathElement, WebRequest request) {
228
        String requestedUri = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
1✔
229
                RequestAttributes.SCOPE_REQUEST);
230
        if (requestedUri == null) {
1✔
231
            throw new CustomInternalServerError("Unable to obtain request URI.");
×
232
        }
233
        return requestedUri.substring(requestedUri.indexOf(lastPathElement + "/") + (lastPathElement + "/").length());
1✔
234
    }
235

236
    @Override
237
    public ResponseEntity<PIDRecord> getRecord(
238
            boolean validation,
239

240
            final WebRequest request,
241
            final HttpServletResponse response,
242
            final UriComponentsBuilder uriBuilder
243
    ) throws IOException {
244
        String pid = getContentPathFromRequest("pid", request);
1✔
245
        PIDRecord pidRecord = this.typingService.queryAllProperties(pid);
1✔
246
        if (applicationProps.getStorageStrategy().storesResolved()) {
1✔
247
            storeLocally(pid, false);
1✔
248
        }
249
        this.saveToElastic(pidRecord);
1✔
250
        if (validation) {
1✔
251
            typingService.validate(pidRecord);
1✔
252
        }
253
        return ResponseEntity.ok().eTag(quotedEtag(pidRecord)).body(pidRecord);
1✔
254
    }
255

256
    private void saveToElastic(PIDRecord rec) {
257
        this.elastic.ifPresent(
1✔
258
            database -> database.save(
×
259
                new PidRecordElasticWrapper(rec, typingService.getOperations())
×
260
            )
261
        );
262
    }
1✔
263

264
    @Override
265
    public ResponseEntity<KnownPid> findByPid(
266
            WebRequest request,
267
            HttpServletResponse response,
268
            UriComponentsBuilder uriBuilder
269
    ) throws IOException {
270
        String pid = getContentPathFromRequest("known-pid", request);
1✔
271
        Optional<KnownPid> known = this.localPidStorage.findByPid(pid);
1✔
272
        if (known.isPresent()) {
1✔
273
            return ResponseEntity.ok().body(known.get());
1✔
274
        }
275
        return ResponseEntity.notFound().build();
1✔
276
    }
277

278
    public Page<KnownPid> findAllPage(
279
        Instant createdAfter,
280
        Instant createdBefore,
281
        Instant modifiedAfter,
282
        Instant modifiedBefore,
283
        Pageable pageable
284
    ) {
285
        final boolean queriesCreated = createdAfter != null || createdBefore != null;
1✔
286
        final boolean queriesModified = modifiedAfter != null || modifiedBefore != null;
1✔
287
        if (queriesCreated && createdAfter == null) {
1✔
288
            createdAfter = Instant.EPOCH;
1✔
289
        }
290
        if (queriesCreated && createdBefore == null) {
1✔
291
            createdBefore = Instant.now().plus(1, ChronoUnit.DAYS);
1✔
292
        }
293
        if (queriesModified && modifiedAfter == null) {
1✔
294
            modifiedAfter = Instant.EPOCH;
1✔
295
        }
296
        if (queriesModified && modifiedBefore == null) {
1✔
297
            modifiedBefore = Instant.now().plus(1, ChronoUnit.DAYS);
1✔
298
        }
299

300
        Page<KnownPid> resultCreatedTimestamp = Page.empty();
1✔
301
        Page<KnownPid> resultModifiedTimestamp = Page.empty();
1✔
302
        if (queriesCreated) {
1✔
303
            resultCreatedTimestamp = this.localPidStorage
1✔
304
                .findDistinctPidsByCreatedBetween(createdAfter, createdBefore, pageable);
1✔
305
        }
306
        if (queriesModified) {
1✔
307
            resultModifiedTimestamp = this.localPidStorage
1✔
308
                .findDistinctPidsByModifiedBetween(modifiedAfter, modifiedBefore, pageable);
1✔
309
        }
310
        if (queriesCreated && queriesModified) {
1✔
311
            final Page<KnownPid> tmp = resultModifiedTimestamp;
1✔
312
            final List<KnownPid> intersection = resultCreatedTimestamp.filter((x) -> tmp.getContent().contains(x)).toList();
1✔
313
            return new PageImpl<>(intersection);
1✔
314
        } else if (queriesCreated) {
1✔
315
            return resultCreatedTimestamp;
1✔
316
        } else if (queriesModified) {
1✔
317
            return resultModifiedTimestamp;
1✔
318
        }
319
        return new PageImpl<>(this.localPidStorage.findAll());
1✔
320
    }
321

322
    @Override
323
    public ResponseEntity<List<KnownPid>> findAll(
324
            Instant createdAfter,
325
            Instant createdBefore,
326
            Instant modifiedAfter,
327
            Instant modifiedBefore,
328
            Pageable pageable,
329
            WebRequest request,
330
            HttpServletResponse response,
331
            UriComponentsBuilder uriBuilder) throws IOException
332
    {
333
        Page<KnownPid> page = this.findAllPage(createdAfter, createdBefore, modifiedAfter, modifiedBefore, pageable);
1✔
334
        response.addHeader(
1✔
335
            HeaderConstants.CONTENT_RANGE,
336
            ControllerUtils.getContentRangeHeader(
1✔
337
                page.getNumber(),
1✔
338
                page.getSize(),
1✔
339
                page.getTotalElements()));
1✔
340
        return ResponseEntity.ok().body(page.getContent());
1✔
341
    }
342

343
    @Override
344
    public ResponseEntity<TabulatorPaginationFormat<KnownPid>> findAllForTabular(
345
            Instant createdAfter,
346
            Instant createdBefore,
347
            Instant modifiedAfter,
348
            Instant modifiedBefore,
349
            Pageable pageable,
350
            WebRequest request,
351
            HttpServletResponse response,
352
            UriComponentsBuilder uriBuilder) throws IOException
353
    {
354
        Page<KnownPid> page = this.findAllPage(createdAfter, createdBefore, modifiedAfter, modifiedBefore, pageable);
1✔
355
        response.addHeader(
1✔
356
            HeaderConstants.CONTENT_RANGE,
357
            ControllerUtils.getContentRangeHeader(
1✔
358
                page.getNumber(),
1✔
359
                page.getSize(),
1✔
360
                page.getTotalElements()));
1✔
361
        TabulatorPaginationFormat<KnownPid> tabPage = new TabulatorPaginationFormat<>(page);
1✔
362
        return ResponseEntity.ok().body(tabPage);
1✔
363
    }
364

365
    private String quotedEtag(PIDRecord pidRecord) {
366
        return String.format("\"%s\"", pidRecord.getEtag());
1✔
367
    }
368

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

© 2025 Coveralls, Inc