View Javadoc
1   /*
2    * Copyright 2015 Jin Kwon <jinahya_at_gmail.com>.
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  
18  package com.github.jinahya.simple.file.back;
19  
20  
21  import java.io.IOException;
22  import static java.lang.invoke.MethodHandles.lookup;
23  import java.nio.ByteBuffer;
24  import static java.nio.channels.Channels.newInputStream;
25  import static java.nio.channels.Channels.newOutputStream;
26  import java.nio.channels.FileChannel;
27  import java.nio.channels.ReadableByteChannel;
28  import java.nio.channels.WritableByteChannel;
29  import java.nio.file.Files;
30  import static java.nio.file.Files.newByteChannel;
31  import java.nio.file.Path;
32  import java.nio.file.StandardCopyOption;
33  import java.nio.file.StandardOpenOption;
34  import java.security.NoSuchAlgorithmException;
35  import static java.util.Optional.ofNullable;
36  import static java.util.stream.Collectors.joining;
37  import java.util.stream.StreamSupport;
38  import javax.inject.Inject;
39  import org.slf4j.Logger;
40  import static org.slf4j.LoggerFactory.getLogger;
41  
42  
43  /**
44   *
45   * @author Jin Kwon <jinahya_at_gmail.com>
46   */
47  public class LocalFileBack implements FileBack {
48  
49  
50      private static final String KEY_DIGEST_ALGORITHM = "SHA-1"; // 160 bits
51  
52  
53      private static final int PATH_TOKEN_LENGTH = 3;
54  
55  
56      private static final String PATH_TOKEN_DELIMITER = "/";
57  
58  
59      static Path leafPath(final Path rootPath, final ByteBuffer fileKey,
60                           final boolean createParent) {
61  
62          final Logger logger = getLogger(lookup().lookupClass());
63  
64          logger.trace("leafPath({}, {}, {})", rootPath, fileKey, createParent);
65  
66          String pathName = null;
67          try {
68              pathName = FileBackUtilities.fileKeyToPathName(
69                  fileKey, KEY_DIGEST_ALGORITHM, PATH_TOKEN_LENGTH,
70                  PATH_TOKEN_DELIMITER);
71          } catch (final NoSuchAlgorithmException nsae) {
72              throw new RuntimeException(nsae);
73          }
74          logger.trace("path name: {}", pathName);
75  
76          final Path leafPath = rootPath.resolve(pathName.replace(
77              PATH_TOKEN_DELIMITER, rootPath.getFileSystem().getSeparator()));
78          logger.trace("leaf path: {}", leafPath);
79  
80          if (createParent) {
81              final Path parent = leafPath.getParent();
82              logger.trace("parent: {}", parent);
83              logger.trace("parent.directory: {}", Files.isDirectory(parent));
84              if (!Files.isDirectory(parent)) {
85                  try {
86                      final Path created = Files.createDirectories(parent);
87                      logger.trace("parent created: {}", created);
88                  } catch (Exception e) {
89                      logger.error("failed to create parent directory: " + parent,
90                                   e);
91                      throw new RuntimeException(e);
92                  }
93              }
94          }
95  
96          return leafPath;
97      }
98  
99  
100     @Override
101     public void operate(final FileContext fileContext)
102         throws IOException, FileBackException {
103 
104         if (fileContext == null) {
105             throw new NullPointerException("null fileContext");
106         }
107 
108         final FileOperation fileOperation
109             = ofNullable(fileContext.fileOperationSupplier())
110             .orElseThrow(
111                 () -> new FileBackException("no file operation supplier set"))
112             .get();
113         logger.trace("file operation: {}", fileOperation);
114         if (fileOperation == null) {
115             logger.error("null file operation supplied");
116             return;
117         }
118 
119         switch (fileOperation) {
120             case COPY:
121                 copy(fileContext);
122                 break;
123             case DELETE:
124                 delete(fileContext);
125                 break;
126             case READ:
127                 read(fileContext);
128                 break;
129             case WRITE:
130                 write(fileContext);
131                 break;
132             default:
133                 throw new FileBackException(
134                     "unsupported operation: " + fileOperation);
135         }
136     }
137 
138 
139     public void copy(final FileContext fileContext)
140         throws IOException, FileBackException {
141 
142         logger.trace("copy({})", fileContext);
143 
144         if (fileContext == null) {
145             throw new NullPointerException("null fileContext");
146         }
147 
148         final Path[] sourceLeafPath_ = new Path[1];
149         if (sourceLeafPath_[0] == null) {
150             ofNullable(fileContext.sourceKeySupplier()).ifPresent(s -> {
151                 ofNullable(s.get()).ifPresent(v -> {
152                     logger.trace("source key: {}", v);
153                     sourceLeafPath_[0] = leafPath(rootPath, v, false);
154                 });
155             });
156         }
157         final Path sourceLeafPath = sourceLeafPath_[0];
158         logger.trace("source leaf path: {}", sourceLeafPath);
159         ofNullable(fileContext.sourceObjectConsumer()).ifPresent(
160             c -> c.accept(sourceLeafPath));
161         if (sourceLeafPath == null) {
162             logger.error("no source leaf path located");
163             return;
164         }
165         if (!Files.isReadable(sourceLeafPath)) {
166             logger.error("source leaf path is not readable: {}",
167                          sourceLeafPath);
168             return;
169         }
170 
171         final Path[] targetLeafPath_ = new Path[1];
172         if (targetLeafPath_[0] == null) {
173             ofNullable(fileContext.targetKeySupplier()).ifPresent(s -> {
174                 ofNullable(s.get()).ifPresent(v -> {
175                     logger.trace("target key: {}", v);
176                     targetLeafPath_[0] = leafPath(rootPath, v, true);
177                 });
178             });
179         }
180         final Path targetLeafPath = targetLeafPath_[0];
181         logger.trace("target leaf path: {}", targetLeafPath);
182         ofNullable(fileContext.targetObjectConsumer()).ifPresent(
183             c -> c.accept(targetLeafPath));
184         if (targetLeafPath == null) {
185             logger.error("no target leaf path located");
186             return;
187         }
188 
189         if (sourceLeafPath.equals(targetLeafPath)) {
190             logger.error("source leaf path == target leaf path");
191             return;
192         }
193 
194         Files.copy(sourceLeafPath, targetLeafPath,
195                    StandardCopyOption.REPLACE_EXISTING);
196         logger.trace("file copied");
197 
198         final String pathName = StreamSupport
199             .stream(((Iterable<Path>) () -> rootPath.relativize(targetLeafPath)
200                      .iterator()).spliterator(), false)
201             .map(Path::toString).collect(joining("/"));
202         logger.trace("path name: {}", pathName);
203         ofNullable(fileContext.pathNameConsumer()).ifPresent(
204             c -> c.accept(pathName));
205 
206         ofNullable(fileContext.sourceCopiedConsumer()).ifPresent(
207             c -> c.accept(sourceLeafPath.toFile().length()));
208         ofNullable(fileContext.targetCopiedConsumer()).ifPresent(
209             c -> c.accept(targetLeafPath.toFile().length()));
210     }
211 
212 
213     public void delete(final FileContext fileContext)
214         throws IOException, FileBackException {
215 
216         if (fileContext == null) {
217             throw new NullPointerException("null fileContext");
218         }
219 
220         final Path[] leafPath_ = new Path[1];
221         if (leafPath_[0] == null) {
222             ofNullable(fileContext.sourceKeySupplier()).ifPresent(s -> {
223                 ofNullable(s.get()).ifPresent(v -> {
224                     logger.trace("source key: {}", v);
225                     leafPath_[0] = leafPath(rootPath, v, false);
226                 });
227             });
228         }
229         if (leafPath_[0] == null) {
230             ofNullable(fileContext.targetKeySupplier()).ifPresent(s -> {
231                 ofNullable(s.get()).ifPresent(v -> {
232                     logger.trace("target key: {}", v);
233                     leafPath_[0] = leafPath(rootPath, v, false);
234                 });
235             });
236         }
237         final Path leafPath = leafPath_[0];
238         logger.trace("leaf path: {}", leafPath);
239         ofNullable(fileContext.sourceObjectConsumer()).ifPresent(
240             c -> c.accept(leafPath));
241         ofNullable(fileContext.targetObjectConsumer()).ifPresent(
242             c -> c.accept(leafPath));
243 
244         final boolean fileDeleted = Files.deleteIfExists(leafPath);
245         logger.trace("file deleted: {}", fileDeleted);
246     }
247 
248 
249     public void read(final FileContext fileContext)
250         throws IOException, FileBackException {
251 
252         if (fileContext == null) {
253             throw new NullPointerException("null fileContext");
254         }
255 
256         final Path[] sourceLeafPath_ = new Path[1];
257         if (sourceLeafPath_[0] == null) {
258             ofNullable(fileContext.sourceKeySupplier()).ifPresent(s -> {
259                 ofNullable(s.get()).ifPresent(v -> {
260                     logger.trace("source key: {}", v);
261                     sourceLeafPath_[0] = leafPath(rootPath, v, false);
262                 });
263             });
264         }
265         if (sourceLeafPath_[0] == null) {
266             ofNullable(fileContext.pathNameSupplier()).ifPresent(s -> {
267                 ofNullable(s.get()).ifPresent(v -> {
268                     logger.trace("path name: {}", v);
269                     sourceLeafPath_[0] = rootPath.resolve(v);
270                 });
271             });
272         }
273         final Path sourceLeafPath = sourceLeafPath_[0];
274         logger.trace("source leaf path: {}", sourceLeafPath);
275         ofNullable(fileContext.sourceObjectConsumer()).ifPresent(
276             c -> c.accept(sourceLeafPath));
277         if (sourceLeafPath == null) {
278             logger.warn("no source leaf path located");
279             return;
280         }
281         if (!Files.isRegularFile(sourceLeafPath)) {
282             logger.warn("source leaf path is not a regular file: {}",
283                         sourceLeafPath);
284             return;
285         }
286 
287         final String pathName = StreamSupport
288             .stream(((Iterable<Path>) () -> rootPath.relativize(sourceLeafPath)
289                      .iterator()).spliterator(), false)
290             .map(Path::toString).collect(joining("/"));
291         logger.trace("path name: {}", pathName);
292         ofNullable(fileContext.pathNameConsumer()).ifPresent(
293             c -> c.accept(pathName));
294 
295         ofNullable(fileContext.sourceChannelConsumer()).ifPresent(c -> {
296             logger.trace("source channel consumer presents");
297             try {
298                 try (ReadableByteChannel sourceChannel = newByteChannel(
299                     sourceLeafPath, StandardOpenOption.READ)) {
300                     c.accept(sourceChannel);
301                 }
302             } catch (IOException ioe) {
303                 logger.error(
304                     "failed to open source leaf path: " + sourceLeafPath, ioe);
305             }
306         });
307 
308         ofNullable(fileContext.targetChannelSupplier()).ifPresent(s -> {
309             logger.trace("target channel supplier presents");
310             final WritableByteChannel targetChannel = s.get();
311             logger.trace("target channel: {}", targetChannel);
312             try {
313                 final long copied = Files.copy(
314                     sourceLeafPath, newOutputStream(targetChannel));
315                 ofNullable(fileContext.sourceCopiedConsumer()).ifPresent(
316                     c -> c.accept(copied));
317                 ofNullable(fileContext.targetCopiedConsumer()).ifPresent(
318                     c -> c.accept(copied));
319             } catch (final IOException ioe) {
320                 logger.error(
321                     "failed to copy from source leaf path to target channel",
322                     ioe);
323             }
324         });
325     }
326 
327 
328     public void write(final FileContext fileContext)
329         throws IOException, FileBackException {
330 
331         if (fileContext == null) {
332             throw new NullPointerException("null fileContext");
333         }
334 
335         final Path[] targetLeafPath_ = new Path[1];
336         if (targetLeafPath_[0] == null) {
337             ofNullable(fileContext.targetKeySupplier()).ifPresent(s -> {
338                 ofNullable(s.get()).ifPresent(v -> {
339                     logger.trace("target key: {}", v);
340                     targetLeafPath_[0] = leafPath(rootPath, v, true);
341                 });
342             });
343         }
344         final Path targetLeafPath = targetLeafPath_[0];
345         logger.trace("target leaf path: {}", targetLeafPath);
346         ofNullable(fileContext.targetObjectConsumer()).ifPresent(
347             c -> c.accept(targetLeafPath));
348         if (targetLeafPath == null) {
349             logger.warn("no target leaf path located");
350             return;
351         }
352 
353         final String pathName = StreamSupport
354             .stream(((Iterable<Path>) () -> rootPath.relativize(targetLeafPath)
355                      .iterator()).spliterator(), false)
356             .map(Path::toString).collect(joining("/"));
357         logger.trace("path name: {}", pathName);
358         ofNullable(fileContext.pathNameConsumer()).ifPresent(
359             c -> {
360                 logger.trace("accepting path name consumer");
361                 c.accept(pathName);
362             });
363 
364         ofNullable(fileContext.targetChannelConsumer()).ifPresent(c -> {
365             logger.trace("target channel consumer presents");
366             try {
367                 try (FileChannel targetChannel = FileChannel.open(
368                     targetLeafPath, StandardOpenOption.CREATE,
369                     StandardOpenOption.WRITE)) {
370                     c.accept(targetChannel);
371                     targetChannel.force(true);
372                 }
373             } catch (IOException ioe) {
374                 logger.error(
375                     "failed to open target leaf path: " + targetLeafPath, ioe);
376             }
377         });
378 
379         ofNullable(fileContext.sourceChannelSupplier()).ifPresent(s -> {
380             logger.trace("source channel supplier: {}", s);
381             final ReadableByteChannel sourceChannel = s.get();
382             logger.trace("target channel: {}", sourceChannel);
383             try {
384                 final long copied = Files.copy(
385                     newInputStream(sourceChannel), targetLeafPath,
386                     StandardCopyOption.REPLACE_EXISTING);
387                 ofNullable(fileContext.sourceCopiedConsumer()).ifPresent(
388                     c -> c.accept(copied));
389                 ofNullable(fileContext.targetCopiedConsumer()).ifPresent(
390                     c -> c.accept(copied));
391             } catch (final IOException ioe) {
392                 logger.error(
393                     "failed to copy from source channel to target leaf path",
394                     ioe);
395             }
396         });
397     }
398 
399 
400     Path rootPath() {
401 
402         return rootPath;
403     }
404 
405 
406     private transient final Logger logger = getLogger(lookup().lookupClass());
407 
408 
409     @Inject
410     @RootPath
411     private Path rootPath;
412 
413 
414 }
415