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

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

16 Apr 2025 11:48AM UTC coverage: 72.318%. Remained the same
#499

push

github

web-flow
Merge pull request #294 from kit-data-manager/fix-get-all-pids

Fix: get all PIDs of prefix

0 of 4 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

883 of 1221 relevant lines covered (72.32%)

0.72 hits per line

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

33.61
/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.Set;
14
import java.util.Map.Entry;
15
import java.util.stream.Collectors;
16
import java.util.stream.Stream;
17

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

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

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

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

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

85
    private static final String SERVICE_NAME_HANDLE = "Handle System";
86

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

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

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

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

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

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

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

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

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

194
    @Override
195
    public String queryProperty(final String pid, final TypeDefinition typeDefinition) throws PidNotFoundException, ExternalServiceException {
196
        String[] typeArray = { typeDefinition.getIdentifier() };
1✔
197
        try {
198
            // TODO we assume here that the property only exists once, which will not be
199
            // true in every case.
200
            // The interface likely should be adjusted so we can return all types and do not
201
            // need to return a String.
202
            return this.client.resolveHandle(pid, typeArray, null)[0].getDataAsString();
1✔
203
        } catch (HandleException e) {
1✔
204
            if (e.getCode() == HandleException.INVALID_VALUE) {
1✔
205
                return null;
1✔
206
            } else if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
1✔
207
                throw new PidNotFoundException(pid);
1✔
208
            } else {
209
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
210
            }
211
        }
212
    }
213

214
    @Override
215
    public String registerPidUnchecked(final PIDRecord pidRecord) throws PidAlreadyExistsException, ExternalServiceException {
216
        // Add admin value for configured user only
217
        // TODO add options to add additional adminValues e.g. for user lists?
218
        ArrayList<HandleValue> admin = new ArrayList<>();
×
219
        admin.add(this.adminValue);
×
220
        PIDRecord preparedRecord = pidRecord;
×
221
        for (RecordModifier modifier : this.props.getConfiguredModifiers()) {
×
222
            preparedRecord = modifier.apply(preparedRecord);
×
223
        }
×
224
        ArrayList<HandleValue> futurePairs = this.handleValuesFrom(preparedRecord, Optional.of(admin));
×
225

226
        HandleValue[] futurePairsArray = futurePairs.toArray(new HandleValue[] {});
×
227

228
        try {
229
            this.client.createHandle(preparedRecord.getPid(), futurePairsArray);
×
230
        } catch (HandleException e) {
×
231
            if (e.getCode() == HandleException.HANDLE_ALREADY_EXISTS) {
×
232
                // Should not happen as this has to be checked on the REST handler level.
233
                throw new PidAlreadyExistsException(preparedRecord.getPid());
×
234
            } else {
235
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
236
            }
237
        }
×
238
        return preparedRecord.getPid();
×
239
    }
240

241
    @Override
242
    public boolean updatePID(final PIDRecord pidRecord) throws PidNotFoundException, ExternalServiceException, RecordValidationException {
243
        if (!this.isValidPID(pidRecord.getPid())) {
×
244
            return false;
×
245
        }
246
        PIDRecord preparedRecord = pidRecord;
×
247
        for (RecordModifier modifier : this.props.getConfiguredModifiers()) {
×
248
            preparedRecord = modifier.apply(preparedRecord);
×
249
        }
×
250
        // We need to override the old record as the user has no possibility to update
251
        // single values, and matching is hard.
252
        // The API expects the user to insert what the result should be. Due to the
253
        // Handle Protocol client available
254
        // functions and the way the handle system works with indices (basically value
255
        // identifiers), we use this approach:
256
        // 1) from the old values, take all we want to keep (handle internal values, technical stuff).
257
        // 2) together with the user-given record, merge "valuesToKeep" to a list of
258
        // values with unique indices. Now we have exactly the representation we want.
259
        // But: we cannot tell the handle API what we want, we have to declare how to do it.
260
        // This is why we need two more steps:
261
        // 3) see (by index) which values have to be added, deleted, or updated.
262
        // 4) then add, update, delete in this order. Why this order? We could also remove everything
263
        // at first and then add everything we want, but this would require more actions on the server
264
        // side. And, deleting everything would also delete access control information. So, the safe
265
        // way to do it, is to add things which do not exist yet, update what needs to be updated,
266
        // and in the end remove what needs to be removed (usually nothing!).
267

268
        // index value
269
        Collection<HandleValue> oldHandleValues = this.queryAllHandleValues(preparedRecord.getPid());
×
270
        Map<Integer, HandleValue> recordOld = oldHandleValues.stream()
×
271
                .collect(Collectors.toMap(HandleValue::getIndex, v -> v));
×
272
        // 1)
273
        List<HandleValue> valuesToKeep = oldHandleValues.stream()
×
274
                .filter(this::isHandleInternalValue)
×
275
                .collect(Collectors.toList());
×
276

277
        // 2) Merge requested record and things we want to keep.
278
        Map<Integer, HandleValue> recordNew = handleValuesFrom(preparedRecord, Optional.of(valuesToKeep))
×
279
                .stream()
×
280
                .collect(Collectors.toMap(HandleValue::getIndex, v -> v));
×
281

282
        try {
283
            // 3)
284
            HandleDiff diff = new HandleDiff(recordOld, recordNew);
×
285
            // 4)
286
            if (diff.added().length > 0) {
×
287
                this.client.addHandleValues(preparedRecord.getPid(), diff.added());
×
288
            }
289
            if (diff.updated().length > 0) {
×
290
                this.client.updateHandleValues(preparedRecord.getPid(), diff.updated());
×
291
            }
292
            if (diff.removed().length > 0) {
×
293
                this.client.deleteHandleValues(preparedRecord.getPid(), diff.removed());
×
294
            }
295
        } catch (HandleException e) {
×
296
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
297
                return false;
×
298
            } else {
299
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
300
            }
301
        } catch (Exception e) {
×
302
            throw new RuntimeException("Implementation error in calculating record difference. PLEASE REPORT!", e);
×
303
        }
