3131 */
3232package uk .gov .nationalarchives .droid .internal .api ;
3333
34+ import java .io .*;
3435import java .net .URI ;
3536import java .net .URISyntaxException ;
3637import java .net .http .HttpClient ;
38+ import java .net .http .HttpRequest ;
39+ import java .net .http .HttpResponse ;
3740import java .nio .file .Files ;
38- import java .io .IOException ;
3941import 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 .*;
4543import java .util .concurrent .atomic .AtomicLong ;
44+ import java .util .function .BiFunction ;
4645import java .util .stream .Collectors ;
46+ import java .util .stream .Stream ;
4747
4848import org .apache .commons .lang3 .StringUtils ;
4949
5050import org .apache .http .client .utils .URIBuilder ;
51+ import software .amazon .awssdk .core .ResponseInputStream ;
5152import software .amazon .awssdk .core .exception .SdkClientException ;
5253import software .amazon .awssdk .regions .Region ;
5354import software .amazon .awssdk .regions .providers .DefaultAwsRegionProviderChain ;
5455import software .amazon .awssdk .services .s3 .S3Client ;
5556import software .amazon .awssdk .services .s3 .S3Uri ;
5657import software .amazon .awssdk .services .s3 .S3Utilities ;
58+ import software .amazon .awssdk .services .s3 .model .GetObjectRequest ;
59+ import software .amazon .awssdk .services .s3 .model .GetObjectResponse ;
5760import software .amazon .awssdk .services .s3 .model .S3Object ;
5861import uk .gov .nationalarchives .droid .core .BinarySignatureIdentifier ;
5962import uk .gov .nationalarchives .droid .core .SignatureParseException ;
6063import uk .gov .nationalarchives .droid .core .interfaces .*;
6164import 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 ;
6269import 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 ) {
0 commit comments