package org.jbake.parser;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.IOUtils;
import org.jbake.app.ConfigUtil.Keys;
import org.jbake.app.Crawler;
import org.json.simple.JSONValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for markup engine wrappers. A markup engine is responsible for rendering
* markup in a source file and exporting the result into the {@link ParserContext#getContents() contents} map.
*
* This specific engine does nothing, meaning that the body is rendered as raw contents.
*
* @author Cédric Champeau
*/
public abstract class MarkupEngine implements ParserEngine {
private static final Logger LOGGER = LoggerFactory.getLogger(MarkupEngine.class);
private static final String HEADER_SEPARATOR = "~~~~~~";
/**
* Tests if this markup engine can process the document.
* @param context the parser context
* @return true if this markup engine has enough context to process this document. false otherwise
*/
public boolean validate(ParserContext context) { return true; }
/**
* Processes the document header. Usually subclasses will parse the document body and look for
* specific header metadata and export it into {@link ParserContext#getContents() contents} map.
* @param context the parser context
*/
public void processHeader(final ParserContext context) {}
/**
* Processes the body of the document. Usually subclasses will parse the document body and render
* it, exporting the result using the {@link org.jbake.parser.ParserContext#setBody(String)} method.
* @param context the parser context
*/
public void processBody(final ParserContext context) {}
/**
* Parse given file to extract as much infos as possible
* @param file file to process
* @return a map containing all infos. Returning null indicates an error, even if an exception would be better.
*/
public Map<String, Object> parse(Configuration config, File file, String contentPath) {
Map<String,Object> content = new HashMap<String, Object>();
InputStream is = null;
List<String> fileContents = null;
try {
is = new FileInputStream(file);
fileContents = IOUtils.readLines(is, config.getString(Keys.RENDER_ENCODING));
} catch (IOException e) {
LOGGER.error("Error while opening file {}: {}", file, e);
return null;
} finally {
IOUtils.closeQuietly(is);
}
boolean hasHeader = hasHeader(fileContents);
ParserContext context = new ParserContext(
file,
fileContents,
config,
contentPath,
hasHeader,
content
);
if (hasHeader) {
// read header from file
processHeader(config, fileContents, content);
}
// then read engine specific headers
processHeader(context);
if (content.get(Crawler.Attributes.DATE) == null) {
content.put(Crawler.Attributes.DATE, new Date(file.lastModified()));
}
if (config.getString(Keys.DEFAULT_STATUS) != null) {
// default status has been set
if (content.get(Crawler.Attributes.STATUS) == null) {
// file hasn't got status so use default
content.put(Crawler.Attributes.STATUS, config.getString(Keys.DEFAULT_STATUS));
}
}
if (config.getString(Keys.DEFAULT_TYPE) != null) {
// default type has been set
if (content.get(Crawler.Attributes.TYPE) == null) {
// file hasn't got type so use default
content.put(Crawler.Attributes.TYPE, config.getString(Keys.DEFAULT_TYPE));
}
}
if (content.get(Crawler.Attributes.TYPE)==null||content.get(Crawler.Attributes.STATUS)==null) {
// output error
LOGGER.warn("Error parsing meta data from header (missing type or status value) for file {}!", file);
return null;
}
// generate default body
processBody(fileContents, content);
// eventually process body using specific engine
if (validate(context)) {
processBody(context);
} else {
LOGGER.error("Incomplete source file ({}) for markup engine:", file, getClass().getSimpleName());
return null;
}
if (content.get(Crawler.Attributes.TAGS) != null) {
String[] tags = (String[]) content.get(Crawler.Attributes.TAGS);
for( int i=0; i<tags.length; i++ ) {
tags[i]=tags[i].trim();
if (config.getBoolean(Keys.TAG_SANITIZE)) {
tags[i]=tags[i].replace(" ", "-");
}
}
content.put(Crawler.Attributes.TAGS, tags);
}
// TODO: post parsing plugins to hook in here?
return content;
}
/**
* Checks if the file has a meta-data header.
*
* @param contents Contents of file
* @return true if header exists, false if not
*/
private boolean hasHeader(List<String> contents) {
boolean headerValid = false;
boolean headerSeparatorFound = false;
boolean statusFound = false;
boolean typeFound = false;
List<String> header = new ArrayList<String>();
for (String line : contents) {
if (!line.isEmpty()){
header.add(line);
}
if (line.contains("=")) {
if (line.startsWith("type=")) {
typeFound = true;
}
if (line.startsWith("status=")) {
statusFound = true;
}
}
if (line.equals(HEADER_SEPARATOR)) {
headerSeparatorFound = true;
header.remove(line);
break;
}
}
if (headerSeparatorFound) {
headerValid = true;
for (String headerLine : header) {
if (!headerLine.contains("=")) {
headerValid = false;
break;
}
}
}
return headerValid && statusFound && typeFound;
}
/**
* Process the header of the file.
* @param config
*
* @param contents Contents of file
* @param content
*/
private void processHeader(Configuration config, List<String> contents, final Map<String, Object> content) {
for (String line : contents) {
if (line.equals(HEADER_SEPARATOR)) {
break;
}
if (line.isEmpty()) {
continue;
}
String[] parts = line.split("=",2);
if (parts.length != 2) {
continue;
}
String utf8BOM = "\uFEFF";
String key;
if (parts[0].contains(utf8BOM)) {
key = parts[0].trim().replace(utf8BOM, "");
} else {
key = parts[0].trim();
}
String value = parts[1].trim();
if (key.equalsIgnoreCase(Crawler.Attributes.DATE)) {
DateFormat df = new SimpleDateFormat(config.getString(Keys.DATE_FORMAT));
Date date = null;
try {
date = df.parse(value);
content.put(key, date);
} catch (ParseException e) {
e.printStackTrace();
}
} else if (key.equalsIgnoreCase(Crawler.Attributes.TAGS)) {
content.put(key, getTags(value));
} else if (isJson(value)) {
content.put(key, JSONValue.parse(value));
} else {
content.put(key, value);
}
}
}
private String[] getTags(String tagsPart) {
String[] tags = tagsPart.split(",");
for( int i=0; i<tags.length; i++ )
tags[i]=tags[i].trim();
return tags;
}
private boolean isJson(String part) {
return part.startsWith("{") && part.endsWith("}");
}
/**
* Process the body of the file.
*
* @param contents Contents of file
* @param content
*/
private void processBody(List<String> contents, final Map<String, Object> content) {
StringBuilder body = new StringBuilder();
boolean inBody = false;
for (String line : contents) {
if (inBody) {
body.append(line).append("\n");
}
if (line.equals(HEADER_SEPARATOR)) {
inBody = true;
}
}
if (body.length() == 0) {
for (String line : contents) {
body.append(line).append("\n");
}
}
content.put(Crawler.Attributes.BODY, body.toString());
}
}