×
304
        return true;
×
305
    }
306

307
    @Override
308
    public PIDRecord queryByType(String pid, TypeDefinition typeDefinition) throws PidNotFoundException, ExternalServiceException {
309
        PIDRecord allProps = queryAllProperties(pid);
×
310
        if (allProps == null) {
×
311
            return null;
×
312
        }
313
        // only return properties listed in the type definition
314
        Set<String> typeProps = typeDefinition.getAllProperties();
×
315
        PIDRecord result = new PIDRecord();
×
316
        for (String propID : allProps.getPropertyIdentifiers()) {
×
317
            if (typeProps.contains(propID)) {
×
318
                String[] values = allProps.getPropertyValues(propID);
×
319
                for (String value : values) {
×
320
                    result.addEntry(propID, "", value);
×
321
                }
322
            }
323
        }
×
324
        return result;
×
325
    }
326

327
    @Override
328
    public boolean deletePID(final String pid) throws ExternalServiceException {
329
        try {
330
            this.client.deleteHandle(pid);
×
331
        } catch (HandleException e) {
×
332
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
333
                return false;
×
334
            } else {
335
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
336
            }
337
        }
×
338
        return true;
×
339
    }
340

341
    @Override
342
    public Collection<String> resolveAllPidsOfPrefix() throws ExternalServiceException, InvalidConfigException {
343
        HandleCredentials handleCredentials = this.props.getCredentials();
×
344
        if (handleCredentials == null) {
×
345
            throw new InvalidConfigException("No credentials for handle protocol configured.");
×
346
        }
347

348
        PrivateKey key;
349
        {
350
            byte[] privateKeyBytes;
351
            try {
352
                privateKeyBytes = handleCredentials.getPrivateKeyFileContent();
×
353
            } catch (IOException e) {
×
354
                throw new InvalidConfigException("Could not read private key file content.");
×
355
            }
×
356
            if (privateKeyBytes == null || privateKeyBytes.length == 0) {
×
357
                throw new InvalidConfigException("Private Key is empty!");
×
358
            }
359
            byte[] passphrase = handleCredentials.getPrivateKeyPassphraseAsBytes();
×
360
            byte[] privateKeyDecrypted;
361
            // decrypt the private key using the passphrase/cypher
362
            try {
363
                privateKeyDecrypted = Util.decrypt(privateKeyBytes, passphrase);
×
364
            } catch (Exception e) {
×
365
                throw new InvalidConfigException("Private key decryption failed: " + e.getMessage());
×
366
            }
×
367
            try {
368
                key = Util.getPrivateKeyFromBytes(privateKeyDecrypted, 0);
×
369
            } catch (HandleException | InvalidKeySpecException e) {
×
370
                throw new InvalidConfigException("Private key conversion failed: " + e.getMessage());
×
371
            }
×
372
        }
373

374
        PublicKeyAuthenticationInfo auth = new PublicKeyAuthenticationInfo(
×
375
                Util.encodeString(handleCredentials.getUserHandle()),
×
376
                handleCredentials.getPrivateKeyIndex(),
×
377
                key);
378

379
        HandleResolver resolver = new HandleResolver();
×
380
        SiteInfo site;
NEW
381
        String prefix = handleCredentials.getHandleIdentifierPrefix().replace("/", "");
×
382
        {
383
            HandleValue[] prefixValues;
384
            try {
NEW
385
                prefixValues = resolver.resolveHandle(prefix);
×
386
                site = BatchUtil.getFirstPrimarySiteFromHserv(prefixValues, resolver);
×
387
            } catch (HandleException e) {
×
NEW
388
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, "Tried resolving " + prefix, e);
×
389
            }
×
390
        }
