Skip to content

Commit a8ca8f1

Browse files
authored
Merge pull request #1341 from digital-preservation/add-checksum-option-to-droid-api
Add checksum option to DROID API
2 parents 1d0fcfa + f38cb2e commit a8ca8f1

7 files changed

Lines changed: 344 additions & 171 deletions

File tree

droid-api/checkstyle/suppressions.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@
6868
<suppress checks="ClassFanOutComplexity|ClassDataAbstractionCoupling"
6969
files=".*ProfileInstanceManagerImpl\.java$"/>
7070

71+
<suppress checks="ClassFanOutComplexity|ClassDataAbstractionCoupling"
72+
files=".*DroidAPI\.java$"/>
73+
74+
<suppress checks="JavadocVariable"
75+
files=".*HashAlgorithm\.java$"/>
76+
7177
<!-- suppress requirement for @return on enum, which conflicts with new XDocLint -->
7278
<suppress checks="JavadocMethodCheck"
7379
files=".*ProfileState\.java$"/>

droid-api/src/main/java/uk/gov/nationalarchives/droid/internal/api/DroidAPI.java

Lines changed: 139 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,41 @@
3131
*/
3232
package uk.gov.nationalarchives.droid.internal.api;
3333

34+
import java.io.*;
3435
import java.net.URI;
3536
import java.net.URISyntaxException;
3637
import java.net.http.HttpClient;
38+
import java.net.http.HttpRequest;
39+
import java.net.http.HttpResponse;
3740
import java.nio.file.Files;
38-
import java.io.IOException;
3941
import java.nio.file.Path;
40-
import java.util.ArrayList;
41-
import java.util.Arrays;
42-
import java.util.List;
43-
import java.util.Optional;
44-
import java.util.ResourceBundle;
42+
import java.util.*;
4543
import java.util.concurrent.atomic.AtomicLong;
44+
import java.util.function.BiFunction;
4645
import java.util.stream.Collectors;
46+
import java.util.stream.Stream;
4747

4848
import org.apache.commons.lang3.StringUtils;
4949

5050
import org.apache.http.client.utils.URIBuilder;
51+
import software.amazon.awssdk.core.ResponseInputStream;
5152
import software.amazon.awssdk.core.exception.SdkClientException;
5253
import software.amazon.awssdk.regions.Region;
5354
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
5455
import software.amazon.awssdk.services.s3.S3Client;
5556
import software.amazon.awssdk.services.s3.S3Uri;
5657
import software.amazon.awssdk.services.s3.S3Utilities;
58+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
59+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
5760
import software.amazon.awssdk.services.s3.model.S3Object;
5861
import uk.gov.nationalarchives.droid.core.BinarySignatureIdentifier;
5962
import uk.gov.nationalarchives.droid.core.SignatureParseException;
6063
import uk.gov.nationalarchives.droid.core.interfaces.*;
6164
import uk.gov.nationalarchives.droid.core.interfaces.archive.ContainerIdentifier;
65+
import uk.gov.nationalarchives.droid.core.interfaces.hash.MD5HashGenerator;
66+
import uk.gov.nationalarchives.droid.core.interfaces.hash.SHA1HashGenerator;
67+
import uk.gov.nationalarchives.droid.core.interfaces.hash.SHA256HashGenerator;
68+
import uk.gov.nationalarchives.droid.core.interfaces.hash.SHA512HashGenerator;
6269
import uk.gov.nationalarchives.droid.core.interfaces.resource.*;
6370

6471

