• 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

34.89
/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 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.domain.TypeDefinition;
38
import edu.kit.datamanager.pit.pidsystem.IIdentifierSystem;
39
import net.handle.api.HSAdapter;
40
import net.handle.api.HSAdapterFactory;
41
import net.handle.apps.batch.BatchUtil;
42
import net.handle.hdllib.Common;
43
import net.handle.hdllib.HandleException;
44
import net.handle.hdllib.HandleResolver;
45
import net.handle.hdllib.HandleValue;
46
import net.handle.hdllib.PublicKeyAuthenticationInfo;
47
import net.handle.hdllib.SiteInfo;
48
import net.handle.hdllib.Util;
49

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

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

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

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

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

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

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

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

121
        } else {
122
            HandleCredentials credentials = props.getCredentials();
×
123
            // Check if key file is plausible, throw exceptions if something is wrong.
124
            byte[] privateKey = credentials.getPrivateKeyFileContent();
×
125
            byte[] passphrase = credentials.getPrivateKeyPassphraseAsBytes();
×
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.of(this.props.getCredentials()).map(HandleCredentials::getHandleIdentifierPrefix);
×
144
        } else {
145
            return Optional.empty();
×
146
        }
147
    }
148

149
    @Override
150
    public boolean isIdentifierRegistered(final String pid) throws ExternalServiceException {
151
        HandleValue[] recordProperties = null;
1✔
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 {
NEW
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 queryAllProperties(final String pid) throws PidNotFoundException, ExternalServiceException {
166
        Collection<HandleValue> allValues = this.queryAllHandleValues(pid);
1✔
167
        if (allValues.isEmpty()) {
1✔
168
            return null;
1✔
169
        }
170
        Collection<HandleValue> recordProperties = Streams.stream(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
1✔
181
                    .of(values)
1✔
182
                    .collect(Collectors.toCollection(ArrayList::new));
1✔
183
        } catch (HandleException e) {
1✔
184
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
1✔
185
                return new ArrayList<>();
1✔
186
            } else {
NEW
187
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
188
            }
189
        }
190
    }
191

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

212
    @Override
213
    public String registerPidUnchecked(final PIDRecord pidRecord) throws PidAlreadyExistsException, ExternalServiceException {
214
        // Add admin value for configured user only
215
        // TODO add options to add additional adminValues e.g. for user lists?
216
        ArrayList<HandleValue> admin = new ArrayList<>();
×
217
        admin.add(this.adminValue);
×
218
        ArrayList<HandleValue> futurePairs = this.handleValuesFrom(pidRecord, Optional.of(admin));
×
219

220
        HandleValue[] futurePairsArray = futurePairs.toArray(new HandleValue[] {});
×
221

222
        try {
223
            this.client.createHandle(pidRecord.getPid(), futurePairsArray);
×
224
        } catch (HandleException e) {
×
225
            if (e.getCode() == HandleException.HANDLE_ALREADY_EXISTS) {
×
226
                // Should not happen as this has to be checked on the REST handler level.
NEW
227
                throw new PidAlreadyExistsException(pidRecord.getPid());
×
228
            } else {
NEW
229
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
230
            }
231
        }
×
232
        return pidRecord.getPid();
×
233
    }
234

235
    @Override
236
    public boolean updatePID(PIDRecord pidRecord) throws PidNotFoundException, ExternalServiceException, RecordValidationException {
237
        if (!this.isValidPID(pidRecord.getPid())) {
×
238
            return false;
×
239
        }
240
        // We need to override the old record as the user has no possibility to update
241
        // single values, and matching is hard.
242
        // The API expects the user to insert what the result should be. Due to the
243
        // Handle Protocol client available
244
        // functions and the way the handle system works with indices (basically value
245
        // identifiers), we use this approach:
246
        // 1) from the old values, take all we want to keep.
247
        // 2) together with the user-given record, merge "valuesToKeep" to a list of
248
        // values with unique indices.
249
        // 3) see (by index) which values have to be added, deleted, or updated.
250
        // 4) then add, update, delete in this order.
251

252
        // index value
253
        Map<Integer, HandleValue> recordOld = this.queryAllHandleValues(pidRecord.getPid())
×
254
                .stream()
×
255
                .collect(Collectors.toMap(v -> v.getIndex(), v -> v));
×
256
        // Streams.stream makes a stream failable, i.e. allows filtering with
257
        // exceptions. A new Java version **might** solve this.
258
        List<HandleValue> valuesToKeep = Streams.stream(this.queryAllHandleValues(pidRecord.getPid()).stream())
×
259
                .filter(this::isHandleInternalValue)
×
260
                .collect(Collectors.toList());
×
261

262
        // Merge requested record and things we want to keep.
263
        Map<Integer, HandleValue> recordNew = handleValuesFrom(pidRecord, Optional.of(valuesToKeep))
×
264
                .stream()
×
265
                .collect(Collectors.toMap(v -> v.getIndex(), v -> v));
×
266

267
        try {
268
            HandleDiff diff = new HandleDiff(recordOld, recordNew);
×
269
            if (diff.added().length > 0) {
×
270
                this.client.addHandleValues(pidRecord.getPid(), diff.added());
×
271
            }
272
            if (diff.updated().length > 0) {
×
273
                this.client.updateHandleValues(pidRecord.getPid(), diff.updated());
×
274
            }
275
            if (diff.removed().length > 0) {
×
276
                this.client.deleteHandleValues(pidRecord.getPid(), diff.removed());
×
277
            }
278
        } catch (HandleException e) {
×
279
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
280
                return false;
×
281
            } else {
NEW
282
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
283
            }
284
        } catch (Exception e) {
×
NEW
285
            throw new RuntimeException("Implementation error in calculating record difference. PLEASE REPORT!", e);
×
286
        }
×
287
        return true;
×
288
    }