391

392
        try {
UNCOV
393
            return BatchUtil.listHandles(prefix, site, resolver, auth);
×
394
        } catch (HandleException e) {
×
NEW
395
            throw new ExternalServiceException(SERVICE_NAME_HANDLE, "Tried resolving " + prefix, e);
×
396
        }
397
    }
398

399
    /**
400
     * Avoids an extra constructor in `PIDRecord`. Instead,
401
     * keep such details stored in the PID service implementation.
402
     * 
403
     * @param values HandleValue collection (ordering recommended)
404
     *               that shall be converted into a PIDRecord.
405
     * @return a PID record with values copied from values.
406
     */
407
    protected PIDRecord pidRecordFrom(final Collection<HandleValue> values) {
408
        PIDRecord result = new PIDRecord();
1✔
409
        for (HandleValue v : values) {
1✔
410
            // TODO In future, the type could be resolved to store the human readable name
411
            // here.
412
            result.addEntry(v.getTypeAsString(), "", v.getDataAsString());
1✔
413
        }
1✔
414
        return result;
1✔
415
    }
416

417
    /**
418
     * Convert a `PIDRecord` instance to an array of `HandleValue`s. It is the
419
     * inverse method to `pidRecordFrom`.
420
     * 
421
     * @param pidRecord the record containing values to convert / extract.
422
     * @param toMerge   an optional list to merge the result with.
423
     * @return HandleValues containing the same key-value pairs as the given record,
424
     *         but e.g. without the name.
425
     */
426
    protected ArrayList<HandleValue> handleValuesFrom(
427
            final PIDRecord pidRecord,
428
            final Optional<List<HandleValue>> toMerge)
429
        {
430
        ArrayList<Integer> skippingIndices = new ArrayList<>();
×
431
        ArrayList<HandleValue> result = new ArrayList<>();
×
432
        if (toMerge.isPresent()) {
×
433
            for (HandleValue v : toMerge.get()) {
×
434
                result.add(v);
×
435
                skippingIndices.add(v.getIndex());
×
436
            }
×
437
        }
438
        HandleIndex index = new HandleIndex().skipping(skippingIndices);
×
439
        Map<String, List<PIDRecordEntry>> entries = pidRecord.getEntries();
×
440

441
        for (Entry<String, List<PIDRecordEntry>> entry : entries.entrySet()) {
×
442
            for (PIDRecordEntry val : entry.getValue()) {
×
443
                String key = val.getKey();
×
444
                HandleValue hv = new HandleValue();
×
445
                int i = index.nextIndex();
×
446
                hv.setIndex(i);
×
447
                hv.setType(key.getBytes(StandardCharsets.UTF_8));
×
448
                hv.setData(val.getValue().getBytes(StandardCharsets.UTF_8));
×
449
                result.add(hv);
×
450
                LOG.debug("Entry: ({}) {} <-> {}", i, key, val);
×
451
            }
×
452
        }
×
453
        assert result.size() >= pidRecord.getEntries().keySet().size();
×
454
        return result;
×
455
    }
456

457
    protected static class HandleIndex {
×
458
        // handle record indices start at 1
459
        private int index = 1;
×
460
        private List<Integer> skipping = new ArrayList<>();
×
461

462
        public final int nextIndex() {
463
            int result = index;
×
464
            index += 1;
×
465
            if (index == this.getHsAdminIndex() || skipping.contains(index)) {
×
466
                index += 1;
×
467
            }
468
            return result;
×
469
        }
470

471
        public HandleIndex skipping(List<Integer> skipThose) {
472
            this.skipping = skipThose;
×
473
            return this;
×
474
        }
475

476
        public final int getHsAdminIndex() {
477
            return 100;
×
478
        }
479
    }
480

481
    /**
482
     * Returns true if the PID is valid according to the following criteria:
483
     * - PID is valid according to isIdentifierRegistered
484
     * - If a generator prefix is set, the PID is expedted to have this prefix.
485
     * 
486
     * @param pid the identifier / PID to check.
487
     * @return true if PID is registered (and if has the generatorPrefix, if it
488
     *         exists).
489
     */
