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

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

23 Jan 2025 02:48PM UTC coverage: 75.997% (+3.6%) from 72.4%
#451

Pull #218

github

web-flow
Merge a92bd4d02 into 459f0c036
Pull Request #218: Type-Api support and validation speedup

287 of 352 new or added lines in 18 files covered. (81.53%)

3 existing lines in 2 files now uncovered.

915 of 1204 relevant lines covered (76.0%)

0.76 hits per line

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

32.43
/src/main/java/edu/kit/datamanager/pit/pidsystem/impl/HandleProtocolAdapter.java
1
package edu.kit.datamanager.pit.pidsystem.impl;
2

3
import java.io.IOException;
4
import java.nio.charset.StandardCharsets;
5
import java.security.PrivateKey;
6
import java.security.spec.InvalidKeySpecException;
7
import java.util.ArrayList;
8
import java.util.Arrays;
9
import java.util.Collection;
10
import java.util.List;
11
import java.util.Map;
12
import java.util.Optional;
13
import java.util.Map.Entry;
14
import java.util.stream.Collectors;
15
import java.util.stream.Stream;
16

17
import edu.kit.datamanager.pit.recordModifiers.RecordModifier;
18
import jakarta.annotation.PostConstruct;
19
import jakarta.validation.constraints.NotNull;
20

21
import org.apache.commons.lang3.stream.Streams;
22
import org.slf4j.Logger;
23
import org.slf4j.LoggerFactory;
24
import org.springframework.beans.factory.annotation.Autowired;
25
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
26
import org.springframework.stereotype.Component;
27
import edu.kit.datamanager.pit.common.ExternalServiceException;
28
import edu.kit.datamanager.pit.common.InvalidConfigException;
29
import edu.kit.datamanager.pit.common.PidAlreadyExistsException;
30
import edu.kit.datamanager.pit.common.PidNotFoundException;
31
import edu.kit.datamanager.pit.common.PidUpdateException;
32
import edu.kit.datamanager.pit.common.RecordValidationException;
33
import edu.kit.datamanager.pit.configuration.HandleCredentials;
34
import edu.kit.datamanager.pit.configuration.HandleProtocolProperties;
35
import edu.kit.datamanager.pit.domain.PIDRecord;
36
import edu.kit.datamanager.pit.domain.PIDRecordEntry;
37
import edu.kit.datamanager.pit.pidsystem.IIdentifierSystem;
38
import net.handle.api.HSAdapter;
39
import net.handle.api.HSAdapterFactory;
40
import net.handle.apps.batch.BatchUtil;
41
import net.handle.hdllib.Common;
42
import net.handle.hdllib.HandleException;
43
import net.handle.hdllib.HandleResolver;
44
import net.handle.hdllib.HandleValue;
45
import net.handle.hdllib.PublicKeyAuthenticationInfo;
46
import net.handle.hdllib.SiteInfo;
47
import net.handle.hdllib.Util;
48

49
/**
50
 * Uses the official java library to interact with the handle system using the
51
 * handle protocol.
52
 */
