001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.persistence.common; 007 008import static java.lang.String.format; 009import static org.apache.commons.codec.binary.Hex.encodeHexString; 010import static org.apache.commons.lang3.StringUtils.substringAfterLast; 011import static org.fcrepo.kernel.api.utils.ContentDigest.getAlgorithm; 012 013import java.io.IOException; 014import java.io.InputStream; 015import java.net.URI; 016import java.security.DigestInputStream; 017import java.security.MessageDigest; 018import java.security.NoSuchAlgorithmException; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.stream.Collectors; 025 026import org.fcrepo.kernel.api.exception.InvalidChecksumException; 027import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 028import org.fcrepo.kernel.api.exception.UnsupportedAlgorithmException; 029import org.fcrepo.kernel.api.utils.ContentDigest; 030import org.fcrepo.config.DigestAlgorithm; 031 032/** 033 * Wrapper for an InputStream that allows for the computation and evaluation 034 * of multiple digests at once 035 * 036 * @author bbpennel 037 */ 038public class MultiDigestInputStreamWrapper { 039 040 private static final int BUFFER_SIZE = 8192; 041 042 private final InputStream sourceStream; 043 044 private final Map<String, String> algToDigest; 045 046 private final Map<String, DigestInputStream> algToDigestStream; 047 048 private boolean streamRetrieved; 049 050 private Map<String, String> computedDigests; 051 052 /** 053 * Construct a MultiDigestInputStreamWrapper 054 * 055 * @param sourceStream the original source input stream 056 * @param digests collection of digests for the input stream 057 * @param wantDigests list of additional digest algorithms to compute for the input stream 058 */ 059 public MultiDigestInputStreamWrapper(final InputStream sourceStream, final Collection<URI> digests, 060 final Collection<DigestAlgorithm> wantDigests) { 061 this.sourceStream = sourceStream; 062 algToDigest = new HashMap<>(); 063 algToDigestStream = new HashMap<>(); 064 065 if (digests != null) { 066 for (final URI digestUri : digests) { 067 final String algorithm = getAlgorithm(digestUri); 068 final String hash = substringAfterLast(digestUri.toString(), ":"); 069 algToDigest.put(algorithm, hash); 070 } 071 } 072 073 // Merge the list of wanted digest algorithms with set of provided digests 074 if (wantDigests != null) { 075 for (final DigestAlgorithm wantDigest : wantDigests) { 076 if (!algToDigest.containsKey(wantDigest.getAlgorithm())) { 077 algToDigest.put(wantDigest.getAlgorithm(), null); 078 } 079 } 080 } 081 } 082 083 /** 084 * Get the InputStream wrapped to produce the requested digests 085 * 086 * @return wrapped input stream 087 */ 088 public InputStream getInputStream() { 089 streamRetrieved = true; 090 InputStream digestStream = sourceStream; 091 for (final String algorithm : algToDigest.keySet()) { 092 try { 093 // Progressively wrap the original stream in layers of digest streams 094 digestStream = new DigestInputStream( 095 digestStream, MessageDigest.getInstance(algorithm)); 096 } catch (final NoSuchAlgorithmException e) { 097 throw new UnsupportedAlgorithmException("Unsupported digest algorithm: " + algorithm, e); 098 } 099 100 algToDigestStream.put(algorithm, (DigestInputStream) digestStream); 101 } 102 return digestStream; 103 } 104 105 /** 106 * After consuming the inputstream, verify that all of the computed digests 107 * matched the provided digests. 108 * 109 * Note: the wrapped InputStream will be consumed if it has not already been read. 110 * 111 * @throws InvalidChecksumException thrown if any of the digests did not match 112 */ 113 public void checkFixity() throws InvalidChecksumException { 114 calculateDigests(); 115 116 algToDigest.forEach((algorithm, originalDigest) -> { 117 // Skip any algorithms which were calculated but no digest was provided for verification 118 if (originalDigest == null) { 119 return; 120 } 121 final String computed = computedDigests.get(algorithm); 122 123 if (!originalDigest.equalsIgnoreCase(computed)) { 124 throw new InvalidChecksumException(format( 125 "Checksum mismatch, computed %s digest %s did not match expected value %s", 126 algorithm, computed, originalDigest)); 127 } 128 }); 129 130 } 131 132 /** 133 * Returns the list of digests calculated for the wrapped InputStream 134 * 135 * Note: the wrapped InputStream will be consumed if it has not already been read. 136 * 137 * @return list of digests calculated from the wrapped InputStream, in URN format. 138 */ 139 public List<URI> getDigests() { 140 calculateDigests(); 141 142 return computedDigests.entrySet().stream() 143 .map(e -> ContentDigest.asURI(e.getKey(), e.getValue())) 144 .collect(Collectors.toList()); 145 } 146 147 /** 148 * Get the digest calculated for the provided algorithm 149 * 150 * @param alg algorithm of the digest to retrieve 151 * @return the calculated digest, or null if no digest of that type was calculated 152 */ 153 public String getDigest(final DigestAlgorithm alg) { 154 calculateDigests(); 155 156 return computedDigests.entrySet().stream() 157 .filter(entry -> alg.getAlgorithm().equals(entry.getKey())) 158 .map(Entry::getValue) 159 .findFirst() 160 .orElse(null); 161 } 162 163 private void calculateDigests() { 164 if (computedDigests != null) { 165 return; 166 } 167 168 if (!streamRetrieved) { 169 // Stream not previously consumed, consume it now in order to calculate digests 170 final var buffer = new byte[BUFFER_SIZE]; 171 try (final InputStream is = getInputStream()) { 172 while (is.read(buffer) != -1) { 173 } 174 } catch (final IOException e) { 175 throw new RepositoryRuntimeException("Failed to read content stream while calculating digests", e); 176 } 177 } 178 179 computedDigests = new HashMap<>(); 180 algToDigestStream.forEach((algorithm, digestStream) -> { 181 final String computed = encodeHexString(digestStream.getMessageDigest().digest()); 182 computedDigests.put(algorithm, computed); 183 }); 184 } 185}