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

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

pending completion
#268

push

github-actions

Pfeil
fix: sanity check in HandleRecord conversion.

This check will likely be removed in 2.0.0. For 1.x.x I decided to leave
it inside. So far, the code worked in practical examples, so there
should be no issues.

8 of 8 new or added lines in 1 file covered. (100.0%)

1031 of 1696 relevant lines covered (60.79%)

0.61 hits per line

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

20.83
/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.UUID;
15
import java.util.Map.Entry;
16
import java.util.stream.Collectors;
17
import java.util.stream.Stream;
18

19
import javax.annotation.PostConstruct;
20
import javax.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

29
import edu.kit.datamanager.pit.common.InvalidConfigException;
30
import edu.kit.datamanager.pit.common.PidUpdateException;
31
import edu.kit.datamanager.pit.configuration.HandleCredentials;
32
import edu.kit.datamanager.pit.configuration.HandleProtocolProperties;
33
import edu.kit.datamanager.pit.domain.PIDRecord;
34
import edu.kit.datamanager.pit.domain.PIDRecordEntry;
35
import edu.kit.datamanager.pit.domain.TypeDefinition;
36
import edu.kit.datamanager.pit.pidgeneration.PidSuffix;
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
1✔
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
    // Properties specific to this adapter.
84
    @Autowired
85
    private HandleProtocolProperties props;
86
    // Handle Protocol implementation
87
    private HSAdapter client;
88
    // indicates if the adapter can modify and create PIDs or just resolve them.
89
    private boolean isAdminMode = false;
1✔
90
    // the value that is appended to every new record.
91
    private HandleValue adminValue;
92

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

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

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

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

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

146
    @Override
147
    public boolean isIdentifierRegistered(PidSuffix suffix) throws IOException {
148
        Optional<String> maybePrefix = this.getPrefix();
×
149
        if (maybePrefix.isPresent()) {
×
150
            return this.isIdentifierRegistered(suffix.getWithPrefix(maybePrefix.get()));
×
151
        } else {
152
            throw new IOException("No writeable prefix is configured. Can not check if identifier is registered.");
×
153
        }
154
    }
155

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

171
    @Override
172
    public PIDRecord queryAllProperties(final String pid) throws IOException {
173
        Collection<HandleValue> allValues = this.queryAllHandleValues(pid);
1✔
174
        if (allValues.isEmpty()) {
1✔
175
            return null;
1✔
176
        }
177
        Collection<HandleValue> recordProperties = Streams.stream(allValues.stream())
1✔
178
                .filter(value -> !this.isHandleInternalValue(value))
1✔
179
                .collect(Collectors.toList());
1✔
180
        return this.pidRecordFrom(recordProperties).withPID(pid);
1✔
181
    }
182

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

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

217
    @Override
218
    public String registerPidUnchecked(final PIDRecord pidRecord) throws IOException {
219
        // Add admin value for configured user only
220
        // TODO add options to add additional adminValues e.g. for user lists?
221
        ArrayList<HandleValue> admin = new ArrayList<>();
×
222
        admin.add(this.adminValue);
×
223
        ArrayList<HandleValue> futurePairs = this.handleValuesFrom(pidRecord, Optional.of(admin));
×
224

225
        HandleValue[] futurePairsArray = futurePairs.toArray(new HandleValue[] {});
×
226
        // sanity check
227
        if (futurePairsArray.length < pidRecord.getEntries().keySet().size()) {
×
228
            throw new IOException(
×
229
                String.format(
×
230
                    "Error extracting pairs from record. Extracted %d from at least %d",
231
                    futurePairsArray.length,
×
232
                    pidRecord.getEntries().keySet().size()
×
233
                )
234
            );
235
        }
236

237
        try {
238
            this.client.createHandle(pidRecord.getPid(), futurePairsArray);
×
239
        } catch (HandleException e) {
×
240
            if (e.getCode() == HandleException.HANDLE_ALREADY_EXISTS) {
×
241
                // Should not happen as this has to be checked on the REST handler level.
242
                throw new IOException("PID already exists. This is an application error, please report it.", e);
×
243
            } else {
244
                throw new IOException(e);
×
245
            }
246
        }
×
247
        return pidRecord.getPid();
×
248
    }
249

250
    @Override