53
@Component
54
@ConditionalOnBean(HandleProtocolProperties.class)
55
public class HandleProtocolAdapter implements IIdentifierSystem {
56

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

59
    private static final byte[][][] BLACKLIST_NONTYPE_LISTS = {
1✔
60
            Common.SITE_INFO_AND_SERVICE_HANDLE_INCL_PREFIX_TYPES,
61
            Common.DERIVED_PREFIX_SITE_AND_SERVICE_HANDLE_TYPES,
62
            Common.SERVICE_HANDLE_TYPES,
63
            Common.LOCATION_AND_ADMIN_TYPES,
64
            Common.SECRET_KEY_TYPES,
65
            Common.PUBLIC_KEY_TYPES,
66
            // Common.STD_TYPES, // not using because of URL and EMAIL
67
            {
68
                    // URL and EMAIL might contain valuable information and can be considered
69
                    // non-technical.
70
                    // Common.STD_TYPE_URL,
71
                    // Common.STD_TYPE_EMAIL,
72
                    Common.STD_TYPE_HSADMIN,
73
                    Common.STD_TYPE_HSALIAS,
74
                    Common.STD_TYPE_HSSITE,
75
                    Common.STD_TYPE_HSSITE6,
76
                    Common.STD_TYPE_HSSERV,
77
                    Common.STD_TYPE_HSSECKEY,
78
                    Common.STD_TYPE_HSPUBKEY,
79
                    Common.STD_TYPE_HSVALLIST,
80
            }
81
    };
82

83
    private static final String SERVICE_NAME_HANDLE = "Handle System";
84

85
    // Properties specific to this adapter.
86
    @Autowired
87
    final private HandleProtocolProperties props;
88
    // Handle Protocol implementation
89
    private HSAdapter client;
90
    // indicates if the adapter can modify and create PIDs or just resolve them.
91
    private boolean isAdminMode = false;
1✔
92
    // the value that is appended to every new record.
93
    private HandleValue adminValue;
94

95
    // For testing
96
    public HandleProtocolAdapter(HandleProtocolProperties props) {
1✔
97
        this.props = props;
1✔
98
    }
1✔
99

100
    /**
101
     * Initializes internal classes.
102
     * We use this method with the @PostConstruct annotation to run it
103
     * after the constructor and after springs autowiring is done properly
104
     * to make sure that all properties are already autowired.
105
     * 
106
     * @throws HandleException        if a handle system error occurs.
107
     * @throws InvalidConfigException if the configuration is invalid, e.g. a path
108
     *                                does not lead to a file.
109
     * @throws IOException            if the private key file can not be read.
110
     */
111
    @PostConstruct
112
    public void init() throws InvalidConfigException, HandleException, IOException {
113
        LOG.info("Using PID System 'Handle'");
1✔
114
        this.isAdminMode = props.getCredentials() != null;
1✔
115

116
        if (!this.isAdminMode) {
1✔
117
            LOG.warn("No credentials found. Starting Handle Adapter with no administrative privileges.");
1✔
118
            this.client = HSAdapterFactory.newInstance();
1✔
119

120
        } else {
121
            HandleCredentials credentials = props.getCredentials();
×
122
            // Check if key file is plausible, throw exceptions if something is wrong.
123
            byte[] privateKey = credentials.getPrivateKeyFileContent();
×
124
            byte[] passphrase = credentials.getPrivateKeyPassphraseAsBytes();
×
125
            LOG.debug("Logging in with user {}", credentials.getUserHandle());
×
126
            this.client = HSAdapterFactory.newInstance(
×
127
                    credentials.getUserHandle(),
×
128
                    credentials.getPrivateKeyIndex(),
×
129
                    privateKey,
130
                    passphrase // "use null for unencrypted keys"
131
            );
132
            HandleIndex indexManager = new HandleIndex();
×
133
            this.adminValue = this.client.createAdminValue(
×
134
                    props.getCredentials().getUserHandle(),
×
135
                    props.getCredentials().getPrivateKeyIndex(),
×
136
                    indexManager.getHsAdminIndex());
×
137
        }
138
    }
1✔
139

140
    @Override
141
    public Optional<String> getPrefix() {
142
        if (this.isAdminMode) {
×
143
            return Optional.ofNullable(this.props.getCredentials()).map(HandleCredentials::getHandleIdentifierPrefix);
×
144
        } else {
145
            return Optional.empty();
×
146
        }
147
    }
148

149
    @Override
150
    public boolean isPidRegistered(final String pid) throws ExternalServiceException {
151
        HandleValue[] recordProperties;
152
        try {
153
            recordProperties = this.client.resolveHandle(pid, null, null);
1✔
154
        } catch (HandleException e) {
1✔
155
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
1✔
156
                return false;
1✔
157
            } else {
158
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
159
            }
160
        }
1✔
161
        return recordProperties != null && recordProperties.length > 0;
1✔
162
    }
163

164
    @Override
165
    public PIDRecord queryPid(final String pid) throws PidNotFoundException, ExternalServiceException {
166
        Collection<HandleValue> allValues = this.queryAllHandleValues(pid);
1✔
167
        if (allValues.isEmpty()) {
1✔
UNCOV
168
            return null;
×
169
        }
170
        Collection<HandleValue> recordProperties = Streams.failableStream(allValues.stream())
1✔
171
                .filter(value -> !this.isHandleInternalValue(value))
1✔
172
                .collect(Collectors.toList());
1✔
173
        return this.pidRecordFrom(recordProperties).withPID(pid);
1✔
174
    }
