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}