289

290
    @Override
291
    public PIDRecord queryByType(String pid, TypeDefinition typeDefinition) throws PidNotFoundException, ExternalServiceException {
292
        PIDRecord allProps = queryAllProperties(pid);
×
293
        if (allProps == null) {
×
294
            return null;
×
295
        }
296
        // only return properties listed in the type definition
297
        Set<String> typeProps = typeDefinition.getAllProperties();
×
298
        PIDRecord result = new PIDRecord();
×
299
        for (String propID : allProps.getPropertyIdentifiers()) {
×
300
            if (typeProps.contains(propID)) {
×
301
                String[] values = allProps.getPropertyValues(propID);
×
302
                for (String value : values) {
×
303
                    result.addEntry(propID, "", value);
×
304
                }
305
            }
306
        }
×
307
        return result;
×
308
    }
309

310
    @Override
311
    public boolean deletePID(final String pid) throws ExternalServiceException {
312
        try {
313
            this.client.deleteHandle(pid);
×
314
        } catch (HandleException e) {
×
315
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
316
                return false;
×
317
            } else {
NEW
318
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
319
            }
320
        }
×
NEW
321
        return true;
×
322
    }
323

324
    @Override
325
    public Collection<String> resolveAllPidsOfPrefix() throws ExternalServiceException, InvalidConfigException {
326
        HandleCredentials handleCredentials = this.props.getCredentials();
×
327
        if (handleCredentials == null) {
×
328
            throw new InvalidConfigException("No credentials for handle protocol configured.");
×
329
        }
330

331
        PrivateKey key;
332
        {
333
            byte[] privateKeyBytes;
334
            try {
NEW
335
                privateKeyBytes = handleCredentials.getPrivateKeyFileContent();
×
NEW
336
            } catch (IOException e) {
×
NEW
337
                throw new InvalidConfigException("Could not read private key file content.");
×
NEW
338
            }
×
339
            if (privateKeyBytes == null || privateKeyBytes.length == 0) {
×
340
                throw new InvalidConfigException("Private Key is empty!");
×
341
            }
342
            byte[] passphrase = handleCredentials.getPrivateKeyPassphraseAsBytes();
×
343
            byte[] privateKeyDecrypted;
344
            // decrypt the private key using the passphrase/cypher
345
            try {
346
                privateKeyDecrypted = Util.decrypt(privateKeyBytes, passphrase);
×
347
            } catch (Exception e) {
×
348
                throw new InvalidConfigException("Private key decryption failed: " + e.getMessage());
×
349
            }
×
350
            try {
351
                key = Util.getPrivateKeyFromBytes(privateKeyDecrypted, 0);
×
352
            } catch (HandleException | InvalidKeySpecException e) {
×
353
                throw new InvalidConfigException("Private key conversion failed: " + e.getMessage());
×
354
            }
×
355
        }
356

357
        PublicKeyAuthenticationInfo auth = new PublicKeyAuthenticationInfo(
×
358
                Util.encodeString(handleCredentials.getUserHandle()),
×
359
                handleCredentials.getPrivateKeyIndex(),
×
360
                key);
361

362
        HandleResolver resolver = new HandleResolver();
×
363
        SiteInfo site;
364
        {
365
            HandleValue[] prefixValues;
366
            try {
367
                prefixValues = resolver.resolveHandle(handleCredentials.getHandleIdentifierPrefix());
×
368
                site = BatchUtil.getFirstPrimarySiteFromHserv(prefixValues, resolver);
×
369
            } catch (HandleException e) {
×
NEW
370
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
371
            }
×
372
        }
373

374
        String prefix = handleCredentials.getHandleIdentifierPrefix();
×
375
        try {
376
            return BatchUtil.listHandles(prefix, site, resolver, auth);
×
377
        } catch (HandleException e) {
×
NEW
378
            throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
379
        }
380
    }