175

176
    @NotNull
177
    protected Collection<HandleValue> queryAllHandleValues(final String pid) throws PidNotFoundException, ExternalServiceException {
178
        try {
179
            HandleValue[] values = this.client.resolveHandle(pid, null, null);
1✔
180
            return Stream.of(values)
1✔
181
                    .collect(Collectors.toCollection(ArrayList::new));
1✔
182
        } catch (HandleException e) {
1✔
183
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
1✔
184
                throw new PidNotFoundException(pid, e);
1✔
185
            } else {
186
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
187
            }
188
        }
189
    }
190

191
    @Override
192
    public String registerPidUnchecked(final PIDRecord pidRecord) throws PidAlreadyExistsException, ExternalServiceException {
193
        // Add admin value for configured user only
194
        // TODO add options to add additional adminValues e.g. for user lists?
195
        ArrayList<HandleValue> admin = new ArrayList<>();
×
196
        admin.add(this.adminValue);
×
197
        PIDRecord preparedRecord = pidRecord;
×
198
        for (RecordModifier modifier : this.props.getConfiguredModifiers()) {
×
199
            preparedRecord = modifier.apply(preparedRecord);
×
200
        }
×
201
        ArrayList<HandleValue> futurePairs = this.handleValuesFrom(preparedRecord, Optional.of(admin));
×
202

203
        HandleValue[] futurePairsArray = futurePairs.toArray(new HandleValue[] {});
×
204

205
        try {
206
            this.client.createHandle(preparedRecord.getPid(), futurePairsArray);
×
207
        } catch (HandleException e) {
×
208
            if (e.getCode() == HandleException.HANDLE_ALREADY_EXISTS) {
×
209
                // Should not happen as this has to be checked on the REST handler level.
210
                throw new PidAlreadyExistsException(preparedRecord.getPid());
×
211
            } else {
212
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
213
            }
214
        }
×
215
        return preparedRecord.getPid();
×
216
    }
217

218
    @Override
219
    public boolean updatePid(final PIDRecord pidRecord) throws PidNotFoundException, ExternalServiceException, RecordValidationException {
220
        if (!this.isValidPID(pidRecord.getPid())) {
×
221
            return false;
×
222
        }
223
        PIDRecord preparedRecord = pidRecord;
×
224
        for (RecordModifier modifier : this.props.getConfiguredModifiers()) {
×
225
            preparedRecord = modifier.apply(preparedRecord);
×
226
        }
×
227
        // We need to override the old record as the user has no possibility to update
228
        // single values, and matching is hard.
229
        // The API expects the user to insert what the result should be. Due to the
230
        // Handle Protocol client available
231
        // functions and the way the handle system works with indices (basically value
232
        // identifiers), we use this approach:
233
        // 1) from the old values, take all we want to keep (handle internal values, technical stuff).
234
        // 2) together with the user-given record, merge "valuesToKeep" to a list of
235
        // values with unique indices. Now we have exactly the representation we want.
236
        // But: we cannot tell the handle API what we want, we have to declare how to do it.
237
        // This is why we need two more steps:
238
        // 3) see (by index) which values have to be added, deleted, or updated.
239
        // 4) then add, update, delete in this order. Why this order? We could also remove everything
240
        // at first and then add everything we want, but this would require more actions on the server
241
        // side. And, deleting everything would also delete access control information. So, the safe
242
        // way to do it, is to add things which do not exist yet, update what needs to be updated,
243
        // and in the end remove what needs to be removed (usually nothing!).
244

245
        // index value
246
        Collection<HandleValue> oldHandleValues = this.queryAllHandleValues(preparedRecord.getPid());
×
247
        Map<Integer, HandleValue> recordOld = oldHandleValues.stream()
×
248
                .collect(Collectors.toMap(HandleValue::getIndex, v -> v));
×
249
        // 1)
250
        List<HandleValue> valuesToKeep = oldHandleValues.stream()
×
251
                .filter(this::isHandleInternalValue)
×
252
                .collect(Collectors.toList());
×
253

254
        // 2) Merge requested record and things we want to keep.
255
        Map<Integer, HandleValue> recordNew = handleValuesFrom(preparedRecord, Optional.of(valuesToKeep))
×
256
                .stream()
×
257
                .collect(Collectors.toMap(HandleValue::getIndex, v -> v));
×
258

259
        try {
260
            // 3)
261
            HandleDiff diff = new HandleDiff(recordOld, recordNew);
×
262
            // 4)
263
            if (diff.added().length > 0) {
×
264
                this.client.addHandleValues(preparedRecord.getPid(), diff.added());
×
265
            }
266
            if (diff.updated().length > 0) {
×
267
                this.client.updateHandleValues(preparedRecord.getPid(), diff.updated());
×
268
            }
269
            if (diff.removed().length > 0) {
×
270
                this.client.deleteHandleValues(preparedRecord.getPid(), diff.removed());
×
271
            }
272
        } catch (HandleException e) {
×
273
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
274
                return false;
×
275
            } else {
276
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
277
            }
278
        } catch (Exception e) {
×
279
            throw new RuntimeException("Implementation error in calculating record difference. PLEASE REPORT!", e);
×
280
        }
