001/* 002 * (C) Copyright 2006-2020 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 * Nuxeo - initial API and implementation 018 * 019 */ 020package org.nuxeo.ecm.core.convert.cache; 021 022import java.io.File; 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Set; 031import java.util.concurrent.atomic.AtomicLong; 032import java.util.concurrent.locks.ReentrantReadWriteLock; 033 034import org.apache.commons.codec.binary.Base64; 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.nuxeo.common.utils.Path; 038import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 039import org.nuxeo.ecm.core.convert.api.ConversionService; 040import org.nuxeo.ecm.core.convert.service.ConversionServiceImpl; 041 042/** 043 * Manager for the cache system of the {@link ConversionService}. 044 * 045 * @author tiry 046 */ 047public class ConversionCacheHolder { 048 049 protected static final Map<String, ConversionCacheEntry> cache = new HashMap<>(); 050 051 protected static final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock(); 052 053 private static final Log log = LogFactory.getLog(ConversionCacheHolder.class); 054 055 public static final int NB_SUB_PATH_PART = 5; 056 057 public static final int SUB_PATH_PART_SIZE = 2; 058 059 public static final AtomicLong CACHE_HITS = new AtomicLong(); 060 061 // Utility class. 062 private ConversionCacheHolder() { 063 } 064 065 public static long getCacheHits() { 066 return CACHE_HITS.get(); 067 } 068 069 public static int getNbCacheEntries() { 070 return cache.keySet().size(); 071 } 072 073 protected static List<String> getSubPathFromKey(String key) { 074 List<String> subPath = new ArrayList<>(); 075 076 String path = Base64.encodeBase64String(key.getBytes()); 077 078 path = path.replace("+", "X"); 079 path = path.replace("/", "Y"); 080 081 int idx = 0; 082 083 for (int i = 0; i < NB_SUB_PATH_PART; i++) { 084 String subPart = path.substring(idx, idx + SUB_PATH_PART_SIZE); 085 subPath.add(subPart); 086 idx += SUB_PATH_PART_SIZE; 087 if (idx >= path.length()) { 088 break; 089 } 090 } 091 return subPath; 092 } 093 094 protected static String getCacheEntryPath(String key) { 095 Path path = new Path(ConversionServiceImpl.getCacheBasePath()); 096 097 List<String> subPath = getSubPathFromKey(key); 098 099 for (String subPart : subPath) { 100 path = path.append(subPart); 101 new File(path.toString()).mkdir(); 102 } 103 104 // path = path.append(key); 105 106 return path.toString(); 107 } 108 109 public static void addToCache(String key, BlobHolder result) { 110 Objects.requireNonNull(key); 111 cacheLock.writeLock().lock(); 112 try { 113 doAddToCache(key, result); 114 } finally { 115 cacheLock.writeLock().unlock(); 116 } 117 } 118 119 protected static void doAddToCache(String key, BlobHolder result) { 120 ConversionCacheEntry cce = new ConversionCacheEntry(result); 121 boolean persisted = false; 122 123 try { 124 persisted = cce.persist(getCacheEntryPath(key)); 125 } catch (IOException e) { 126 log.error("Error while trying to persist cache entry", e); 127 } 128 129 if (persisted) { 130 cache.put(key, cce); 131 } 132 } 133 134 public static void removeFromCache(String key) { 135 cacheLock.writeLock().lock(); 136 try { 137 doRemoveFromCache(key); 138 } finally { 139 cacheLock.writeLock().unlock(); 140 } 141 142 } 143 144 protected static void doRemoveFromCache(String key) { 145 if (cache.containsKey(key)) { 146 ConversionCacheEntry cce = cache.get(key); 147 cce.remove(); 148 cache.remove(key); 149 } 150 } 151 152 public static ConversionCacheEntry getCacheEntry(String key) { 153 cacheLock.readLock().lock(); 154 try { 155 return doGetCacheEntry(key); 156 } finally { 157 cacheLock.readLock().unlock(); 158 } 159 } 160 161 protected static ConversionCacheEntry doGetCacheEntry(String key) { 162 return cache.get(key); 163 } 164 165 public static BlobHolder getFromCache(String key) { 166 // in case of present key but missing file we need to remove the cache key. 167 cacheLock.writeLock().lock(); 168 try { 169 BlobHolder result = doGetFromCache(key); 170 if (result == null) { 171 removeFromCache(key); 172 } 173 return result; 174 } finally { 175 cacheLock.writeLock().unlock(); 176 } 177 } 178 179 protected static BlobHolder doGetFromCache(String key) { 180 ConversionCacheEntry cacheEntry = cache.get(key); 181 if (cacheEntry != null) { 182 if (CACHE_HITS.incrementAndGet() < 0) { 183 // skip all negative values 184 CACHE_HITS.addAndGet(Long.MIN_VALUE); // back to 0 185 } 186 BlobHolder restored = cacheEntry.restore(); 187 if (restored.getBlob().getFile().exists()) { 188 return restored; 189 } 190 } 191 return null; 192 } 193 194 public static Set<String> getCacheKeys() { 195 cacheLock.readLock().lock(); 196 try { 197 return new HashSet<>(cache.keySet()); 198 } finally { 199 cacheLock.readLock().unlock(); 200 } 201 } 202 203 /** 204 * @since 6.0 205 */ 206 public static void deleteCache() { 207 cacheLock.writeLock().lock(); 208 try { 209 cache.clear(); 210 new File(ConversionServiceImpl.getCacheBasePath()).delete(); 211 } finally { 212 cacheLock.writeLock().unlock(); 213 } 214 } 215}