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}