381

382
    /**
383
     * Avoids an extra constructor in `PIDRecord`. Instead,
384
     * keep such details stored in the PID service implementation.
385
     * 
386
     * @param values HandleValue collection (ordering recommended)
387
     *               that shall be converted into a PIDRecord.
388
     * @return a PID record with values copied from values.
389
     */
390
    protected PIDRecord pidRecordFrom(final Collection<HandleValue> values) {
391
        PIDRecord result = new PIDRecord();
1✔
392
        for (HandleValue v : values) {
1✔
393
            // TODO In future, the type could be resolved to store the human readable name
394
            // here.
395
            result.addEntry(v.getTypeAsString(), "", v.getDataAsString());
1✔
396
        }
1✔
397
        return result;
1✔
398
    }
399

400
    /**
401
     * Convert a `PIDRecord` instance to an array of `HandleValue`s. It is the
402
     * inverse method to `pidRecordFrom`.
403
     * 
404
     * @param pidRecord the record containing values to convert / extract.
405
     * @param toMerge   an optional list to merge the result with.
406
     * @return HandleValues containing the same key-value pairs as the given record,
407
     *         but e.g. without the name.
408
     */
409
    protected ArrayList<HandleValue> handleValuesFrom(
410
            final PIDRecord pidRecord,
411
            final Optional<List<HandleValue>> toMerge)
412
        {
413
        ArrayList<Integer> skippingIndices = new ArrayList<>();
×
414
        ArrayList<HandleValue> result = new ArrayList<>();
×
415
        if (toMerge.isPresent()) {
×
416
            for (HandleValue v : toMerge.get()) {
×
417
                result.add(v);
×
418
                skippingIndices.add(v.getIndex());
×
419
            }
×
420
        }
421
        HandleIndex index = new HandleIndex().skipping(skippingIndices);
×
422
        Map<String, List<PIDRecordEntry>> entries = pidRecord.getEntries();
×
423

424
        for (Entry<String, List<PIDRecordEntry>> entry : entries.entrySet()) {
×
425
            for (PIDRecordEntry val : entry.getValue()) {
×
426
                String key = val.getKey();
×
427
                HandleValue hv = new HandleValue();
×
428
                int i = index.nextIndex();
×
429
                hv.setIndex(i);
×
430
                hv.setType(key.getBytes(StandardCharsets.UTF_8));
×
431
                hv.setData(val.getValue().getBytes(StandardCharsets.UTF_8));
×
432
                result.add(hv);
×
433
                LOG.debug("Entry: ({}) {} <-> {}", i, key, val);
×
434
            }
×
435
        }
×
436
        assert result.size() >= pidRecord.getEntries().keySet().size();
×
437
        return result;
×
438
    }
439

440
    protected static class HandleIndex {
×
441
        // handle record indices start at 1
442
        private int index = 1;
×
443
        private List<Integer> skipping = new ArrayList<>();
×
444

445
        public final int nextIndex() {
446
            int result = index;
×
447
            index += 1;
×
448
            if (index == this.getHsAdminIndex() || skipping.contains(index)) {
×
449
                index += 1;
×
450
            }
451
            return result;
×
452
        }
453

454
        public HandleIndex skipping(List<Integer> skipThose) {
455
            this.skipping = skipThose;
×
456
            return this;
×
457
        }
458

459
        public final int getHsAdminIndex() {
460
            return 100;
×
461
        }
462
    }
463

464
    /**
465
     * Returns true if the PID is valid according to the following criteria:
466
     * - PID is valid according to isIdentifierRegistered
467
     * - If a generator prefix is set, the PID is expedted to have this prefix.
468
     * 
469
     * @param pid the identifier / PID to check.
470
     * @return true if PID is registered (and if has the generatorPrefix, if it
471
     *         exists).
472
     */
