DirectoryCache.java

  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}