@@ -107,8 +114,21 @@ public final class DroidAPI implements AutoCloseable {
107114

108115
private final HttpClient httpClient;
109116

110-
111-
private DroidAPI(DroidCore droidCore, ContainerIdentifier zipIdentifier, ContainerIdentifier ole2Identifier, ContainerIdentifier gzIdentifier, String containerSignatureVersion, String binarySignatureVersion, String droidVersion, S3Client s3Client, HttpClient httpClient, Region s3Region) {
117+
private final List<HashAlgorithm> hashAlgorithms;
118+
119+
private DroidAPI(
120+
DroidCore droidCore,
121+
ContainerIdentifier zipIdentifier,
122+
ContainerIdentifier ole2Identifier,
123+
ContainerIdentifier gzIdentifier,
124+
String containerSignatureVersion,
125+
String binarySignatureVersion,
126+
String droidVersion,
127+
S3Client s3Client,
128+
HttpClient httpClient,
129+
Region s3Region,
130+
List<HashAlgorithm> hashAlgorithms
131+
) {
112132
this.droidCore = droidCore;
113133
this.zipIdentifier = zipIdentifier;
114134
this.ole2Identifier = ole2Identifier;
@@ -119,8 +139,14 @@ private DroidAPI(DroidCore droidCore, ContainerIdentifier zipIdentifier, Contain
119139
this.s3Region = getRegionOrDefault(s3Region);
120140
this.s3Client = getS3ClientOrDefault(s3Client);
121141
this.httpClient = getHttpClientOrDefault(httpClient);
142+
this.hashAlgorithms = hashAlgorithms;
122143
}
123144

145+
public record APIResult(List<APIIdentificationResult> identificationResults, Map<HashAlgorithm, String> hashResults) {}
146+
147+
public record APIIdentificationResult(String extension, IdentificationMethod method, String puid, String name,
148+
boolean fileExtensionMismatch, URI uri) { }
149+
124150
private HttpClient getHttpClientOrDefault(HttpClient httpClient) {
125151
return Optional.ofNullable(httpClient)
126152
.orElse(HttpClient.newHttpClient());
@@ -154,6 +180,7 @@ public static class DroidAPIBuilder {
154180
private S3Client s3Client;
155181
private Region s3Region;
156182
private HttpClient httpClient;
183+
private List<HashAlgorithm> hashAlgorithms = Collections.emptyList();
157184

158185
public DroidAPIBuilder binarySignature(final Path binarySignature) {
159186
this.binarySignature = binarySignature;
@@ -180,6 +207,11 @@ public DroidAPIBuilder httpClient(final HttpClient httpClient) {
180207
return this;
181208
}
182209

210+
public DroidAPIBuilder hashAlgorithms(final List<HashAlgorithm> hashAlgorithms) {
211+
this.hashAlgorithms = hashAlgorithms == null ? Collections.emptyList() : hashAlgorithms;
212+
return this;
213+
}
214+
183215
public DroidAPI build() throws SignatureParseException {
184216
if (this.binarySignature == null || this.containerSignature == null) {
185217
throw new IllegalArgumentException("Container signature and binary signature are mandatory arguments");
@@ -192,7 +224,7 @@ public DroidAPI build() throws SignatureParseException {
192224
String containerVersion = StringUtils.substringAfterLast(containerSignature.getFileName().toString(), "-").split("\\.")[0];
193225
String droidVersion = ResourceBundle.getBundle("options").getString("version_no");
194226
ContainerApi containerApi = new ContainerApi(droidCore, containerSignature);
195-
return new DroidAPI(droidCore, containerApi.zipIdentifier(), containerApi.ole2Identifier(), containerApi.gzIdentifier(), containerVersion, droidCore.getSigFile().getVersion(), droidVersion, this.s3Client, this.httpClient, this.s3Region);
227+
return new DroidAPI(droidCore, containerApi.zipIdentifier(), containerApi.ole2Identifier(), containerApi.gzIdentifier(), containerVersion, droidCore.getSigFile().getVersion(), droidVersion, this.s3Client, this.httpClient, this.s3Region, this.hashAlgorithms);
196228
}
197229
}
198230

@@ -207,7 +239,7 @@ public static DroidAPIBuilder builder() {
207239
* @return File identification result. File can have multiple matching signatures.
208240
* @throws IOException If File can't be read or there is IO error.
209241
*/
210-
public List<ApiResult> submit(final URI uri, String extension) throws IOException {
242+
public List<APIResult> submit(final URI uri, String extension) throws IOException {
211243
if (S3_SCHEME.equals(uri.getScheme())) {
212244
return submitS3Identification(uri, extension);
213245
} else if (List.of("http", "https").contains(uri.getScheme())) {
@@ -224,11 +256,11 @@ public List<ApiResult> submit(final URI uri, String extension) throws IOExceptio
224256
* @return File identification result. File can have multiple matching signatures.
225257
* @throws IOException If File can't be read or there is IO error.
226258
*/
227-
public List<ApiResult> submit(final URI uri) throws IOException {
259+
public List<APIResult> submit(final URI uri) throws IOException {
228260
return submit(uri, null);
229261
}
230262

231-
private List<ApiResult> submitHttpIdentification(final URI uri, String extension) throws IOException {
263+
private List<APIResult> submitHttpIdentification(final URI uri, String extension) throws IOException {
232264
HttpClient httpClient = this.httpClient == null ? HttpClient.newHttpClient() : this.httpClient;
233265
HttpUtils httpUtils = new HttpUtils(httpClient);
234266
HttpUtils.HttpMetadata httpMetadata = httpUtils.getHttpMetadata(uri);
@@ -243,18 +275,72 @@ private List<ApiResult> submitHttpIdentification(final URI uri, String extension
243275

244276
final RequestIdentifier id = getRequestIdentifier(uri);
245277

278+
Map<HashAlgorithm, String> hashResults = generateHashResults(uri, this::getHttpHash);
246279

247280
try (final HttpIdentificationRequest request = new HttpIdentificationRequest(metaData, id, httpClient)) {
248281
request.setExtension(extension);
249282
request.open(uri);
250-
return getApiResults(request);
283+
return List.of(new APIResult(getIdentificationResults(request), hashResults));
251284
}
252285
}
253286

254-
private List<ApiResult> submitS3Identification(final URI uri, String extension) throws IOException {
287+
private <T> Map<HashAlgorithm, String> generateHashResults(T identifier, BiFunction<HashAlgorithm, T, String> hashFunction) {
288+
return hashAlgorithms.stream().collect(Collectors.toMap(
289+
algorithm -> algorithm,
290+
algorithm -> hashFunction.apply(algorithm, identifier)
291+
));
292+
}
293+
294+
private String getFileHash(HashAlgorithm hashAlgorithm, Path path) {
295+
try (InputStream fileInputStream = new FileInputStream(path.toFile())) {
296+
return getHash(hashAlgorithm, fileInputStream);
297+
} catch (IOException e) {
298+
throw new RuntimeException(e);
299+
}
300+
}
301+
302+
private String getS3Hash(HashAlgorithm algorithm, S3Uri s3Uri) {
303+
String key = s3Uri.key().orElseThrow(() -> new RuntimeException("Key not found in uri " + s3Uri.uri()));
304+
String bucket = s3Uri.bucket().orElseThrow(() -> new RuntimeException("Bucket not found in uri " + s3Uri.uri()));
305+
306+
try (ResponseInputStream<GetObjectResponse> responseInputStream = s3Client.getObject(GetObjectRequest.builder().bucket(bucket).key(key).build())) {
307+
return getHash(algorithm, responseInputStream);
308+
} catch (IOException e) {
309+
throw new RuntimeException(e);
310+
}
311+
}
312+
313+
private String getHttpHash(HashAlgorithm algorithm, URI httpUri) {
314+
HttpRequest request = HttpRequest.newBuilder()
315+
.uri(httpUri)
316+
.GET()
317+
.build();
318+
try {
319+
try (InputStream responseStream = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()).body()) {
320+
return getHash(algorithm, responseStream);
321+
}
322+
} catch (IOException | InterruptedException e) {
323+
throw new RuntimeException(e);
324+
}
325+
}
326+
327+
private String getHash(HashAlgorithm algorithm, InputStream inputStream) {
328+
try {
329+
return switch (algorithm) {
330+
case MD5 -> new MD5HashGenerator().hash(inputStream);
331+
case SHA1 -> new SHA1HashGenerator().hash(inputStream);
332+
case SHA256 -> new SHA256HashGenerator().hash(inputStream);
333+
case SHA512 -> new SHA512HashGenerator().hash(inputStream);
334+
};
335+
} catch (IOException e) {
336+
throw new RuntimeException(e);
337+
}
338+
}
339+
340+
private List<APIResult> submitS3Identification(final URI uri, String extension) throws IOException {
255341
S3Utils s3Utils = new S3Utils(s3Client);
256342
S3Utils.S3ObjectList objectList = s3Utils.listObjects(uri);
257-
List<ApiResult> apiResults = new ArrayList<>();
343+
List<APIResult> apiResults = new ArrayList<>();
258344

259345
for (S3Object s3Object: objectList.contents()) {
260346
URIBuilder uriBuilder = new URIBuilder();
@@ -265,12 +351,14 @@ private List<ApiResult> submitS3Identification(final URI uri, String extension)
265351
throw new RuntimeException(e);
266352
}
267353
S3Uri s3Uri = S3Utilities.builder().region(s3Region).build().parseUri(objectUri);
354+
Map<HashAlgorithm, String> hashResults = generateHashResults(s3Uri, this::getS3Hash);
355+
268356
final RequestIdentifier id = getRequestIdentifier(s3Uri.uri());
269357
RequestMetaData metaData = new RequestMetaData(s3Object.size(), s3Object.lastModified().getEpochSecond(), s3Uri.uri().toString());
270358
try (final S3IdentificationRequest request = new S3IdentificationRequest(metaData, id, s3Client)) {
271359
request.setExtension(extension);
272360
request.open(s3Uri);
273-
apiResults.addAll(getApiResults(request));
361+
apiResults.add(new APIResult(getIdentificationResults(request), hashResults));
274362
}
275363
}
276364
return apiResults;
@@ -284,23 +372,45 @@ private static RequestIdentifier getRequestIdentifier(URI uri) {
284372
}
285373

286374

287-
private List<ApiResult> submitFileSystemIdentification(final Path file, String extension) throws IOException {
288-
final RequestMetaData metaData = new RequestMetaData(
289-
Files.size(file),
290-
Files.getLastModifiedTime(file).toMillis(),
291-
file.toAbsolutePath().toString()
292-
);
375+
private List<APIResult> submitFileSystemIdentification(final Path file, String extension) throws IOException {
376+
if (Files.isDirectory(file)) {
377+
try (Stream<Path> files = Files.walk(file)) {
378+
return files.filter(Files::isRegularFile)
379+
.map(eachFile -> getApiResultForFile(extension, eachFile))
380+
.toList();
381+
}
382+
383+
} else {
384+
return List.of(getApiResultForFile(extension, file));
385+
}
386+
}
387+
388+
private APIResult getApiResultForFile(String extension, Path eachFile) {
389+
final RequestMetaData metaData;
390+
try {
391+
metaData = new RequestMetaData(
392+
Files.size(eachFile),
393+
Files.getLastModifiedTime(eachFile).toMillis(),
394+
eachFile.toAbsolutePath().toString()
395+
);
396+
} catch (IOException e) {
397+
throw new RuntimeException(e);
398+
}
399+
400+
final RequestIdentifier id = getRequestIdentifier(eachFile.toAbsolutePath().toUri());
293401

294-
final RequestIdentifier id = getRequestIdentifier(file.toAbsolutePath().toUri());
402+
Map<HashAlgorithm, String> hashResults = generateHashResults(eachFile, this::getFileHash);
295403

296404
try (final FileSystemIdentificationRequest request = new FileSystemIdentificationRequest(metaData, id)) {
297405
request.setExtension(extension);
298-
request.open(file);
299-
return getApiResults(request);
406+
request.open(eachFile);
407+
return new APIResult(getIdentificationResults(request), hashResults);
408+
} catch (IOException e) {
409+
throw new RuntimeException(e);
300410
}
301411
}
302412

303-
private <T> List<ApiResult> getApiResults(IdentificationRequest<T> request) throws IOException {
413+
private <T> List<APIIdentificationResult> getIdentificationResults(IdentificationRequest<T> request) throws IOException {
304414
IdentificationResultCollection resultCollection;
305415
String extension = request.getExtension();
306416

@@ -322,17 +432,17 @@ private <T> List<ApiResult> getApiResults(IdentificationRequest<T> request) thro
322432
boolean fileExtensionMismatch = resultCollection.getExtensionMismatch();
323433

324434
return resultCollection.getResults()
325-
.stream().map(res -> createApiResult(res, extension, fileExtensionMismatch, request.getIdentifier().getUri()))
435+
.stream().map(res -> createIdentificationResult(res, extension, fileExtensionMismatch, request.getIdentifier().getUri()))
326436
.collect(Collectors.toList());
327437
}
328438

329-
private ApiResult createApiResult(IdentificationResult result, String extension, boolean extensionMismatch, URI uri) {
439+
private APIIdentificationResult createIdentificationResult(IdentificationResult result, String extension, boolean extensionMismatch, URI uri) {
330440
String name = result.getName();
331441
if (result.getMethod().equals(IdentificationMethod.CONTAINER)
332442
&& (droidCore.formatNameByPuid(result.getPuid()) != null)) {
333443
name = droidCore.formatNameByPuid(result.getPuid());
334444
}
335-
return new ApiResult(extension, result.getMethod(), result.getPuid(), name, extensionMismatch, uri);
445+
return new APIIdentificationResult(extension, result.getMethod(), result.getPuid(), name, extensionMismatch, uri);
336446
}
337447

338448
private <T> IdentificationResultCollection identifyByExtension(final IdentificationRequest<T> identificationRequest) {

droid-api/src/main/java/uk/gov/nationalarchives/droid/internal/api/ApiResult.java renamed to droid-api/src/main/java/uk/gov/nationalarchives/droid/internal/api/HashAlgorithm.java

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -31,48 +31,9 @@
3131
*/
3232
package uk.gov.nationalarchives.droid.internal.api;
3333

34-
import uk.gov.nationalarchives.droid.core.interfaces.IdentificationMethod;
35-
36-
import java.net.URI;
37-
38-
public class ApiResult {
39-
private final String extension;
40-
private final IdentificationMethod method;
41-
private final String puid;
42-
private final String name;
43-
private final boolean fileExtensionMismatch;
44-
private final URI uri;
45-
46-
public ApiResult(String extension, IdentificationMethod method, String puid, String name, boolean fileExtensionMismatch, URI uri) {
47-
this.extension = extension;
48-
this.method = method;
49-
this.puid = puid;
50-
this.name = name;
51-
this.fileExtensionMismatch = fileExtensionMismatch;
52-
this.uri = uri;
53-
}
54-
55-
public String getName() {
56-
return name;
57-
}
58-
59-
public String getPuid() {
60-
return puid;
61-
}
62-
63-
public IdentificationMethod getMethod() {
64-
return method;
65-
}
66-
67-
public String getExtension() {
68-
return extension;
69-
}
70-
71-
public boolean isFileExtensionMismatch() {
72-
return fileExtensionMismatch;
73-
}
74-
75-
public URI getUri() {
76-
return uri;
77-
}
34+
public enum HashAlgorithm {
35+
MD5,
36+
SHA1,
37+
SHA256,
38+
SHA512
7839
}

0 commit comments

Comments
 (0)