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

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

29 Jan 2025 04:42PM UTC coverage: 77.85%. First build
#474

Pull #271

github

web-flow
Merge 76236f5ae into 4c17b004d
Pull Request #271: Separate resolver from the configured system

83 of 116 new or added lines in 6 files covered. (71.55%)

956 of 1228 relevant lines covered (77.85%)

0.78 hits per line

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

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

3
import java.io.IOException;
4
import java.security.PrivateKey;
5
import java.security.spec.InvalidKeySpecException;
6
import java.util.ArrayList;
7
import java.util.Collection;
8
import java.util.List;
9
import java.util.Map;
10
import java.util.Optional;
11
import java.util.stream.Collectors;
12
import java.util.stream.Stream;
13

14
import edu.kit.datamanager.pit.recordModifiers.RecordModifier;
15
import jakarta.annotation.PostConstruct;
16
import jakarta.validation.constraints.NotNull;
17

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

43
/**
44
 * Uses the official java library to interact with the handle system using the
45
 * handle protocol.
46
 */
47
@Component
48
@ConditionalOnBean(HandleProtocolProperties.class)
49
public class HandleProtocolAdapter implements IIdentifierSystem {
50

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

53
    private static final String SERVICE_NAME_HANDLE = "Handle System";
54

55
    // Properties specific to this adapter.
56
    @Autowired
57
    final private HandleProtocolProperties props;
58
    // Handle Protocol implementation
59
    private HSAdapter client;
60
    // indicates if the adapter can modify and create PIDs or just resolve them.
61
    private boolean isAdminMode = false;
1✔
62
    // the value that is appended to every new record.
63
    private HandleValue adminValue;
64

65
    // For testing
66
    public HandleProtocolAdapter(HandleProtocolProperties props) {
1✔
67
        this.props = props;
1✔
68
    }
1✔
69

70
    /**
71
     * Initializes internal classes.
72
     * We use this method with the @PostConstruct annotation to run it
73
     * after the constructor and after springs autowiring is done properly
74
     * to make sure that all properties are already autowired.
75
     * 
76
     * @throws HandleException        if a handle system error occurs.
77
     * @throws InvalidConfigException if the configuration is invalid, e.g. a path
78
     *                                does not lead to a file.
79
     * @throws IOException            if the private key file can not be read.
80
     */
81
    @PostConstruct
82
    public void init() throws InvalidConfigException, HandleException, IOException {
83
        LOG.info("Using PID System 'Handle'");
1✔
84
        this.isAdminMode = props.getCredentials() != null;
1✔
85

86
        if (!this.isAdminMode) {
1✔
87
            LOG.warn("No credentials found. Starting Handle Adapter with no administrative privileges.");
1✔
88
            this.client = HSAdapterFactory.newInstance();
1✔
89

90
        } else {
91
            HandleCredentials credentials = props.getCredentials();
×
92
            // Check if key file is plausible, throw exceptions if something is wrong.
93
            byte[] privateKey = credentials.getPrivateKeyFileContent();
×
94
            byte[] passphrase = credentials.getPrivateKeyPassphraseAsBytes();
×
95
            LOG.debug("Logging in with user {}", credentials.getUserHandle());
×
96
            this.client = HSAdapterFactory.newInstance(
×
97
                    credentials.getUserHandle(),
×
98
                    credentials.getPrivateKeyIndex(),
×
99
                    privateKey,
100
                    passphrase // "use null for unencrypted keys"
101
            );
102
            this.adminValue = this.client.createAdminValue(
×
103
                    props.getCredentials().getUserHandle(),
×
104
                    props.getCredentials().getPrivateKeyIndex(),
×
NEW
105
                    new HandleIndex().getHsAdminIndex());
×
106
        }
107
    }
1✔
108

109
    @Override
110
    public Optional<String> getPrefix() {
111
        if (this.isAdminMode) {
1✔
112
            return Optional.ofNullable(this.props.getCredentials()).map(HandleCredentials::getHandleIdentifierPrefix);
×
113
        } else {
114
            return Optional.empty();
1✔
115
        }
116
    }
117

118
    @Override
119
    public boolean isPidRegistered(final String pid) throws ExternalServiceException {
120
        HandleValue[] recordProperties;
121
        try {
122
            recordProperties = this.client.resolveHandle(pid, null, null);
1✔
123
        } catch (HandleException e) {
1✔
124
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
1✔
125
                return false;
1✔
126
            } else {
127
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
128
            }
129
        }
1✔
130
        return recordProperties != null && recordProperties.length > 0;
1✔
131
    }
132

133
    @Override
