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}