/* * Copyright 2015 Pawan Dubey pawandubey@outlook.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pawandubey.griffin; import com.moandjiezana.toml.Toml; import static com.pawandubey.griffin.Configurator.LINE_SEPARATOR; import static com.pawandubey.griffin.Data.config; import com.pawandubey.griffin.model.Page; import com.pawandubey.griffin.model.Parsable; import com.pawandubey.griffin.model.Post; import static com.pawandubey.griffin.renderer.Renderer.templateRoot; import java.io.BufferedReader; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Pawan Dubey pawandubey@outlook.com */ public class DirectoryCrawler { public static final String USERHOME = System.getProperty("user.home"); public static final String FILE_SEPARATOR = System.getProperty("file.separator"); //TODO remove hardcoded value public static String ROOT_DIRECTORY = System.getProperty("user.dir"); public static final String SOURCE_DIRECTORY = ROOT_DIRECTORY + FILE_SEPARATOR + "content"; public static final String OUTPUT_DIRECTORY = ROOT_DIRECTORY + FILE_SEPARATOR + "output"; public static final String INFO_FILE = ROOT_DIRECTORY + FILE_SEPARATOR + ".info"; public static String author = config.getSiteAuthor(); public final String HEADER_DELIMITER = "#####"; public static final String TAG_DIRECTORY = OUTPUT_DIRECTORY + FILE_SEPARATOR + "tags"; private final StringBuilder header = new StringBuilder(); private final Toml toml = new Toml(); public static final String EXCERPT_MARKER = "##more##"; public DirectoryCrawler() { } /** * Creates a new DirectoryCrawler with the root path of the site directory * set to path. * * @param path the path to the root of the site's directory. */ public DirectoryCrawler(String path) { ROOT_DIRECTORY = path; } /** * Crawls the whole content directory and adds the files to the main queue * for parsing. * * @param rootPath path to the content directory * @throws IOException the exception */ protected void readIntoQueue(Path rootPath) throws IOException, InterruptedException { cleanOutputDirectory(); copyTemplateAssets(); Files.walkFileTree(rootPath, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path correspondingOutputPath = Paths.get(OUTPUT_DIRECTORY).resolve(rootPath.relativize(dir)); if (config.getExcludeDirs().contains(dir.getFileName().toString())) { return FileVisitResult.SKIP_SUBTREE; } if (Files.notExists(correspondingOutputPath)) { Files.createDirectory(correspondingOutputPath); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { try { Path resolvedPath = Paths.get(OUTPUT_DIRECTORY).resolve(rootPath.relativize(file)); if (Files.probeContentType(file).equals("text/x-markdown")) { Parsable parsable = createParsable(file); Data.fileQueue.put(parsable); } else { Files.copy(file, resolvedPath, StandardCopyOption.REPLACE_EXISTING); } } catch (InterruptedException ex) { Logger.getLogger(DirectoryCrawler.class.getName()).log(Level.SEVERE, null, ex); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.TERMINATE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } }); } //TODO refactor this method to make use of the above method someway. /** * Checks if the file has been modified after the last parse event and only * then adds the file into the queue for parsing, hence saving time. * * @param rootPath * @throws IOException the exception */ protected void fastReadIntoQueue(Path rootPath) throws IOException, InterruptedException { cleanOutputDirectory(); copyTemplateAssets(); Files.walkFileTree(rootPath, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path correspondingOutputPath = Paths.get(OUTPUT_DIRECTORY).resolve(rootPath.relativize(dir)); if (config.getExcludeDirs().contains(dir.getFileName().toString())) { return FileVisitResult.SKIP_SUBTREE; } if (Files.notExists(correspondingOutputPath)) { Files.createDirectory(correspondingOutputPath); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { try { LocalDateTime fileModified = LocalDateTime.ofInstant(Files.getLastModifiedTime(file).toInstant(), ZoneId.systemDefault()); LocalDateTime lastParse = LocalDateTime.parse(InfoHandler.LAST_PARSE_DATE, InfoHandler.formatter); Path resolvedPath = Paths.get(OUTPUT_DIRECTORY).resolve(rootPath.relativize(file)); if (Files.probeContentType(file).equals("text/x-markdown")) { if (fileModified.isAfter(lastParse)) { Parsable parsable = createParsable(file); Data.fileQueue.removeIf(p -> p.getPermalink().equals(parsable.getPermalink())); Data.fileQueue.put(parsable); } } else { Files.copy(file, resolvedPath, StandardCopyOption.REPLACE_EXISTING); } } catch (InterruptedException ex) { Logger.getLogger(DirectoryCrawler.class.getName()).log(Level.SEVERE, null, ex); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.TERMINATE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } }); } /** * Creates an appropriate instance of a Parsable implementation depending * upon the header of the file. * * @param file the path of the file from which to create a Parsable. * @return the created Parsable. */ private Parsable createParsable(Path file) { try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { header.setLength(0); String line; while ((line = br.readLine()) != null && !line.equals(HEADER_DELIMITER)) { header.append(line).append(LINE_SEPARATOR); } toml.parse(header.toString()); String title = toml.getString("title"); author = toml.getString("author") != null ? toml.getString("author") : author; String date = toml.getString("date"); String slug = toml.getString("slug"); LocalDate publishDate = LocalDate.parse(date, DateTimeFormatter.ofPattern(config.getInputDateFormat())); publishDate = LocalDate.parse(publishDate.format(DateTimeFormatter.ofPattern(config.getOutputDateFormat())), DateTimeFormatter.ofPattern(config.getOutputDateFormat())); String layout = toml.getString("layout"); List<String> tag = toml.getList("tags"); String img = toml.getString("image"); StringBuilder content = new StringBuilder(); String[] halves; while ((line = br.readLine()) != null) { content.append(line).append(LINE_SEPARATOR); } if (layout.equals("post")) { return new Post(title, author, publishDate, file, content.toString(), img, slug, layout, tag); } else { return new Page(title, author, file, content.toString(), img, slug, layout, tag); } } catch (IOException ex) { Logger.getLogger(DirectoryCrawler.class.getName()).log(Level.SEVERE, null, ex); } return null; } /** * Cleans up the output directory before running the full parse. * * @throws IOException When a file visit goes wrong */ private void cleanOutputDirectory() throws IOException, InterruptedException { System.out.println("Cleaning up the output area..."); Path pathToClean = Paths.get(OUTPUT_DIRECTORY).toAbsolutePath().normalize(); Files.walkFileTree(pathToClean, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.TERMINATE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); Files.createDirectory(pathToClean); System.out.println("Cleanup done."); } /** * Copies the assets i.e images, CSS, JS etc needed by the theme to the * output directory. * * @throws IOException the exception */ private void copyTemplateAssets() throws IOException { System.out.println("Carefully copying the assests..."); Path assetsPath = Paths.get(templateRoot, "assets"); Path outputAssetsPath = Paths.get(OUTPUT_DIRECTORY, "assets"); if (Files.notExists(outputAssetsPath)) { Files.createDirectory(outputAssetsPath); } Files.walkFileTree(assetsPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path correspondingOutputPath = outputAssetsPath.resolve(assetsPath.relativize(dir)); if (Files.notExists(correspondingOutputPath)) { Files.createDirectory(correspondingOutputPath); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Path resolvedPath = outputAssetsPath.resolve(assetsPath.relativize(file)); Files.copy(file, resolvedPath, StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } }); System.out.println("Copying done."); } }