001/*
002 * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume, jcarsique
018 */
019
020package org.nuxeo.ecm.core.blob.binary;
021
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.security.MessageDigest;
028import java.security.NoSuchAlgorithmException;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.Map;
033import java.util.regex.Pattern;
034
035import org.apache.commons.codec.binary.Hex;
036import org.apache.commons.codec.digest.DigestUtils;
037import org.nuxeo.common.xmap.XMap;
038import org.nuxeo.ecm.core.api.Blob;
039
040/**
041 * Abstract BinaryManager implementation that provides a few utilities
042 *
043 * @author Florent Guillaume
044 */
045public abstract class AbstractBinaryManager implements BinaryManager {
046
047    public static final String MD5_DIGEST = "MD5";
048
049    /** @deprecated since 11.1, unused */
050    @Deprecated
051    public static final String SHA1_DIGEST = "SHA-1";
052
053    /** @deprecated since 11.1, unused */
054    @Deprecated
055    public static final String SHA256_DIGEST = "SHA-256";
056
057    /** @deprecated since 11.1, unused */
058    @Deprecated
059    public static final int MD5_DIGEST_LENGTH = 32;
060
061    /** @deprecated since 11.1, unused */
062    @Deprecated
063    public static final int SHA1_DIGEST_LENGTH = 40;
064
065    /** @deprecated since 11.1, unused */
066    @Deprecated
067    public static final int SHA256_DIGEST_LENGTH = 64;
068
069    /**
070     * @since 7.4
071     */
072    public static final Map<Integer, String> DIGESTS_BY_LENGTH;
073
074    static {
075        Map<Integer, String> map = new HashMap<>();
076        map.put(32, "MD5");
077        map.put(40, "SHA-1");
078        map.put(56, "SHA-224");
079        map.put(64, "SHA-256");
080        map.put(96, "SHA-384");
081        map.put(128, "SHA-512");
082        // TODO new algorithms like SHA3-256 will collide, so this mechanism is brittle
083        DIGESTS_BY_LENGTH = Collections.unmodifiableMap(map);
084    }
085
086    public static final String DEFAULT_DIGEST = MD5_DIGEST; // SHA256_DIGEST
087
088    public static final int DEFAULT_DEPTH = 2;
089
090    public String blobProviderId;
091
092    protected Map<String, String> properties;
093
094    protected BinaryManagerRootDescriptor descriptor;
095
096    protected BinaryGarbageCollector garbageCollector;
097
098    protected Pattern digestPattern;
099
100    @Override
101    public void initialize(String blobProviderId, Map<String, String> properties) throws IOException {
102        this.blobProviderId = blobProviderId;
103        this.properties = properties;
104    }
105
106    protected void setDescriptor(BinaryManagerRootDescriptor descriptor) {
107        this.descriptor = descriptor;
108        computeDigestPattern();
109    }
110
111    /**
112     * Creates a binary value from the given input stream.
113     */
114    // not in the public API of BinaryManager anymore
115    abstract protected Binary getBinary(InputStream in) throws IOException;
116
117    /*
118     * This abstract implementation just opens the stream.
119     */
120    @Override
121    public Binary getBinary(Blob blob) throws IOException {
122        if (blob instanceof BinaryBlob) {
123            Binary binary = ((BinaryBlob) blob).getBinary();
124            if (binary.getBlobProviderId().equals(blobProviderId)) {
125                return binary;
126            }
127            // don't reuse the binary if it comes from another blob provider
128        }
129        try (InputStream stream = blob.getStream()) {
130            return getBinary(stream);
131        }
132    }
133
134    @Override
135    abstract public Binary getBinary(String digest);
136
137    @Override
138    public void removeBinaries(Collection<String> digests) {
139        throw new UnsupportedOperationException();
140    }
141
142    /**
143     * Gets existing descriptor or creates a default one.
144     */
145    protected BinaryManagerRootDescriptor getDescriptor(File configFile) throws IOException {
146        BinaryManagerRootDescriptor desc;
147        if (configFile.exists()) {
148            XMap xmap = new XMap();
149            xmap.register(BinaryManagerRootDescriptor.class);
150            try (InputStream in = new FileInputStream(configFile)) {
151                desc = (BinaryManagerRootDescriptor) xmap.load(in);
152            }
153        } else {
154            desc = new BinaryManagerRootDescriptor();
155            // TODO fetch from repo descriptor
156            desc.digest = getDefaultDigestAlgorithm();
157            desc.depth = DEFAULT_DEPTH;
158            desc.write(configFile); // may throw IOException
159        }
160        return desc;
161    }
162
163    public static final int MIN_BUF_SIZE = 8 * 1024; // 8 kB
164
165    public static final int MAX_BUF_SIZE = 64 * 1024; // 64 kB
166
167    protected String storeAndDigest(InputStream in, OutputStream out) throws IOException {
168        MessageDigest digest;
169        try {
170            digest = MessageDigest.getInstance(getDigestAlgorithm());
171        } catch (NoSuchAlgorithmException e) {
172            throw (IOException) new IOException().initCause(e);
173        }
174
175        int size = in.available();
176        if (size == 0) {
177            size = MAX_BUF_SIZE;
178        } else if (size < MIN_BUF_SIZE) {
179            size = MIN_BUF_SIZE;
180        } else if (size > MAX_BUF_SIZE) {
181            size = MAX_BUF_SIZE;
182        }
183        byte[] buf = new byte[size];
184
185        /*
186         * Copy and digest.
187         */
188        int n;
189        while ((n = in.read(buf)) != -1) {
190            digest.update(buf, 0, n);
191            out.write(buf, 0, n);
192        }
193        out.flush();
194
195        return Hex.encodeHexString(digest.digest());
196    }
197
198    /** @deprecated since 11.1, use {@link Hex#encodeHexString} directly */
199    @Deprecated
200    public static String toHexString(byte[] data) {
201        return Hex.encodeHexString(data);
202    }
203
204    protected void computeDigestPattern() {
205        // compute a dummy digest (from 0-length input) to know its length and derive a regexp
206        int len = new DigestUtils(getDigestAlgorithm()).digestAsHex(new byte[0]).length();
207        digestPattern = Pattern.compile("[0-9a-f]{" + len + "}");
208    }
209
210    public boolean isValidDigest(String digest) {
211        return digestPattern.matcher(digest).matches();
212    }
213
214    @Override
215    public BinaryGarbageCollector getGarbageCollector() {
216        return garbageCollector;
217    }
218
219    @Override
220    public String getDigestAlgorithm() {
221        return descriptor.digest;
222    }
223
224    /**
225     * Gets the default message digest to use to hash binaries.
226     *
227     * @since 6.0
228     */
229    protected String getDefaultDigestAlgorithm() {
230        return DEFAULT_DIGEST;
231    }
232
233}