×
281
        return true;
×
282
    }
283

284
    @Override
285
    public boolean deletePid(final String pid) throws ExternalServiceException {
286
        try {
287
            this.client.deleteHandle(pid);
×
288
        } catch (HandleException e) {
×
289
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
290
                return false;
×
291
            } else {
292
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
293
            }
294
        }
×
295
        return true;
×
296
    }
297

298
    @Override
299
    public Collection<String> resolveAllPidsOfPrefix() throws ExternalServiceException, InvalidConfigException {
300
        HandleCredentials handleCredentials = this.props.getCredentials();
×
301
        if (handleCredentials == null) {
×
302
            throw new InvalidConfigException("No credentials for handle protocol configured.");
×
303
        }
304

305
        PrivateKey key;
306
        {
307
            byte[] privateKeyBytes;
308
            try {
309
                privateKeyBytes = handleCredentials.getPrivateKeyFileContent();
×
310
            } catch (IOException e) {
×
311
                throw new InvalidConfigException("Could not read private key file content.");
×
312
            }
×
313
            if (privateKeyBytes == null || privateKeyBytes.length == 0) {
×
314
                throw new InvalidConfigException("Private Key is empty!");
×
315
            }
316
            byte[] passphrase = handleCredentials.getPrivateKeyPassphraseAsBytes();
×
317
            byte[] privateKeyDecrypted;
318
            // decrypt the private key using the passphrase/cypher
319
            try {
320
                privateKeyDecrypted = Util.decrypt(privateKeyBytes, passphrase);
×
321
            } catch (Exception e) {
×
322
                throw new InvalidConfigException("Private key decryption failed: " + e.getMessage());
×
323
            }
×
324
            try {
325
                key = Util.getPrivateKeyFromBytes(privateKeyDecrypted, 0);
×
326
            } catch (HandleException | InvalidKeySpecException e) {
×
327
                throw new InvalidConfigException("Private key conversion failed: " + e.getMessage());
×
328
            }
×
329
        }
330

331
        PublicKeyAuthenticationInfo auth = new PublicKeyAuthenticationInfo(
×
332
                Util.encodeString(handleCredentials.getUserHandle()),
×
333
                handleCredentials.getPrivateKeyIndex(),
×
334
                key);
335

336
        HandleResolver resolver = new HandleResolver();
×
337
        SiteInfo site;
338
        {
339
            HandleValue[] prefixValues;
340
            try {
341
                prefixValues = resolver.resolveHandle(handleCredentials.getHandleIdentifierPrefix());
×
342
                site = BatchUtil.getFirstPrimarySiteFromHserv(prefixValues, resolver);
×
343
            } catch (HandleException e) {
×
344
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
345
            }
×
346
        }
347

348
        String prefix = handleCredentials.getHandleIdentifierPrefix();
×
349
        try {
350
            return BatchUtil.listHandles(prefix, site, resolver, auth);
×
351
        } catch (HandleException e) {
×
352
            throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
353
        }
354
    }
355

