1 /**
2  *  Copyright Terracotta, Inc.
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License");
5  *  you may not use this file except in compliance with the License.
6  *  You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  */

16
17 package net.sf.ehcache;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.io.RandomAccessFile;
22 import java.nio.channels.FileChannel;
23 import java.nio.channels.FileLock;
24 import java.nio.channels.OverlappingFileLockException;
25 import java.util.HashSet;
26 import java.util.Set;
27
28 import net.sf.ehcache.config.DiskStoreConfiguration;
29
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * Manager class to handle disk store path. CacheManager has a reference to this manager.
35  *
36  * @author hhuynh
37  *
38  */

39 public final class DiskStorePathManager {
40     /**
41      * If the CacheManager needs to resolve a conflict with the disk path, it will create a
42      * subdirectory in the given disk path with this prefix followed by a number. The presence of this
43      * name is used to determined whether it makes sense for a persistent DiskStore to be loaded. Loading
44      * persistent DiskStores will only have useful semantics where the diskStore path has not changed.
45      */

46     private static final String AUTO_DISK_PATH_DIRECTORY_PREFIX = "ehcache_auto_created";
47     private static final Logger LOG = LoggerFactory.getLogger(DiskStorePathManager.class);
48     private static final String LOCK_FILE_NAME = ".ehcache-diskstore.lock";
49
50     private static final int DEL = 0x7F;
51     private static final char ESCAPE = '%';
52     private static final Set<Character> ILLEGALS = new HashSet<Character>();
53
54     private final File initialPath;
55     private final boolean defaultPath;
56
57     private volatile DiskStorePath path;
58
59     static {
60         ILLEGALS.add('/');
61         ILLEGALS.add('\\');
62         ILLEGALS.add('<');
63         ILLEGALS.add('>');
64         ILLEGALS.add(':');
65         ILLEGALS.add('"');
66         ILLEGALS.add('|');
67         ILLEGALS.add('?');
68         ILLEGALS.add('*');
69         ILLEGALS.add('.');
70     }
71
72     /**
73      * Create a diskstore path manager with provided initial path.
74      *
75      * @param path
76      */

77     public DiskStorePathManager(String initialPath) {
78         this.initialPath = new File(initialPath);
79         this.defaultPath = false;
80     }
81
82     /**
83      * Create a diskstore path manager using the default path.
84      */

85     public DiskStorePathManager() {
86         this.initialPath = new File(DiskStoreConfiguration.getDefaultPath());
87         this.defaultPath = true;
88     }
89
90     /**
91      * Resolve and lock this disk store path if the resultant path contains the supplied file.
92      *
93      * @param file file to check for
94      * @return {@code trueif the file existed and the path was successfully locked
95      */

96     public boolean resolveAndLockIfExists(String file) {
97         if (path != null) {
98             return getFile(file).exists();
99         }
100
101         synchronized (this) {
102             if (path != null) {
103                 return getFile(file).exists();
104             }
105
106             // ensure disk store path exists
107             if (!initialPath.isDirectory()) {
108                 return false;
109             } else if (!new File(initialPath, file).exists()) {
110                 return false;
111             } else {
112                 try {
113                     path = new DiskStorePath(initialPath, false, defaultPath);
114                 } catch (DiskstoreNotExclusiveException e) {
115                     throw new CacheException(e);
116                 }
117
118                 LOG.debug("Using diskstore path {}", path.getDiskStorePath());
119                 LOG.debug("Holding exclusive lock on {}", path.getLockFile());
120                 return true;
121             }
122         }
123     }
124
125     private void resolveAndLockIfNeeded(boolean allowAutoCreate) throws DiskstoreNotExclusiveException {
126         if (path != null) {
127             return;
128         }
129
130         synchronized (this) {
131             if (path != null) {
132                 return;
133             }
134
135             File candidate = initialPath;
136
137             boolean autoCreated = false;
138             while (true) {
139                 // ensure disk store path exists
140                 if (!candidate.isDirectory() && !candidate.mkdirs()) {
141                     throw new CacheException("Disk store path can't be created: " + candidate);
142                 }
143
144                 try {
145                     path = new DiskStorePath(candidate, autoCreated, !autoCreated && defaultPath);
146                     break;
147                 } catch (DiskstoreNotExclusiveException e) {
148                     if (!allowAutoCreate) { throw e; }
149
150                     autoCreated = true;
151                     try {
152                         candidate = File.createTempFile(AUTO_DISK_PATH_DIRECTORY_PREFIX, "diskstore", initialPath);
153                         // we want to create a directory with this temp name so deleting the file first
154                         candidate.delete();
155                     } catch (IOException ioe) {
156                         throw new CacheException(ioe);
157                     }
158                 }
159             }
160
161             if (autoCreated) {
162                 LOG.warn("diskStorePath '" + initialPath
163                         + "' is already used by an existing CacheManager either in the same VM or in a different process.\n"
164                         + "The diskStore path for this CacheManager will be set to " + candidate + ".\nTo avoid this"
165                         + " warning consider using the CacheManager factory methods to create a singleton CacheManager "
166                         + "or specifying a separate ehcache configuration (ehcache.xml) for each CacheManager instance.");
167             }
168
169             LOG.debug("Using diskstore path {}", path.getDiskStorePath());
170             LOG.debug("Holding exclusive lock on {}", path.getLockFile());
171         }
172     }
173
174     /**
175      * sanitize a name for valid file or directory name
176      *
177      * @param name
178      * @return sanitized version of name
179      */

180     private static String safeName(String name) {
181         int len = name.length();
182         StringBuilder sb = new StringBuilder(len);
183         for (int i = 0; i < len; i++) {
184             char c = name.charAt(i);
185             if (c <= ' ' || c >= DEL || (c >= 'A' && c <= 'Z') || ILLEGALS.contains(c) || c == ESCAPE) {
186                 sb.append(ESCAPE);
187                 sb.append(String.format("%04x", (int) c));
188             } else {
189                 sb.append(c);
190             }
191         }
192         return sb.toString();
193     }
194
195     private static void deleteFile(File f) {
196         if (!f.delete()) {
197             LOG.debug("Failed to delete file {}", f.getAbsolutePath());
198         }
199     }
200
201     /**
202      * Was this path auto-created (ie. the result of a collision)
203      *
204      * @return true if path is auto created
205      */

206     public boolean isAutoCreated() {
207         DiskStorePath diskStorePath = path;
208         if (diskStorePath == null) {
209             throw new IllegalStateException();
210         }
211
212         return diskStorePath.isAutoCreated();
213     }
214
215     /**
216      * Was this path sourced from the default value.
217      *
218      * @return true if path is the default
219      */

220     public boolean isDefault() {
221         DiskStorePath diskStorePath = path;
222         if (diskStorePath == null) {
223             throw new IllegalStateException();
224         }
225
226         return diskStorePath.isDefault();
227     }
228
229     /**
230      * release the lock file used for collision detection
231      * should be called when cache manager shutdowns
232      */

233     public synchronized void releaseLock() {
234         try {
235             if (path != null) {
236                 path.unlock();
237             }
238         } finally {
239             path = null;
240         }
241     }
242
243     /**
244      * Get a file object for the given cache-name and suffix
245      *
246      * @param cacheName the cache name
247      * @param suffix a file suffix
248      * @return a file object
249      */

250     public File getFile(String cacheName, String suffix) {
251         return getFile(safeName(cacheName) + suffix);
252     }
253
254     /**
255      * Get a file object for the given name
256      *
257      * @param name the file name
258      * @return a file object
259      */

260     public File getFile(String name) {
261         try {
262             resolveAndLockIfNeeded(true);
263         } catch (DiskstoreNotExclusiveException e) {
264             throw new CacheException(e);
265         }
266
267         File diskStorePath = path.getDiskStorePath();
268
269         File file = new File(diskStorePath, name);
270         for (File parent = file.getParentFile(); parent != null; parent = parent.getParentFile()) {
271             if (diskStorePath.equals(parent)) {
272                 return file;
273             }
274         }
275
276         throw new IllegalArgumentException("Attempted to access file outside the disk-store path");
277     }
278
279     /**
280      * Exception class thrown when a diskstore path collides with an existing one
281      *
282      */

283     private static class DiskstoreNotExclusiveException extends Exception {
284
285         /**
286          * Constructor for the DiskstoreNotExclusiveException object.
287          */

288         public DiskstoreNotExclusiveException() {
289             super();
290         }
291
292         /**
293          * Constructor for the DiskstoreNotExclusiveException object.
294          *
295          * @param message the exception detail message
296          */

297         public DiskstoreNotExclusiveException(String message) {
298             super(message);
299         }
300     }
301
302     /**
303      * Resolved path and lock details
304      */

305     private static class DiskStorePath {
306         private final FileLock directoryLock;
307         private final File lockFile;
308         private final File diskStorePath;
309         private final boolean autoCreated;
310         private final boolean defaultPath;
311
312         DiskStorePath(File path, boolean autoCreated, boolean defaultPath) throws DiskstoreNotExclusiveException {
313             this.diskStorePath = path;
314             this.autoCreated = autoCreated;
315             this.defaultPath = defaultPath;
316
317             lockFile = new File(path.getAbsoluteFile(), LOCK_FILE_NAME);
318             lockFile.deleteOnExit();
319
320             FileLock dirLock;
321             try {
322                 lockFile.createNewFile();
323                 if (!lockFile.exists()) {
324                     throw new AssertionError("Failed to create lock file " + lockFile);
325                 }
326                 FileChannel lockFileChannel = new RandomAccessFile(lockFile, "rw").getChannel();
327                 dirLock = lockFileChannel.tryLock();
328             } catch (OverlappingFileLockException ofle) {
329                 dirLock = null;
330             } catch (IOException ioe) {
331                 throw new CacheException(ioe);
332             }
333
334             if (dirLock == null) {
335                 throw new DiskstoreNotExclusiveException(path.getAbsolutePath() + " is not exclusive.");
336             }
337
338             this.directoryLock = dirLock;
339         }
340
341         boolean isAutoCreated() {
342             return autoCreated;
343         }
344
345         boolean isDefault() {
346             return defaultPath;
347         }
348
349         File getDiskStorePath() {
350             return diskStorePath;
351         }
352
353         File getLockFile() {
354             return lockFile;
355         }
356
357         void unlock() {
358             if (directoryLock != null && directoryLock.isValid()) {
359                 try {
360                     directoryLock.release();
361                     directoryLock.channel().close();
362                     deleteFile(lockFile);
363                 } catch (IOException e) {
364                     throw new CacheException("Failed to release disk store path's lock file:" + lockFile, e);
365                 }
366             }
367
368
369             if (autoCreated) {
370                 if (diskStorePath.delete()) {
371                     LOG.debug("Deleted directory " + diskStorePath.getName());
372                 }
373             }
374         }
375
376         @Override
377         public String toString() {
378             return diskStorePath.getAbsolutePath();
379         }
380     }
381
382 }
383