1/*
2 * The MIT License (MIT)
3 *
4 * Copyright (c) 2014-2016 Christian Schudt
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24
25package rocks.xmpp.util.cache;
26
27import java.io.IOException;
28import java.io.UncheckedIOException;
29import java.nio.file.FileVisitResult;
30import java.nio.file.Files;
31import java.nio.file.Path;
32import java.nio.file.SimpleFileVisitor;
33import java.nio.file.attribute.BasicFileAttributes;
34import java.util.Arrays;
35import java.util.Collection;
36import java.util.Collections;
37import java.util.Map;
38import java.util.Optional;
39import java.util.Set;
40import java.util.function.BiConsumer;
41import java.util.function.Predicate;
42import java.util.stream.Collectors;
43import java.util.stream.Stream;
44
45/**
46 * A simple directory based cache for caching of persistent items like avatars or entity capabilities.
47 *
48 * @author Christian Schudt
49 */
50public final class DirectoryCache implements Map<String, byte[]> {
51
52 private final Path cacheDirectory;
53
54 public DirectoryCache(Path cacheDirectory) {
55 this.cacheDirectory = cacheDirectory;
56 }
57
58 @Override
59 public final int size() {
60 try (final Stream<Path> files = cacheContent()) {
61 return (int) Math.min(files.count(), Integer.MAX_VALUE);
62 }
63 }
64
65 @Override
66 public final boolean isEmpty() {
67 try (final Stream<Path> files = cacheContent()) {
68 return files.findAny().map(file -> Boolean.FALSE).orElse(Boolean.TRUE);
69 }
70 }
71
72 @Override
73 public final boolean containsKey(Object key) {
74 return Files.exists(cacheDirectory.resolve(key.toString()));
75 }
76
77 @Override
78 public final boolean containsValue(Object value) {
79 throw new UnsupportedOperationException();
80 }
81
82 @Override
83 public final byte[] get(final Object key) {
84 return Optional.ofNullable(key).map(Object::toString).filter(((Predicate<String>) String::isEmpty).negate()).map(cacheDirectory::resolve).filter(Files::isReadable).map(file -> {
85 try {
86 return Files.readAllBytes(file);
87 } catch (IOException e) {
88 throw new UncheckedIOException(e);
89 }
90 }).orElse(null);
91 }
92
93 @Override
94 public final byte[] put(String key, byte[] value) {
95 // Make sure the directory exists.
96 byte[] data = get(key);
97 if (!Arrays.equals(data, value))
98 try {
99 if (Files.notExists(cacheDirectory)) {
100 Files.createDirectories(cacheDirectory);
101 }
102 Path file = cacheDirectory.resolve(key);
103 Files.write(file, value);
104 } catch (IOException e) {
105 throw new UncheckedIOException(e);
106 }
107 return data;
108 }
109
110 @Override
111 public final byte[] remove(Object key) {
112 byte[] data = get(key);
113 try {
114 Files.deleteIfExists(cacheDirectory.resolve(key.toString()));
115 } catch (IOException e) {
116 throw new UncheckedIOException(e);
117 }
118 return data;
119 }
120
121 @Override
122 public final void putAll(Map<? extends String, ? extends byte[]> m) {
123 m.forEach(this::put);
124 }
125
126 @Override
127 public final void clear() {
128 try {
129 Files.walkFileTree(cacheDirectory, new SimpleFileVisitor<Path>() {
130 @Override
131 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
132 Files.deleteIfExists(file);
133 return FileVisitResult.CONTINUE;
134 }
135
136 @Override
137 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
138 // Don't delete the cache directory itself.
139 if (!Files.isSameFile(dir, cacheDirectory)) {
140 Files.deleteIfExists(dir);
141 }
142 return FileVisitResult.CONTINUE;
143 }
144 });
145 } catch (IOException e) {
146 throw new UncheckedIOException(e);
147 }
148 }
149
150 @Override
151 public final Set<String> keySet() {
152 try (final Stream<Path> files = Files.list(cacheDirectory)) {
153 return Collections.unmodifiableSet(files.map(Path::getFileName).map(Path::toString).collect(Collectors.toSet()));
154 } catch (IOException e) {
155 throw new UncheckedIOException(e);
156 }
157 }
158
159 @Override
160 public final Collection<byte[]> values() {
161 throw new UnsupportedOperationException();
162 }
163
164 @Override
165 public final Set<Entry<String, byte[]>> entrySet() {
166 throw new UnsupportedOperationException();
167 }
168
169 @Override
170 public final void forEach(final BiConsumer<? super String, ? super byte[]> action) {
171 if (Files.exists(cacheDirectory))
172 try (final Stream<Path> files = cacheContent().filter(Files::isReadable)) {
173 files.forEach(file -> {
174 try {
175 action.accept(file.getFileName().toString(), Files.readAllBytes(file));
176 } catch (final IOException e) {
177 throw new UncheckedIOException(e);
178 }
179 });
180 }
181 }
182
183 @SuppressWarnings("StreamResourceLeak")
184 private final Stream<Path> cacheContent() {
185 try {
186 return Files.walk(cacheDirectory).filter(Files::isRegularFile);
187 } catch (final IOException e) {
188 throw new UncheckedIOException(e);
189 }
190 }
191
192}