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 final InputStream sourceStream; 041 042 private final Map<String, String> algToDigest; 043 044 private final Map<String, DigestInputStream> algToDigestStream; 045 046 private boolean streamRetrieved; 047 048 private Map<String, String> computedDigests; 049 050 /** 051 * Construct a MultiDigestInputStreamWrapper 052 * 053 * @param sourceStream the original source input stream 054 * @param digests collection of digests for the input stream 055 * @param wantDigests list of additional digest algorithms to compute for the input stream 056 */ 057 public MultiDigestInputStreamWrapper(final InputStream sourceStream, final Collection<URI> digests, 058 final Collection<DigestAlgorithm> wantDigests) { 059 this.sourceStream = sourceStream; 060 algToDigest = new HashMap<>(); 061 algToDigestStream = new HashMap<>(); 062 063 if (digests != null) { 064 for (final URI digestUri : digests) { 065 final String algorithm = getAlgorithm(digestUri); 066 final String hash = substringAfterLast(digestUri.toString(), ":"); 067 algToDigest.put(algorithm, hash); 068 } 069 } 070 071 // Merge the list of wanted digest algorithms with set of provided digests 072 if (wantDigests != null) { 073 for (final DigestAlgorithm wantDigest : wantDigests) { 074 if (!algToDigest.containsKey(wantDigest.getAlgorithm())) { 075 algToDigest.put(wantDigest.getAlgorithm(), null); 076 } 077 } 078 } 079 } 080 081 /** 082 * Get the InputStream wrapped to produce the requested digests 083 * 084 * @return wrapped input stream 085 */ 086 public InputStream getInputStream() { 087 streamRetrieved = true; 088 InputStream digestStream = sourceStream; 089 for (final String algorithm : algToDigest.keySet()) { 090 try { 091 // Progressively wrap the original stream in layers of digest streams 092 digestStream = new DigestInputStream( 093 digestStream, MessageDigest.getInstance(algorithm)); 094 } catch (final NoSuchAlgorithmException e) { 095 throw new UnsupportedAlgorithmException("Unsupported digest algorithm: " + algorithm, e); 096 } 097 098 algToDigestStream.put(algorithm, (DigestInputStream) digestStream); 099 } 100 return digestStream; 101 } 102 103 /** 104 * After consuming the inputstream, verify that all of the computed digests 105 * matched the provided digests. 106 * 107 * Note: the wrapped InputStream will be consumed if it has not already been read. 108 * 109 * @throws InvalidChecksumException thrown if any of the digests did not match 110 */ 111 public void checkFixity() throws InvalidChecksumException { 112 calculateDigests(); 113 114 algToDigest.forEach((algorithm, originalDigest) -> { 115 // Skip any algorithms which were calculated but no digest was provided for verification 116 if (originalDigest == null) { 117 return; 118 } 119 final String computed = computedDigests.get(algorithm); 120 121 if (!originalDigest.equalsIgnoreCase(computed)) { 122 throw new InvalidChecksumException(format( 123 "Checksum mismatch, computed %s digest %s did not match expected value %s", 124 algorithm, computed, originalDigest)); 125 } 126 }); 127 128 } 129 130 /** 131 * Returns the list of digests calculated for the wrapped InputStream 132 * 133 * Note: the wrapped InputStream will be consumed if it has not already been read. 134 * 135 * @return list of digests calculated from the wrapped InputStream, in URN format. 136 */ 137 public List<URI> getDigests() { 138 calculateDigests(); 139 140 return computedDigests.entrySet().stream() 141 .map(e -> ContentDigest.asURI(e.getKey(), e.getValue())) 142 .collect(Collectors.toList()); 143 } 144 145 /** 146 * Get the digest calculated for the provided algorithm 147 * 148 * @param alg algorithm of the digest to retrieve 149 * @return the calculated digest, or null if no digest of that type was calculated 150 */ 151 public String getDigest(final DigestAlgorithm alg) { 152 calculateDigests(); 153 154 return computedDigests.entrySet().stream() 155 .filter(entry -> alg.getAlgorithm().equals(entry.getKey())) 156 .map(Entry::getValue) 157 .findFirst() 158 .orElse(null); 159 } 160 161 private void calculateDigests() { 162 if (computedDigests != null) { 163 return; 164 } 165 166 if (!streamRetrieved) { 167 // Stream not previously consumed, consume it now in order to calculate digests 168 try (final InputStream is = getInputStream()) { 169 while (is.read() != -1) { 170 } 171 } catch (final IOException e) { 172 throw new RepositoryRuntimeException("Failed to read content stream while calculating digests", e); 173 } 174 } 175 176 computedDigests = new HashMap<>(); 177 algToDigestStream.forEach((algorithm, digestStream) -> { 178 final String computed = encodeHexString(digestStream.getMessageDigest().digest()); 179 computedDigests.put(algorithm, computed); 180 }); 181 } 182}