/*
* 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.github.rjeschke.txtmark.Configuration;
import com.github.rjeschke.txtmark.Processor;
import static com.pawandubey.griffin.Configurator.LINE_SEPARATOR;
import static com.pawandubey.griffin.Data.config;
import static com.pawandubey.griffin.Data.executorSet;
import static com.pawandubey.griffin.Data.tags;
import static com.pawandubey.griffin.DirectoryCrawler.OUTPUT_DIRECTORY;
import static com.pawandubey.griffin.DirectoryCrawler.SOURCE_DIRECTORY;
import static com.pawandubey.griffin.DirectoryCrawler.TAG_DIRECTORY;
import com.pawandubey.griffin.model.Parsable;
import com.pawandubey.griffin.model.Post;
import com.pawandubey.griffin.renderer.HandlebarsRenderer;
import com.pawandubey.griffin.renderer.Renderer;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
*
* @author Pawan Dubey pawandubey@outlook.com
*/
public class Parser {
private Indexer indexer;
private final Configuration renderConfig;
private final Renderer renderer;
private String parsedContent;
private Lock lock = new ReentrantLock();
private int i = 0;
/**
* creates a parser with configuration set to enable safe mode HTML with
* extended profile from txtmark, allowing spaces in fenced code blocks and
* encoding set to UTF-8.
*
* @throws java.io.IOException the exception
*/
public Parser() throws IOException {
indexer = new Indexer();
renderer = new HandlebarsRenderer();
renderConfig = Configuration.builder().enableSafeMode()
.forceExtentedProfile()
.setAllowSpacesInFencedCodeBlockDelimiters(true)
.setEncoding("UTF-8")
.setCodeBlockEmitter(new CodeBlockEmitter())
.build();
indexer.initIndexes();
}
/**
* Parses the collection of files in the queue to produce HTML output
*
* @param collection the queue of files to be parsed
* @throws InterruptedException the exception
* @throws java.io.IOException the exception
*/
protected void parse(BlockingQueue<Parsable> collection) throws InterruptedException, IOException {
Parsable p;
String content;
if (config.getRenderTags() && Files.notExists(Paths.get(TAG_DIRECTORY))) {
Files.createDirectory(Paths.get(TAG_DIRECTORY));
}
if (Files.notExists(Paths.get(OUTPUT_DIRECTORY).resolve("SITEMAP.xml"))
&& Files.exists(Paths.get(HandlebarsRenderer.templateRoot).resolve("SITEMAP.html"))) {
try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(OUTPUT_DIRECTORY).resolve("SITEMAP.xml"), StandardCharsets.UTF_8)) {
bw.write(renderer.renderSitemap());
}
catch (IOException ex) {
Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
}
}
while (!collection.isEmpty()) {
p = collection.take();
writeParsedFile(p);
renderTags();
}
indexer.sortIndexes();
//System.out.println(indexer.getIndexList().size());
renderIndexRssAnd404();
}
private void renderIndexRssAnd404() throws IOException {
List<SingleIndex> list = indexer.getIndexList();
if (Files.notExists(Paths.get(OUTPUT_DIRECTORY).resolve("index.html"))) {
try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(OUTPUT_DIRECTORY).resolve("index.html"), StandardCharsets.UTF_8)) {
bw.write(renderer.renderIndex(list.get(0)));
}
catch (IOException ex) {
Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (Files.notExists(Paths.get(OUTPUT_DIRECTORY, "page"))) {
Files.createDirectory(Paths.get(OUTPUT_DIRECTORY, "page"));
}
list.remove(0);
for (SingleIndex s : list) {
Path secondaryIndexPath = Paths.get(OUTPUT_DIRECTORY, "page", "" + (list.indexOf(s) + 2));
Files.createDirectory(secondaryIndexPath);
try (BufferedWriter bw = Files.newBufferedWriter(secondaryIndexPath.resolve("index.html"), StandardCharsets.UTF_8)) {
bw.write(renderer.renderIndex(s));
}
}
if (Files.notExists(Paths.get(OUTPUT_DIRECTORY).resolve("feed.xml")) && Files.exists(Paths.get(HandlebarsRenderer.templateRoot).resolve("feed.htm;"))) {
try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(OUTPUT_DIRECTORY).resolve("feed.xml"), StandardCharsets.UTF_8)) {
bw.write(renderer.renderRssFeed());
}
catch (IOException ex) {
Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (Files.notExists(Paths.get(OUTPUT_DIRECTORY, "404.html")) && Files.exists(Paths.get(HandlebarsRenderer.templateRoot, "404.html"))) {
try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(OUTPUT_DIRECTORY).resolve("404.html"), StandardCharsets.UTF_8)) {
bw.write(renderer.render404());
}
catch (IOException ex) {
Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
//TODO Method doesnt fit on screen -> TOO LONG. Refactor.
/**
* Renders the posts of each tag as symbolic links. This method calcualtes
* the number of threads needed based on the criteria that one thread will
* handle 10 tags. Then partitions the tags keyset into that many Lists of
* tags. Then each list is processed by a new thread in the executor
* service.
*
* @throws IOException the exception
*/
protected void renderTags() throws IOException {
if (config.getRenderTags()) {
for (List<Parsable> l : tags.values()) {
l.sort((s, t) -> {
return t.getDate().compareTo(s.getDate());
});
}
int numThreads = (tags.size() / 10) + 1;
ExecutorService tagExecutor = Executors.newFixedThreadPool(numThreads);
executorSet.add(tagExecutor);
Set<List<String>> parts = new HashSet<>();
for (int i = 0; i < numThreads; i++) {
parts.add(tags.keySet()
.stream()
.skip(i * 10)
.limit(10)
.collect(Collectors.toList()));
}
parts.stream()
.forEach(l -> tagExecutor.submit(() -> {
for (String a : l) {
Path tagDir = Paths.get(TAG_DIRECTORY).resolve(a);
if (Files.notExists(tagDir)) {
try {// (BufferedWriter bw = Files.newBufferedWriter(tagDir.resolve("index.html"), StandardCharsets.UTF_8)) {
Files.createDirectory(tagDir);
//bw.write(renderer.renderTagIndex(a, tags.get(a)));
}
catch (IOException ex) {
Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
}
}
List<Parsable> parsables = tags.get(a);
for (Parsable p : parsables) {
Path slugPath = tagDir.resolve(p.getSlug());
if (Files.notExists(slugPath)) {
if (lock.tryLock()) {
try {
if (Files.notExists(slugPath)) {
Files.createDirectory(slugPath);
Path linkedFile = resolveHtmlPath(p);
Files.createSymbolicLink(slugPath.resolve("index.html"), Paths.get("/").resolve(Paths.get(OUTPUT_DIRECTORY)).relativize(linkedFile));
}
}
catch (IOException ex) {
Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
}
finally {
lock.unlock();
}
}
}
}
if (lock.tryLock()) {
try (BufferedWriter bw = Files.newBufferedWriter(tagDir.resolve("index.html"), StandardCharsets.UTF_8)) {
bw.write(renderer.renderTagIndex(a, parsables));
}
catch (IOException ex) {
Logger.getLogger(Parser.class.getName()).log(Level.SEVERE, null, ex);
}
finally {
lock.unlock();
}
}
}
}));
}
}
/**
* Shuts down all executors in the executorSet one by one gracefully.
*/
protected void shutDownExecutors() {
for (ExecutorService e : executorSet) {
try {
e.shutdown();
e.awaitTermination(2, TimeUnit.SECONDS);
}
catch (InterruptedException ex) {
Logger.getLogger(Parser.class
.getName()).log(Level.WARNING, null, ex);
}
finally {
if (!e.isTerminated()) {
e.shutdownNow();
}
}
}
}
/**
* Reads the content from the given path into a String object.
*
* @param p the path to the file
* @return the String contents
*/
private String readFile(Path p) {
StringBuilder sb = new StringBuilder();
String line;
try (BufferedReader br = Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
while ((line = br.readLine()) != null) {
sb.append(line).append(LINE_SEPARATOR);
}
}
catch (IOException ex) {
Logger.getLogger(Parser.class
.getName()).log(Level.SEVERE, null, ex);
}
return sb.toString();
}
/**
* Writes the content of the Parsable to the path resolved from the slug by
* creating a directory from the given slug and then writing the contents
* into the index.html file inside it for pretty links.
*
* @param p the Parsable instance
*/
private void writeParsedFile(Parsable p) throws IOException {
Path htmlPath = resolveHtmlPath(p);
if (config.getRenderTags() && p instanceof Post) {
resolveTags(p, htmlPath);
}
try (BufferedWriter bw = Files.newBufferedWriter(htmlPath, StandardCharsets.UTF_8)) {
parsedContent = Processor.process(p.getContent(), renderConfig);
p.setContent(parsedContent);
if (p instanceof Post) {
indexer.addToIndex(p);
}
bw.write(renderer.renderParsable(p));
}
catch (IOException ex) {
Logger.getLogger(Parser.class
.getName()).log(Level.SEVERE, null, ex);
}
}
private Path resolveHtmlPath(Parsable p) throws IOException {
String name = p.getSlug();
Path parsedDirParent = Paths.get(OUTPUT_DIRECTORY).resolve(Paths.get(SOURCE_DIRECTORY).relativize(p.getLocation().getParent()));
Path parsedDir = parsedDirParent.resolve(name);
if (Files.notExists(parsedDir)) {
Files.createDirectory(parsedDir);
}
Path htmlPath = parsedDir.resolve("index.html");
return htmlPath;
}
private void resolveTags(Parsable p, Path htmlPath) throws IOException {
List<String> ptags = p.getTags();
for (String t : ptags) {
if (!t.equals("nav")) {
if (tags.get(t) != null) {
tags.get(t).removeIf(q -> q.getPermalink().equals(p.getPermalink()));
tags.get(t).add(p);
}
else {
List<Parsable> l = new ArrayList<>();
l.add(p);
tags.put(t, l);
}
}
}
}
}