001/*
002 * (C) Copyright 2019 Nuxeo (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
018 */
019package org.nuxeo.ecm.core.blob;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.util.concurrent.ThreadLocalRandom;
027
028import org.apache.commons.io.IOUtils;
029import org.apache.commons.lang3.mutable.MutableObject;
030import org.apache.logging.log4j.LogManager;
031import org.apache.logging.log4j.Logger;
032import org.nuxeo.ecm.core.api.NuxeoException;
033import org.nuxeo.ecm.core.blob.KeyStrategy.WriteObserver;
034
035/**
036 * Basic helper implementations for a {@link BlobStore}.
037 *
038 * @since 11.1
039 */
040public abstract class AbstractBlobStore implements BlobStore {
041
042    /**
043     * Separator between key and byte range (start/end, specified at end of key).
044     * <p>
045     * Used when the blob provider is configured to allow byte ranges.
046     */
047    public static final char BYTE_RANGE_SEP = ';';
048
049    private static final Logger log = LogManager.getLogger(AbstractBlobStore.class);
050
051    protected final String name;
052
053    protected final KeyStrategy keyStrategy;
054
055    public AbstractBlobStore(String name, KeyStrategy keyStrategy) {
056        this.name = name;
057        this.keyStrategy = keyStrategy;
058    }
059
060    @Override
061    public String getName() {
062        return name;
063    }
064
065    protected void logTrace(String arrow, String message) {
066        logTrace(null, arrow, null, message);
067    }
068
069    protected void logTrace(String source, String arrow, String dest, String message) {
070        if (source == null) {
071            source = "Nuxeo";
072        }
073        if (dest == null) {
074            dest = name;
075        }
076        logTrace(source + " " + arrow + " " + dest + ": " + message);
077    }
078
079    protected void logTrace(String message) {
080        log.trace(message);
081    }
082
083    @Override
084    public boolean hasVersioning() {
085        return false;
086    }
087
088    @Override
089    public KeyStrategy getKeyStrategy() {
090        return keyStrategy;
091    }
092
093    @Override
094    public BlobStore unwrap() {
095        return this;
096    }
097
098    @Override
099    public String writeBlob(BlobContext blobContext) throws IOException {
100        BlobWriteContext blobWriteContext = keyStrategy.getBlobWriteContext(blobContext);
101        return writeBlob(blobWriteContext);
102    }
103
104    @Override
105    public void writeBlobProperties(BlobUpdateContext blobUpdateContext) throws IOException {
106        // ignore properties updates by default
107    }
108
109    @Override
110    public void deleteBlob(BlobContext blobContext) {
111        BlobWriteContext blobWriteContext = keyStrategy.getBlobWriteContext(blobContext);
112        String key = blobWriteContext.getKey();
113        if (key == null) {
114            throw new NuxeoException("Cannot delete blob with " + getClass().getName());
115        }
116        deleteBlob(key);
117    }
118
119    @Override
120    public boolean copyBlobIsOptimized(BlobStore sourceStore) {
121        BlobStore unwrapped = unwrap();
122        if (unwrapped == this) {
123            throw new UnsupportedOperationException(
124                    "Class " + getClass().getName() + " must implement copyBlobIsOptimized");
125        }
126        return unwrapped.copyBlobIsOptimized(sourceStore.unwrap());
127    }
128
129    protected String stripBlobKeyPrefix(String key) {
130        int colon = key.indexOf(':');
131        if (colon >= 0) {
132            key = key.substring(colon + 1);
133        }
134        return key;
135    }
136
137    public static String setByteRangeInKey(String key, ByteRange byteRange) {
138        return key + String.valueOf(BYTE_RANGE_SEP) + byteRange.getStart() + String.valueOf(BYTE_RANGE_SEP)
139                + byteRange.getEnd();
140    }
141
142    public static ByteRange getByteRangeFromKey(MutableObject<String> keyHolder) {
143        String key = keyHolder.getValue();
144        int j = key.lastIndexOf(BYTE_RANGE_SEP);
145        int i = key.lastIndexOf(BYTE_RANGE_SEP, j - 1);
146        if (j > 0) {
147            try {
148                long start = Long.parseLong(key.substring(i + 1, j));
149                long end = Long.parseLong(key.substring(j + 1));
150                keyHolder.setValue(key.substring(0, i));
151                return ByteRange.inclusive(start, end);
152            } catch (NumberFormatException e) {
153                log.debug("Cannot parse byte range in key: {}", key, e);
154            }
155        }
156        return null;
157    }
158
159    /** Returns a random string suitable as a key. */
160    protected String randomString() {
161        return String.valueOf(randomLong());
162    }
163
164    /** Returns a random positive long. */
165    protected long randomLong() {
166        long value;
167        do {
168            value = ThreadLocalRandom.current().nextLong();
169        } while (value == Long.MIN_VALUE);
170        if (value < 0) {
171            value = -value;
172        }
173        return value;
174    }
175
176    /**
177     * Transfers a blob to a file, notifying an observer while doing this.
178     *
179     * @param blobWriteContext the blob write context, to get the blob stream and write observer
180     * @param dest the destination file
181     */
182    public void transfer(BlobWriteContext blobWriteContext, Path dest) throws IOException {
183        // no need for BufferedOutputStream as we write a buffer already
184        try (OutputStream out = Files.newOutputStream(dest)) {
185            transfer(blobWriteContext, out);
186        }
187    }
188
189    /**
190     * Transfers a blob to an output stream, notifying an observer while doing this.
191     *
192     * @param blobWriteContext the blob write context, to get the blob stream and write observer
193     * @param out the output stream
194     */
195    public void transfer(BlobWriteContext blobWriteContext, OutputStream out) throws IOException {
196        try (InputStream in = blobWriteContext.getStream()) {
197            transfer(in, out, blobWriteContext.writeObserver);
198        }
199    }
200
201    /**
202     * Copies bytes from an input stream to an output stream, notifying an observer while doing this.
203     *
204     * @param in the input stream
205     * @param out the output stream
206     * @param writeObserver the write observer
207     */
208    @SuppressWarnings("resource")
209    public void transfer(InputStream in, OutputStream out, WriteObserver writeObserver) throws IOException {
210        if (writeObserver != null) {
211            out = writeObserver.wrap(out);
212        }
213        IOUtils.copy(in, out);
214        if (writeObserver != null) {
215            writeObserver.done();
216        }
217    }
218
219}