• 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

84.91
/src/main/java/edu/kit/datamanager/pit/pitservice/impl/EmbeddedStrictValidatorStrategy.java
1
package edu.kit.datamanager.pit.pitservice.impl;
2

3
import edu.kit.datamanager.pit.common.ExternalServiceException;
4
import edu.kit.datamanager.pit.common.RecordValidationException;
5
import edu.kit.datamanager.pit.common.TypeNotFoundException;
6
import edu.kit.datamanager.pit.configuration.ApplicationProperties;
7
import edu.kit.datamanager.pit.domain.PIDRecord;
8
import edu.kit.datamanager.pit.pitservice.IValidationStrategy;
9
import edu.kit.datamanager.pit.typeregistry.AttributeInfo;
10
import edu.kit.datamanager.pit.typeregistry.ITypeRegistry;
11

12
import java.util.Arrays;
13
import java.util.List;
14
import java.util.Set;
15
import java.util.concurrent.*;
16

17
import org.slf4j.Logger;
18
import org.slf4j.LoggerFactory;
19

20
/**
21
 * Validates a PID record using embedded profile(s).
22
 * <p>
23
 * - checks if all mandatory attributes are present
24
 * - validates all available attributes
25
 * - fails if an attribute is not defined within the profile
26
 */
27
public class EmbeddedStrictValidatorStrategy implements IValidationStrategy {
28

29
    private static final Logger LOG = LoggerFactory.getLogger(EmbeddedStrictValidatorStrategy.class);
1✔
30

31
    protected final ITypeRegistry typeRegistry;
32
    protected final boolean alwaysAcceptAdditionalAttributes;
33
    protected final Set<String> profileKeys;
34

35
    public EmbeddedStrictValidatorStrategy(
36
            ITypeRegistry typeRegistry,
37
            ApplicationProperties config
38
    ) {
1✔
39
        this.typeRegistry = typeRegistry;
1✔
40
        this.profileKeys = config.getProfileKeys();
1✔
41
        this.alwaysAcceptAdditionalAttributes = config.isValidationAlwaysAllowAdditionalAttributes();
1✔
42
    }
1✔
43

44
    @Override
45
    public void validate(PIDRecord pidRecord)
46
            throws RecordValidationException, ExternalServiceException
47
    {
48
        if (pidRecord.getPropertyIdentifiers().isEmpty()) {
1✔
49
            throw new RecordValidationException(pidRecord, "Record is empty!");
1✔
50
        }
51

52
        // For each attribute in record, resolve schema and check the value
53
        List<CompletableFuture<AttributeInfo>> attributeInfoFutures = pidRecord.getPropertyIdentifiers().stream()
1✔
54
                // resolve attribute info (type and schema)
55
                .map(this.typeRegistry::queryAttributeInfo)
1✔
56
                // validate values using schema
57
                .map(attributeInfoFuture -> attributeInfoFuture.thenApply(attributeInfo -> {
1✔
58
                    for (String value : pidRecord.getPropertyValues(attributeInfo.pid())) {
1✔
59
                        boolean isValid = attributeInfo.validate(value);
1✔
60
                        if (!isValid) {
1✔
61
                            throw new RecordValidationException(
1✔
62
                                    pidRecord,
63
                                    "Attribute %s has a non-complying value %s"
64
                                            .formatted(attributeInfo.pid(), value));
1✔
65
                        }
66
                    }
67
                    return attributeInfo;
1✔
68
                }))
69
                // resolve profiles and apply their validation
70
                .map(attributeInfoFuture -> attributeInfoFuture.thenCompose(attributeInfo -> {
1✔
71
                    boolean indicatesProfileValue = this.profileKeys.contains(attributeInfo.pid());
1✔
72
                    if (!indicatesProfileValue) {
1✔
73
                        return CompletableFuture.completedFuture(attributeInfo);
1✔
74
                    }
75
                    CompletableFuture<?>[] profileFutures = Arrays.stream(pidRecord.getPropertyValues(attributeInfo.pid()))
1✔
76
                            .map(this.typeRegistry::queryAsProfile)
1✔
77
                            .map(registeredProfileFuture -> registeredProfileFuture.thenAccept(
1✔
78
                                    registeredProfile -> {
79
                                        registeredProfile.validateAttributes(pidRecord, this.alwaysAcceptAdditionalAttributes);
1✔
80
                                    }))
1✔
81
                            .toArray(CompletableFuture[]::new);
1✔
82
                    return CompletableFuture.allOf(profileFutures).thenApply(v -> attributeInfo);
1✔
83
                }))
84
                .toList();
1✔
85

86

87
        try {
88
            LOG.trace("Processing all attributes in the record {}.", pidRecord.getPid());
1✔
89
            CompletableFuture.allOf(attributeInfoFutures.toArray(new CompletableFuture<?>[0])).join();
1✔
90
            LOG.trace("Finished processing all attributes in the record {}.", pidRecord.getPid());
1✔
91
        } catch (CompletionException e) {
1✔
92
            LOG.trace("Exception occurred during validation of record {}. Unpack Exception, if required.", pidRecord.getPid(), e);
1✔
NEW
93
            unpackAsyncExceptions(pidRecord, e);
×
NEW
94
            LOG.trace("Exception was not unpacked. Rethrowing.", e);
×
NEW
95
            throw new ExternalServiceException(this.typeRegistry.getRegistryIdentifier());
×
NEW
96
        } catch (CancellationException e) {
×
NEW
97
            unpackAsyncExceptions(pidRecord, e);
×
UNCOV
98
            throw new RecordValidationException(
×
99
                    pidRecord,
NEW
100
                    String.format("Validation task was cancelled for %s. Please report.", pidRecord.getPid()));
×
101
        }
1✔
102
    }
1✔
103

104
    /**
105
     * Checks Exceptions' causes for a RecordValidationExceptions, and throws them, if present.
106
     * <p>
107
     * Usually used to avoid exposing exceptions related to futures.
108
     * @param e the exception to unwrap.
109
     */
110
    private static void unpackAsyncExceptions(PIDRecord pidRecord, Throwable e) {
111
        final int MAX_LEVEL = 10;
1✔
112
        Throwable cause = e;
1✔
113

114
        for (int level = 0; level <= MAX_LEVEL && cause != null; level++) {
1✔
115
            cause = cause.getCause();
1✔
116
            if (cause instanceof RecordValidationException rve) {
1✔
117
                throw rve;
1✔
118
            } else if (cause instanceof TypeNotFoundException tnf) {
1✔
119
                throw new RecordValidationException(
1✔
120
                        pidRecord,
121
                        "Type not found: %s".formatted(tnf.getMessage()));
1✔
122
            }
123
        }
UNCOV
124
    }
×
125
}
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