001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.api.utils;
019
020import static org.apache.commons.codec.binary.Hex.encodeHexString;
021import static org.fcrepo.kernel.api.utils.ContentDigest.DIGEST_ALGORITHM.SHA1;
022import static org.slf4j.LoggerFactory.getLogger;
023
024import java.net.URI;
025import java.net.URISyntaxException;
026import java.util.Arrays;
027import java.util.Set;
028import java.util.stream.Collectors;
029
030import org.apache.commons.lang3.ArrayUtils;
031import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
032import org.slf4j.Logger;
033
034/**
035 * Digest helpers to convert digests (checksums) into URI strings
036 * (based loosely on Magnet URIs)
037 * @author Chris Beer
038 * @since Mar 6, 2013
039 */
040public final class ContentDigest {
041
042    private static final Logger LOGGER = getLogger(ContentDigest.class);
043
044    private final static String DEFAULT_DIGEST_ALGORITHM_PROPERTY = "fcrepo.persistence.defaultDigestAlgorithm";
045    public final static DIGEST_ALGORITHM DEFAULT_DIGEST_ALGORITHM;
046
047    static {
048        // Establish the default digest algorithm for fedora, defaulting to SHA512
049        DEFAULT_DIGEST_ALGORITHM = DIGEST_ALGORITHM.fromAlgorithm(
050                System.getProperty(DEFAULT_DIGEST_ALGORITHM_PROPERTY, DIGEST_ALGORITHM.SHA512.algorithm));
051
052        // Throw error if the configured default digest is not known to fedora
053        if (DIGEST_ALGORITHM.MISSING.equals(DEFAULT_DIGEST_ALGORITHM)) {
054            throw new IllegalArgumentException("Invalid " + DEFAULT_DIGEST_ALGORITHM_PROPERTY
055                    + " property configured: " + System.getProperty(DEFAULT_DIGEST_ALGORITHM_PROPERTY));
056        }
057    }
058
059    public enum DIGEST_ALGORITHM {
060        SHA1("SHA", "urn:sha1", "sha-1", "sha1"),
061        SHA256("SHA-256", "urn:sha-256", "sha256"),
062        SHA512("SHA-512", "urn:sha-512", "sha512"),
063        SHA512256("SHA-512/256", "urn:sha-512/256", "sha512/256"),
064        MD5("MD5", "urn:md5"),
065        MISSING("NONE", "missing");
066
067        final public String algorithm;
068        final private String scheme;
069        final private Set<String> aliases;
070
071        DIGEST_ALGORITHM(final String alg, final String scheme, final String... aliases) {
072            this.algorithm = alg;
073            this.scheme = scheme;
074            this.aliases = Arrays.stream(ArrayUtils.add(aliases, algorithm))
075                    .map(String::toLowerCase)
076                    .collect(Collectors.toSet());
077        }
078
079        /**
080         * Return the scheme associated with the provided algorithm (e.g. SHA-1 returns urn:sha1)
081         *
082         * @param alg for which scheme is requested
083         * @return scheme
084         */
085        public static String getScheme(final String alg) {
086            return Arrays.stream(values()).filter(value ->
087                    value.algorithm.equalsIgnoreCase(alg) || value.algorithm.replace("-", "").equalsIgnoreCase(alg)
088            ).findFirst().orElse(MISSING).scheme;
089        }
090
091        /**
092         * Return enum value for the provided scheme (e.g. urn:sha1 returns SHA-1)
093         *
094         * @param argScheme for which enum is requested
095         * @return enum value associated with the arg scheme
096         */
097        public static DIGEST_ALGORITHM fromScheme(final String argScheme) {
098            return Arrays.stream(values()).filter(value -> value.scheme.equalsIgnoreCase(argScheme)
099            ).findFirst().orElse(MISSING);
100        }
101
102        /**
103         * Return enum value for the provided algorithm
104         *
105         * @param alg algorithm name to seek
106         * @return enum value associated with the algorithm name, or missing if not found
107         */
108        public static DIGEST_ALGORITHM fromAlgorithm(final String alg) {
109            final String seek = alg.toLowerCase();
110            return Arrays.stream(values())
111                    .filter(value -> value.aliases.contains(seek))
112                    .findFirst()
113                    .orElse(MISSING);
114        }
115
116        /**
117         * Return true if the provided algorithm is included in this enum
118         *
119         * @param alg to test
120         * @return true if arg algorithm is supported
121         */
122        public static boolean isSupportedAlgorithm(final String alg) {
123            return !getScheme(alg).equals(MISSING.scheme);
124        }
125
126        /**
127         * @return the aliases
128         */
129        public Set<String> getAliases() {
130            return aliases;
131        }
132    }
133
134    public static final String DEFAULT_ALGORITHM = DIGEST_ALGORITHM.SHA1.algorithm;
135
136    private ContentDigest() {
137    }
138
139    /**
140     * Convert a MessageDigest algorithm and checksum value to a URN
141     * @param algorithm the message digest algorithm
142     * @param value the checksum value
143     * @return URI
144     */
145    public static URI asURI(final String algorithm, final String value) {
146        try {
147            final String scheme = DIGEST_ALGORITHM.getScheme(algorithm);
148
149            return new URI(scheme, value, null);
150        } catch (final URISyntaxException unlikelyException) {
151            LOGGER.warn("Exception creating checksum URI: alg={}; value={}",
152                               algorithm, value);
153            throw new RepositoryRuntimeException(unlikelyException.getMessage(), unlikelyException);
154        }
155    }
156
157    /**
158     * Convert a MessageDigest algorithm and checksum byte-array data to a URN
159     * @param algorithm the message digest algorithm
160     * @param data the checksum byte-array data
161     * @return URI
162     */
163    public static URI asURI(final String algorithm, final byte[] data) {
164        return asURI(algorithm, asString(data));
165    }
166
167    /**
168     * Given a digest URI, get the corresponding MessageDigest algorithm
169     * @param digestUri the digest uri
170     * @return MessageDigest algorithm
171     */
172    public static String getAlgorithm(final URI digestUri) {
173        if (digestUri == null) {
174            return DEFAULT_ALGORITHM;
175        }
176        return DIGEST_ALGORITHM.fromScheme(digestUri.getScheme() + ":" +
177             digestUri.getSchemeSpecificPart().split(":", 2)[0]).algorithm;
178    }
179
180    private static String asString(final byte[] data) {
181        return encodeHexString(data);
182    }
183
184    /**
185     * Placeholder checksum value.
186     * @return URI
187     */
188    public static URI missingChecksum() {
189        return asURI(SHA1.algorithm, SHA1.scheme);
190    }
191
192}