490
    protected boolean isValidPID(final String pid) {
491
        boolean isAuthMode = this.props.getCredentials() != null;
×
492
        if (isAuthMode && !pid.startsWith(this.props.getCredentials().getHandleIdentifierPrefix())) {
×
493
            return false;
×
494
        }
495
        try {
496
            if (!this.isIdentifierRegistered(pid)) {
×
497
                return false;
×
498
            }
499
        } catch (ExternalServiceException e) {
×
500
            return false;
×
501
        }
×
502
        return true;
×
503
    }
504

505
    /**
506
     * Checks if a given value is considered an "internal" or "handle-native" value.
507
     * <p>
508
     * This may be used to filter out administrative information from a PID record.
509
     * 
510
     * @param v the value to check.
511
     * @return true, if the value is conidered "handle-native".
512
     */
513
    protected boolean isHandleInternalValue(HandleValue v) {
514
        boolean isInternalValue = false;
1✔
515
        for (byte[][] typeList : BLACKLIST_NONTYPE_LISTS) {
1✔
516
            for (byte[] typeCode : typeList) {
1✔
517
                isInternalValue = isInternalValue || Arrays.equals(v.getType(), typeCode);
1✔
518
            }
519
        }
520
        return isInternalValue;
1✔
521
    }
522

523
    /**
524
     * Given two Value Maps, it splits the values in those which have been added,
525
     * updated or removed.
526
     * Using this lists, an update can be applied to the old record, to bring it to
527
     * the state of the new record.
528
     */
529
    protected static class HandleDiff {
530
        private final Collection<HandleValue> toAdd = new ArrayList<>();
1✔
531
        private final Collection<HandleValue> toUpdate = new ArrayList<>();
1✔
532
        private final Collection<HandleValue> toRemove = new ArrayList<>();
1✔
533

534
        HandleDiff(
535
            final Map<Integer, HandleValue> recordOld,
536
            final Map<Integer, HandleValue> recordNew
537
        ) throws PidUpdateException {
1✔
538
            for (Entry<Integer, HandleValue> old : recordOld.entrySet()) {
1✔
539
                boolean wasRemoved = !recordNew.containsKey(old.getKey());
1✔
540
                if (wasRemoved) {
1✔
541
                    // if a row in the record is not available anymore, we need to delete it
542
                    toRemove.add(old.getValue());
1✔
543
                } else {
544
                    // otherwise, we should go and update it.
545
                    // we could also check for equality, but this is the safe and easy way.
546
                    // (the handlevalue classes can be complicated and we'd have to check their
547
                    // equality implementation)
548
                    toUpdate.add(recordNew.get(old.getKey()));
1✔
549
                }
550
            }
1✔
551
            for (Entry<Integer, HandleValue> e : recordNew.entrySet()) {
1✔
552
                boolean isNew = !recordOld.containsKey(e.getKey());
1✔
553
                if (isNew) {
1✔
554
                    // if there is a record which is not in the oldRecord, we need to add it.
555
                    toAdd.add(e.getValue());
1✔
556
                }
557
            }
1✔
558

559
            // runtime testing to avoid messing up record states.
560
            String exceptionMsg = "DIFF NOT VALID. Type: %s. Value: %s";
1✔
561
            for (HandleValue v : toRemove) {
1✔
562
                boolean valid = recordOld.containsValue(v) && !recordNew.containsKey(v.getIndex());
1✔
563
                if (!valid) {
1✔
564
                    String message = String.format(exceptionMsg, "Remove", v.toString());
×
565
                    throw new PidUpdateException(message);
×
566
                }
567
            }
1✔
568
            for (HandleValue v : toAdd) {
1✔
569
                boolean valid = !recordOld.containsKey(v.getIndex()) && recordNew.containsValue(v);
1✔
570
                if (!valid) {
1✔
571
                    String message = String.format(exceptionMsg, "Add", v);
×
572
                    throw new PidUpdateException(message);
×
573
                }
574
            }
1✔
575
            for (HandleValue v : toUpdate) {
1✔
576
                boolean valid = recordOld.containsKey(v.getIndex()) && recordNew.containsValue(v);
1✔
577
                if (!valid) {
1✔
578
                    String message = String.format(exceptionMsg, "Update", v);
×
579
                    throw new PidUpdateException(message);
×
580
                }
581
            }
1✔
582
        }
1✔
583

584
        public HandleValue[] added() {
585
            return this.toAdd.toArray(new HandleValue[] {});
1✔
586
        }
587

588
        public HandleValue[] updated() {
589
            return this.toUpdate.toArray(new HandleValue[] {});
1✔
590
        }
591

592
        public HandleValue[] removed() {
593
            return this.toRemove.toArray(new HandleValue[] {});
1✔
594
        }
595
    }
596
}
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