134
    public PIDRecord queryPid(final String pid) throws PidNotFoundException, ExternalServiceException {
135
        Collection<HandleValue> allValues = this.queryAllHandleValues(pid);
1✔
136
        if (allValues.isEmpty()) {
1✔
137
            return null;
×
138
        }
139
        Collection<HandleValue> recordProperties = Streams.failableStream(allValues.stream())
1✔
140
                .filter(value -> !HandleBehavior.isHandleInternalValue(value))
1✔
141
                .collect(Collectors.toList());
1✔
142
        return HandleBehavior.recordFrom(recordProperties).withPID(pid);
1✔
143
    }
144

145
    @NotNull
146
    protected Collection<HandleValue> queryAllHandleValues(final String pid) throws PidNotFoundException, ExternalServiceException {
147
        try {
148
            HandleValue[] values = this.client.resolveHandle(pid, null, null);
1✔
149
            return Stream.of(values)
1✔
150
                    .collect(Collectors.toCollection(ArrayList::new));
1✔
151
        } catch (HandleException e) {
1✔
152
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
1✔
153
                throw new PidNotFoundException(pid, e);
1✔
154
            } else {
155
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
156
            }
157
        }
158
    }
159

160
    @Override
161
    public String registerPidUnchecked(final PIDRecord pidRecord) throws PidAlreadyExistsException, ExternalServiceException {
162
        // Add admin value for configured user only
163
        // TODO add options to add additional adminValues e.g. for user lists?
164
        ArrayList<HandleValue> admin = new ArrayList<>();
×
165
        admin.add(this.adminValue);
×
166
        PIDRecord preparedRecord = pidRecord;
×
167
        for (RecordModifier modifier : this.props.getConfiguredModifiers()) {
×
168
            preparedRecord = modifier.apply(preparedRecord);
×
169
        }
×
NEW
170
        ArrayList<HandleValue> futurePairs = HandleBehavior.handleValuesFrom(preparedRecord, Optional.of(admin));
×
171

172
        HandleValue[] futurePairsArray = futurePairs.toArray(new HandleValue[] {});
×
173

174
        try {
175
            this.client.createHandle(preparedRecord.getPid(), futurePairsArray);
×
176
        } catch (HandleException e) {
×
177
            if (e.getCode() == HandleException.HANDLE_ALREADY_EXISTS) {
×
178
                // Should not happen as this has to be checked on the REST handler level.
179
                throw new PidAlreadyExistsException(preparedRecord.getPid());
×
180
            } else {
181
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
182
            }
183
        }
×
184
        return preparedRecord.getPid();
×
185
    }
186

187
    @Override
188
    public boolean updatePid(final PIDRecord pidRecord) throws PidNotFoundException, ExternalServiceException, RecordValidationException {
189
        if (!this.isValidPID(pidRecord.getPid())) {
×
190
            return false;
×
191
        }
192
        PIDRecord preparedRecord = pidRecord;
×
193
        for (RecordModifier modifier : this.props.getConfiguredModifiers()) {
×
194
            preparedRecord = modifier.apply(preparedRecord);
×
195
        }
×
196
        // We need to override the old record as the user has no possibility to update
197
        // single values, and matching is hard.
198
        // The API expects the user to insert what the result should be. Due to the
199
        // Handle Protocol client available
200
        // functions and the way the handle system works with indices (basically value
201
        // identifiers), we use this approach:
202
        // 1) from the old values, take all we want to keep (handle internal values, technical stuff).
203
        // 2) together with the user-given record, merge "valuesToKeep" to a list of
204
        // values with unique indices. Now we have exactly the representation we want.
205
        // But: we cannot tell the handle API what we want, we have to declare how to do it.
206
        // This is why we need two more steps:
207
        // 3) see (by index) which values have to be added, deleted, or updated.
208
        // 4) then add, update, delete in this order. Why this order? We could also remove everything
209
        // at first and then add everything we want, but this would require more actions on the server
210
        // side. And, deleting everything would also delete access control information. So, the safe
211
        // way to do it, is to add things which do not exist yet, update what needs to be updated,
212
        // and in the end remove what needs to be removed (usually nothing!).
213

214
        // index value
215
        Collection<HandleValue> oldHandleValues = this.queryAllHandleValues(preparedRecord.getPid());
×
216
        Map<Integer, HandleValue> recordOld = oldHandleValues.stream()
×
217
                .collect(Collectors.toMap(HandleValue::getIndex, v -> v));
×
218
        // 1)
219
        List<HandleValue> valuesToKeep = oldHandleValues.stream()
×
NEW
220
                .filter(HandleBehavior::isHandleInternalValue)
×
221
                .collect(Collectors.toList());
×
222

223
        // 2) Merge requested record and things we want to keep.
NEW
224
        Map<Integer, HandleValue> recordNew = HandleBehavior.handleValuesFrom(preparedRecord, Optional.of(valuesToKeep))
×
225
                .stream()
×
226
                .collect(Collectors.toMap(HandleValue::getIndex, v -> v));
×
227

228
        try {
229
            // 3)
230
            HandleDiff diff = new HandleDiff(recordOld, recordNew);
×
231
            // 4)
232
            if (diff.added().length > 0) {
×
233
                this.client.addHandleValues(preparedRecord.getPid(), diff.added());
×
234
            }
235
            if (diff.updated().length > 0) {
×
236
                this.client.updateHandleValues(preparedRecord.getPid(), diff.updated());
×
237
            }
238
            if (diff.removed().length > 0) {
×
239
                this.client.deleteHandleValues(preparedRecord.getPid(), diff.removed());
×
240
            }
241
        } catch (HandleException e) {
×
242
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
243
                return false;
×
244
            } else {
245
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
246
            }
247
        } catch (Exception e) {
×
248
            throw new RuntimeException("Implementation error in calculating record difference. PLEASE REPORT!", e);
×
249
        }