356
    /**
357
     * Avoids an extra constructor in `PIDRecord`. Instead,
358
     * keep such details stored in the PID service implementation.
359
     * 
360
     * @param values HandleValue collection (ordering recommended)
361
     *               that shall be converted into a PIDRecord.
362
     * @return a PID record with values copied from values.
363
     */
364
    protected PIDRecord pidRecordFrom(final Collection<HandleValue> values) {
365
        PIDRecord result = new PIDRecord();
1✔
366
        for (HandleValue v : values) {
1✔
367
            // TODO In future, the type could be resolved to store the human readable name
368
            // here.
369
            result.addEntry(v.getTypeAsString(), "", v.getDataAsString());
1✔
370
        }
1✔
371
        return result;
1✔
372
    }
373

374
    /**
375
     * Convert a `PIDRecord` instance to an array of `HandleValue`s. It is the
376
     * inverse method to `pidRecordFrom`.
377
     * 
378
     * @param pidRecord the record containing values to convert / extract.
379
     * @param toMerge   an optional list to merge the result with.
380
     * @return HandleValues containing the same key-value pairs as the given record,
381
     *         but e.g. without the name.
382
     */
383
    protected ArrayList<HandleValue> handleValuesFrom(
384
            final PIDRecord pidRecord,
385
            final Optional<List<HandleValue>> toMerge)
386
        {
387
        ArrayList<Integer> skippingIndices = new ArrayList<>();
×
388
        ArrayList<HandleValue> result = new ArrayList<>();
×
389
        if (toMerge.isPresent()) {
×
390
            for (HandleValue v : toMerge.get()) {
×
391
                result.add(v);
×
392
                skippingIndices.add(v.getIndex());
×
393
            }
×
394
        }
395
        HandleIndex index = new HandleIndex().skipping(skippingIndices);
×
396
        Map<String, List<PIDRecordEntry>> entries = pidRecord.getEntries();
×
397

398
        for (Entry<String, List<PIDRecordEntry>> entry : entries.entrySet()) {
×
399
            for (PIDRecordEntry val : entry.getValue()) {
×
400
                String key = val.getKey();
×
401
                HandleValue hv = new HandleValue();
×
402
                int i = index.nextIndex();
×
403
                hv.setIndex(i);
×
404
                hv.setType(key.getBytes(StandardCharsets.UTF_8));
×
405
                hv.setData(val.getValue().getBytes(StandardCharsets.UTF_8));
×
406
                result.add(hv);
×
407
                LOG.debug("Entry: ({}) {} <-> {}", i, key, val);
×
408
            }
×
409
        }
×
410
        assert result.size() >= pidRecord.getEntries().keySet().size();
×
411
        return result;
×
412
    }
413

414
    protected static class HandleIndex {
×
415
        // handle record indices start at 1
416
        private int index = 1;
×
417
        private List<Integer> skipping = new ArrayList<>();
×
418

419
        public final int nextIndex() {
420
            int result = index;
×
421
            index += 1;
×
422
            if (index == this.getHsAdminIndex() || skipping.contains(index)) {
×
423
                index += 1;
×
424
            }
425
            return result;
×
426
        }
427

428
        public HandleIndex skipping(List<Integer> skipThose) {
429
            this.skipping = skipThose;
×
430
            return this;
×
431
        }
432

433
        public final int getHsAdminIndex() {
434
            return 100;
×
435
        }
436
    }
437

438
    /**
439
     * Returns true if the PID is valid according to the following criteria:
440
     * - PID is valid according to isIdentifierRegistered
441
     * - If a generator prefix is set, the PID is expedted to have this prefix.
442
     * 
443
     * @param pid the identifier / PID to check.
444
     * @return true if PID is registered (and if has the generatorPrefix, if it
445
     *         exists).
446
     */
447
    protected boolean isValidPID(final String pid) {
448
        boolean isAuthMode = this.props.getCredentials() != null;
×
449
        if (isAuthMode && !pid.startsWith(this.props.getCredentials().getHandleIdentifierPrefix())) {
×
450
            return false;
×
451
        }
452
        try {
NEW
453
            if (!this.isPidRegistered(pid)) {
×
454
                return false;
×
455
            }
456
        } catch (ExternalServiceException e) {
×
457
            return false;
×
458
        }
×
459
        return true;
×
460
    }
461

