001/*
002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Florent Guillaume, jcarsique
011 */
012
013package org.nuxeo.ecm.core.blob.binary;
014
015import java.io.File;
016import java.io.FileInputStream;
017import java.io.IOException;
018import java.io.InputStream;
019import java.io.OutputStream;
020import java.security.MessageDigest;
021import java.security.NoSuchAlgorithmException;
022import java.util.Collection;
023import java.util.HashMap;
024import java.util.Map;
025
026import org.nuxeo.common.xmap.XMap;
027import org.nuxeo.ecm.core.api.Blob;
028
029/**
030 * Abstract BinaryManager implementation that provides a few utilities
031 *
032 * @author Florent Guillaume
033 */
034public abstract class AbstractBinaryManager implements BinaryManager {
035
036    public static final String MD5_DIGEST = "MD5";
037
038    public static final String SHA1_DIGEST = "SHA-1";
039
040    public static final String SHA256_DIGEST = "SHA-256";
041
042    public static final int MD5_DIGEST_LENGTH = 32;
043
044    public static final int SHA1_DIGEST_LENGTH = 40;
045
046    public static final int SHA256_DIGEST_LENGTH = 64;
047
048    /**
049     * @since 7.4
050     */
051    public static final HashMap<Integer, String> DIGESTS_BY_LENGTH = new HashMap<>();
052
053    public static final String DEFAULT_DIGEST = MD5_DIGEST; // SHA256_DIGEST
054
055    public static final int DEFAULT_DEPTH = 2;
056
057    protected String blobProviderId;
058
059    protected BinaryManagerRootDescriptor descriptor;
060
061    protected BinaryGarbageCollector garbageCollector;
062
063    @Override
064    public void initialize(String blobProviderId, Map<String, String> properties) throws IOException {
065        this.blobProviderId = blobProviderId;
066        DIGESTS_BY_LENGTH.put(MD5_DIGEST_LENGTH, MD5_DIGEST);
067        DIGESTS_BY_LENGTH.put(SHA1_DIGEST_LENGTH, SHA1_DIGEST);
068        DIGESTS_BY_LENGTH.put(SHA256_DIGEST_LENGTH, SHA256_DIGEST);
069    }
070
071    /**
072     * Creates a binary value from the given input stream.
073     */
074    // not in the public API of BinaryManager anymore
075    abstract protected Binary getBinary(InputStream in) throws IOException;
076
077    /*
078     * This abstract implementation just opens the stream.
079     */
080    @Override
081    public Binary getBinary(Blob blob) throws IOException {
082        if (blob instanceof BinaryBlob) {
083            Binary binary = ((BinaryBlob) blob).getBinary();
084            if (binary.getBlobProviderId().equals(blobProviderId)) {
085                return binary;
086            }
087            // don't reuse the binary if it comes from another blob provider
088        }
089        try (InputStream stream = blob.getStream()) {
090            return getBinary(stream);
091        }
092    }
093
094    @Override
095    abstract public Binary getBinary(String digest);
096
097    @Override
098    public void removeBinaries(Collection<String> digests) {
099        throw new UnsupportedOperationException();
100    }
101
102    /**
103     * Gets existing descriptor or creates a default one.
104     */
105    protected BinaryManagerRootDescriptor getDescriptor(File configFile) throws IOException {
106        BinaryManagerRootDescriptor desc;
107        if (configFile.exists()) {
108            XMap xmap = new XMap();
109            xmap.register(BinaryManagerRootDescriptor.class);
110            desc = (BinaryManagerRootDescriptor) xmap.load(new FileInputStream(configFile));
111        } else {
112            desc = new BinaryManagerRootDescriptor();
113            // TODO fetch from repo descriptor
114            desc.digest = getDefaultDigestAlgorithm();
115            desc.depth = DEFAULT_DEPTH;
116            desc.write(configFile); // may throw IOException
117        }
118        return desc;
119    }
120
121    public static final int MIN_BUF_SIZE = 8 * 1024; // 8 kB
122
123    public static final int MAX_BUF_SIZE = 64 * 1024; // 64 kB
124
125    protected String storeAndDigest(InputStream in, OutputStream out) throws IOException {
126        MessageDigest digest;
127        try {
128            digest = MessageDigest.getInstance(getDigestAlgorithm());
129        } catch (NoSuchAlgorithmException e) {
130            throw (IOException) new IOException().initCause(e);
131        }
132
133        int size = in.available();
134        if (size == 0) {
135            size = MAX_BUF_SIZE;
136        } else if (size < MIN_BUF_SIZE) {
137            size = MIN_BUF_SIZE;
138        } else if (size > MAX_BUF_SIZE) {
139            size = MAX_BUF_SIZE;
140        }
141        byte[] buf = new byte[size];
142
143        /*
144         * Copy and digest.
145         */
146        int n;
147        while ((n = in.read(buf)) != -1) {
148            digest.update(buf, 0, n);
149            out.write(buf, 0, n);
150        }
151        out.flush();
152
153        return toHexString(digest.digest());
154    }
155
156    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
157
158    public static String toHexString(byte[] data) {
159        StringBuilder buf = new StringBuilder(2 * data.length);
160        for (byte b : data) {
161            buf.append(HEX_DIGITS[(0xF0 & b) >> 4]);
162            buf.append(HEX_DIGITS[0x0F & b]);
163        }
164        return buf.toString();
165    }
166
167    @Override
168    public BinaryGarbageCollector getGarbageCollector() {
169        return garbageCollector;
170    }
171
172    @Override
173    public String getDigestAlgorithm() {
174        return descriptor.digest;
175    }
176
177    /**
178     * Gets the default message digest to use to hash binaries.
179     *
180     * @since 6.0
181     */
182    protected String getDefaultDigestAlgorithm() {
183        return DEFAULT_DIGEST;
184    }
185
186}