473
    protected boolean isValidPID(final String pid) {
474
        boolean isAuthMode = this.props.getCredentials() != null;
×
475
        if (isAuthMode && !pid.startsWith(this.props.getCredentials().getHandleIdentifierPrefix())) {
×
476
            return false;
×
477
        }
478
        try {
479
            if (!this.isIdentifierRegistered(pid)) {
×
480
                return false;
×
481
            }
NEW
482
        } catch (ExternalServiceException e) {
×
483
            return false;
×
484
        }
×
485
        return true;
×
486
    }
487

488
    /**
489
     * Checks if a given value is considered an "internal" or "handle-native" value.
490
     * 
491
     * This may be used to filter out administrative information from a PID record.
492
     * 
493
     * @param v the value to check.
494
     * @return true, if the value is conidered "handle-native".
495
     */
496
    protected boolean isHandleInternalValue(HandleValue v) {
497
        boolean isInternalValue = false;
1✔
498
        for (byte[][] typeList : BLACKLIST_NONTYPE_LISTS) {
1✔
499
            for (byte[] typeCode : typeList) {
1✔
500
                isInternalValue = isInternalValue || Arrays.equals(v.getType(), typeCode);
1✔
501
            }
502
        }
503
        return isInternalValue;
1✔
504
    }
505

506
    /**
507
     * Given two Value Maps, it splits the values in those which have been added,
508
     * updated or removed.
509
     * Using this lists, an update can be applied to the old record, to bring it to
510
     * the state of the new record.
511
     */
512
    protected static class HandleDiff {
513
        private final Collection<HandleValue> toAdd = new ArrayList<>();
1✔
514
        private final Collection<HandleValue> toUpdate = new ArrayList<>();
1✔
515
        private final Collection<HandleValue> toRemove = new ArrayList<>();
1✔
516

517
        HandleDiff(
518
            final Map<Integer, HandleValue> recordOld,
519
            final Map<Integer, HandleValue> recordNew
520
        ) throws PidUpdateException {
1✔
521
            for (Entry<Integer, HandleValue> old : recordOld.entrySet()) {
1✔
522
                boolean wasRemoved = !recordNew.containsKey(old.getKey());
1✔
523
                if (wasRemoved) {
1✔
524
                    // if a row in the record is not available anymore, we need to delete it
525
                    toRemove.add(old.getValue());
1✔
526
                } else {
527
                    // otherwise, we should go and update it.
528
                    // we could also check for equality, but this is the safe and easy way.
529
                    // (the handlevalue classes can be complicated and we'd have to check their
530
                    // equality implementation)
531
                    toUpdate.add(recordNew.get(old.getKey()));
1✔
532
                }
533
            }
1✔
534
            for (Entry<Integer, HandleValue> e : recordNew.entrySet()) {
1✔
535
                boolean isNew = !recordOld.containsKey(e.getKey());
1✔
536
                if (isNew) {
1✔
537
                    // if there is a record which is not in the oldRecord, we need to add it.
538
                    toAdd.add(e.getValue());
1✔
539
                }
540
            }
1✔
541

542
            // runtime testing to avoid messing up record states.
543
            String exceptionMsg = "DIFF NOT VALID. Type: %s. Value: %s";
1✔
544
            for (HandleValue v : toRemove) {
1✔
545
                boolean valid = recordOld.containsValue(v) && !recordNew.containsKey(v.getIndex());
1✔
546
                if (!valid) {
1✔
547
                    String message = String.format(exceptionMsg, "Remove", v.toString());
×
548
                    throw new PidUpdateException(message);
×
549
                }
550
            }
1✔
551
            for (HandleValue v : toAdd) {
1✔
552
                boolean valid = !recordOld.containsKey(v.getIndex()) && recordNew.containsValue(v);
1✔
553
                if (!valid) {
1✔
554
                    String message = String.format(exceptionMsg, "Add", v.toString());
×
555
                    throw new PidUpdateException(message);
×
556
                }
557
            }
1✔
558
            for (HandleValue v : toUpdate) {
1✔
559
                boolean valid = recordOld.containsKey(v.getIndex()) && recordNew.containsValue(v);
1✔
560
                if (!valid) {
1✔
561
                    String message = String.format(exceptionMsg, "Update", v.toString());
×
562
                    throw new PidUpdateException(message);
×
563
                }
564
            }
1✔
565
        }
1✔
566

567
        public HandleValue[] added() {
568
            return this.toAdd.toArray(new HandleValue[] {});
1✔
569
        }
570

571
        public HandleValue[] updated() {
572
            return this.toUpdate.toArray(new HandleValue[] {});
1✔
573
        }
574

575
        public HandleValue[] removed() {
576
            return this.toRemove.toArray(new HandleValue[] {});
1✔
577
        }
578
    }
579
}
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