×
250
        return true;
×
251
    }
252

253
    @Override
254
    public boolean deletePid(final String pid) throws ExternalServiceException {
255
        try {
256
            this.client.deleteHandle(pid);
×
257
        } catch (HandleException e) {
×
258
            if (e.getCode() == HandleException.HANDLE_DOES_NOT_EXIST) {
×
259
                return false;
×
260
            } else {
261
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
262
            }
263
        }
×
264
        return true;
×
265
    }
266

267
    @Override
268
    public Collection<String> resolveAllPidsOfPrefix() throws ExternalServiceException, InvalidConfigException {
269
        HandleCredentials handleCredentials = this.props.getCredentials();
×
270
        if (handleCredentials == null) {
×
271
            throw new InvalidConfigException("No credentials for handle protocol configured.");
×
272
        }
273

274
        PrivateKey key;
275
        {
276
            byte[] privateKeyBytes;
277
            try {
278
                privateKeyBytes = handleCredentials.getPrivateKeyFileContent();
×
279
            } catch (IOException e) {
×
280
                throw new InvalidConfigException("Could not read private key file content.");
×
281
            }
×
282
            if (privateKeyBytes == null || privateKeyBytes.length == 0) {
×
283
                throw new InvalidConfigException("Private Key is empty!");
×
284
            }
285
            byte[] passphrase = handleCredentials.getPrivateKeyPassphraseAsBytes();
×
286
            byte[] privateKeyDecrypted;
287
            // decrypt the private key using the passphrase/cypher
288
            try {
289
                privateKeyDecrypted = Util.decrypt(privateKeyBytes, passphrase);
×
290
            } catch (Exception e) {
×
291
                throw new InvalidConfigException("Private key decryption failed: " + e.getMessage());
×
292
            }
×
293
            try {
294
                key = Util.getPrivateKeyFromBytes(privateKeyDecrypted, 0);
×
295
            } catch (HandleException | InvalidKeySpecException e) {
×
296
                throw new InvalidConfigException("Private key conversion failed: " + e.getMessage());
×
297
            }
×
298
        }
299

300
        PublicKeyAuthenticationInfo auth = new PublicKeyAuthenticationInfo(
×
301
                Util.encodeString(handleCredentials.getUserHandle()),
×
302
                handleCredentials.getPrivateKeyIndex(),
×
303
                key);
304

305
        HandleResolver resolver = new HandleResolver();
×
306
        SiteInfo site;
307
        {
308
            HandleValue[] prefixValues;
309
            try {
310
                prefixValues = resolver.resolveHandle(handleCredentials.getHandleIdentifierPrefix());
×
311
                site = BatchUtil.getFirstPrimarySiteFromHserv(prefixValues, resolver);
×
312
            } catch (HandleException e) {
×
313
                throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
314
            }
×
315
        }
316

317
        String prefix = handleCredentials.getHandleIdentifierPrefix();
×
318
        try {
319
            return BatchUtil.listHandles(prefix, site, resolver, auth);
×
320
        } catch (HandleException e) {
×
321
            throw new ExternalServiceException(SERVICE_NAME_HANDLE, e);
×
322
        }
323
    }
324

325
    /**
326
     * Returns true if the PID is valid according to the following criteria:
327
     * - PID is valid according to isIdentifierRegistered
328
     * - If a generator prefix is set, the PID is expedted to have this prefix.
329
     * 
330
     * @param pid the identifier / PID to check.
331
     * @return true if PID is registered (and if has the generatorPrefix, if it
332
     *         exists).
333
     */
334
    protected boolean isValidPID(final String pid) {
335
        boolean isAuthMode = this.props.getCredentials() != null;
×
336
        if (isAuthMode && !pid.startsWith(this.props.getCredentials().getHandleIdentifierPrefix())) {
×
337
            return false;
×
338
        }
339
        try {
340
            if (!this.isPidRegistered(pid)) {
×
341
                return false;
×
342
            }
343
        } catch (ExternalServiceException e) {
×
344
            return false;
×
345
        }
×
346
        return true;
×
347
    }
348
}
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