251
    public boolean updatePID(PIDRecord pidRecord) throws IOException {
252
        if (!this.isValidPID(pidRecord.getPid())) {
×
253
            return false;
×
254
        }
255
        // We need to override the old record as the user has no possibility to update
256
        // single values, and matching is hard.
257
        // The API expects the user to insert what the result should be. Due to the
258
        // Handle Protocol client available
259
        // functions and the way the handle system works with indices (basically value
260
        // identifiers), we use this approach:
261
        // 1) from the old values, take all we want to keep.
262
        // 2) together with the user-given record, merge "valuesToKeep" to a list of
263
        // values with unique indices.
264
        // 3) see (by index) which values have to be added, deleted, or updated.
265
        // 4) then add, update, delete in this order.
266

267
        // index value
268
        Map<Integer, HandleValue> recordOld = this.queryAllHandleValues(pidRecord.getPid())
×
269
                .stream()
×
270
                .collect(Collectors.toMap(v -> v.getIndex(), v -> v));
×
271
        // Streams.stream makes a stream failable, i.e. allows filtering with
272
        // exceptions. A new Java version **might** solve this.
273
        List<HandleValue> valuesToKeep = Streams.stream(this.queryAllHandleValues(pidRecord.getPid()).stream())
×
274
                .filter(this::isHandleInternalValue)
×
275
                .collect(Collectors.toList());
×
276

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

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

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

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

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

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

367
        PublicKeyAuthenticationInfo auth = new PublicKeyAuthenticationInfo(
×
368
                Util.encodeString(handleCredentials.getUserHandle()),
×
369
                handleCredentials.getPrivateKeyIndex(),
×
370
                key);
371

372
        HandleResolver resolver = new HandleResolver();
×
373
        SiteInfo site;
374
        {
375
            HandleValue[] prefixValues;
376
            try {
377
                prefixValues = resolver.resolveHandle(handleCredentials.getHandleIdentifierPrefix());
×
378
                site = BatchUtil.getFirstPrimarySiteFromHserv(prefixValues, resolver);
×
379
            } catch (HandleException e) {
×
380
                throw new IOException(e.getMessage());
×
381
            }
×
382
        }
383

384
        String prefix = handleCredentials.getHandleIdentifierPrefix();
×
385
        try {
386
            return BatchUtil.listHandles(prefix, site, resolver, auth);
×
387
        } catch (HandleException e) {
×
388
            throw new IOException(e.getMessage());
×
389
        }
390
    }
391

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

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

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

450
    protected static class HandleIndex {
×
451
        // handle record indices start at 1
452
        private int index = 1;
×
453
        private List<Integer> skipping = new ArrayList<>();
×
454

455
        public final int nextIndex() {
456
            int result = index;
×
457
            index += 1;
×
458
            if (index == this.getHsAdminIndex() || skipping.contains(index)) {
×
459
                index += 1;
×
460
            }
461
            return result;
×
462
        }
463

464
        public HandleIndex skipping(List<Integer> skipThose) {
465
            this.skipping = skipThose;
×
466
            return this;
×
467
        }
468

469
        public final int getHsAdminIndex() {
470
            return 100;
×
471
        }
472
    }
473

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

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

516
    /**
517
     * Given two Value Maps, it splits the values in those which have been added,
518
     * updated or removed.
519
     * Using this lists, an update can be applied to the old record, to bring it to
520
     * the state of the new record.
521
     */
522
    protected static class HandleDiff {
523
        private final Collection<HandleValue> toAdd = new ArrayList<>();
×
524
        private final Collection<HandleValue> toUpdate = new ArrayList<>();
×
525
        private final Collection<HandleValue> toRemove = new ArrayList<>();
×
526

527
        HandleDiff(final Map<Integer, HandleValue> recordOld, final Map<Integer, HandleValue> recordNew)
528
                throws PidUpdateException {
×
529
            // old_indexes should only contain indexes we do not override/update anyway, so
530
            // we can delete them afterwards.
531
            for (Entry<Integer, HandleValue> old : recordOld.entrySet()) {
×
532
                boolean wasRemoved = !recordNew.containsKey(old.getKey());
×
533
                if (wasRemoved) {
×
534
                    toRemove.add(old.getValue());
×
535
                } else {
536
                    toUpdate.add(recordNew.get(old.getKey()));
×
537
                }
538
            }
×
539
            for (Entry<Integer, HandleValue> e : recordNew.entrySet()) {
×
540
                boolean isNew = !recordOld.containsKey(e.getKey());
×
541
                if (isNew) {
×
542
                    toAdd.add(e.getValue());
×
543
                }
544
            }
×
545

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

571
        public HandleValue[] added() {
572
            return this.toAdd.toArray(new HandleValue[] {});
×
573
        }
574

575
        public HandleValue[] updated() {
576
            return this.toUpdate.toArray(new HandleValue[] {});
×
577
        }
578

579
        public HandleValue[] removed() {
580
            return this.toRemove.toArray(new HandleValue[] {});
×
581
        }
582
    }
583
}
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