462
    /**
463
     * Checks if a given value is considered an "internal" or "handle-native" value.
464
     * <p>
465
     * This may be used to filter out administrative information from a PID record.
466
     * 
467
     * @param v the value to check.
468
     * @return true, if the value is conidered "handle-native".
469
     */
470
    protected boolean isHandleInternalValue(HandleValue v) {
471
        boolean isInternalValue = false;
1✔
472
        for (byte[][] typeList : BLACKLIST_NONTYPE_LISTS) {
1✔
473
            for (byte[] typeCode : typeList) {
1✔
474
                isInternalValue = isInternalValue || Arrays.equals(v.getType(), typeCode);
1✔
475
            }
476
        }
477
        return isInternalValue;
1✔
478
    }
479

480
    /**
481
     * Given two Value Maps, it splits the values in those which have been added,
482
     * updated or removed.
483
     * Using this lists, an update can be applied to the old record, to bring it to
484
     * the state of the new record.
485
     */
486
    protected static class HandleDiff {
487
        private final Collection<HandleValue> toAdd = new ArrayList<>();
1✔
488
        private final Collection<HandleValue> toUpdate = new ArrayList<>();
1✔
489
        private final Collection<HandleValue> toRemove = new ArrayList<>();
1✔
490

491
        HandleDiff(
492
            final Map<Integer, HandleValue> recordOld,
493
            final Map<Integer, HandleValue> recordNew
494
        ) throws PidUpdateException {
1✔
495
            for (Entry<Integer, HandleValue> old : recordOld.entrySet()) {
1✔
496
                boolean wasRemoved = !recordNew.containsKey(old.getKey());
1✔
497
                if (wasRemoved) {
1✔
498
                    // if a row in the record is not available anymore, we need to delete it
499
                    toRemove.add(old.getValue());
1✔
500
                } else {
501
                    // otherwise, we should go and update it.
502
                    // we could also check for equality, but this is the safe and easy way.
503
                    // (the handlevalue classes can be complicated and we'd have to check their
504
                    // equality implementation)
505
                    toUpdate.add(recordNew.get(old.getKey()));
1✔
506
                }
507
            }
1✔
508
            for (Entry<Integer, HandleValue> e : recordNew.entrySet()) {
1✔
509
                boolean isNew = !recordOld.containsKey(e.getKey());
1✔
510
                if (isNew) {
1✔
511
                    // if there is a record which is not in the oldRecord, we need to add it.
512
                    toAdd.add(e.getValue());
1✔
513
                }
514
            }
1✔
515

516
            // runtime testing to avoid messing up record states.
517
            String exceptionMsg = "DIFF NOT VALID. Type: %s. Value: %s";
1✔
518
            for (HandleValue v : toRemove) {
1✔
519
                boolean valid = recordOld.containsValue(v) && !recordNew.containsKey(v.getIndex());
1✔
520
                if (!valid) {
1✔
521
                    String message = String.format(exceptionMsg, "Remove", v.toString());
×
522
                    throw new PidUpdateException(message);
×
523
                }
524
            }
1✔
525
            for (HandleValue v : toAdd) {
1✔
526
                boolean valid = !recordOld.containsKey(v.getIndex()) && recordNew.containsValue(v);
1✔
527
                if (!valid) {
1✔
528
                    String message = String.format(exceptionMsg, "Add", v);
×
529
                    throw new PidUpdateException(message);
×
530
                }
531
            }
1✔
532
            for (HandleValue v : toUpdate) {
1✔
533
                boolean valid = recordOld.containsKey(v.getIndex()) && recordNew.containsValue(v);
1✔
534
                if (!valid) {
1✔
535
                    String message = String.format(exceptionMsg, "Update", v);
×
536
                    throw new PidUpdateException(message);
×
537
                }
538
            }
1✔
539
        }
1✔
540

541
        public HandleValue[] added() {
542
            return this.toAdd.toArray(new HandleValue[] {});
1✔
543
        }
544

545
        public HandleValue[] updated() {
546
            return this.toUpdate.toArray(new HandleValue[] {});
1✔
547
        }
548

549
        public HandleValue[] removed() {
550
            return this.toRemove.toArray(new HandleValue[] {});
1✔
551
        }
552
    }
553
}
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