package io.conducive.server.resources;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.markdown4j.Markdown4jProcessor;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Serves templated html (or markdown) from an arbitrary file system location. Intended for simple
* text & images. (Also supports CSS, PDF).
*
* Intended to allow content to be separately managed from the application; it's common for clients to
* want to be able to edit content.
*
* @author Reuben Firmin
*/
@Path("/content")
public class ContentResource {
private final static Pattern tokenPattern = Pattern.compile("\\{\\{([^}]+)\\}\\}");
private final static String PNG = "image/png";
private final static String JPG = "image/jpg";
private final static String GIF = "image/gif";
private final static String CSS = "text/css";
private final static String PDF = "application/pdf";
private final File contentRoot;
@Inject
public ContentResource(@Named("contentRoot") String contentRoot) {
this.contentRoot = new File(contentRoot);
if (!this.contentRoot.exists()) {
throw new RuntimeException(contentRoot + " doesn't exist");
}
}
@GET
@Path("{resource}")
@Produces({MediaType.TEXT_HTML, JPG, PNG, GIF, CSS, PDF})
public Response getResource(@Context UriInfo uriInfo, @PathParam("resource") String resource) {
File file;
String name = resource.endsWith("/") ? resource.substring(0, resource.length() - 1) : resource;
if (name.contains("/")) {
String[] path = name.split("/");
StringBuilder br = new StringBuilder().append(contentRoot).append("/");
for (int i = 0; i < path.length - 1; i++) {
br.append(path[i]).append("/");
}
file = new File(br.toString(), path[path.length - 1]);
} else {
file = new File(contentRoot, name);
}
return returnFile(uriInfo, file);
}
private Response returnFile(UriInfo uriInfo, File file) {
if (!file.exists()) {
return Response.status(Response.Status.NOT_FOUND).build();
}
// make sure content root is in hierarchy
{
File walk = file;
while ((walk = walk.getParentFile()) != null) {
if (walk.getAbsolutePath().equals(contentRoot.getAbsolutePath())) {
break;
}
}
// we hit the root of the filesystem
if (walk == null) {
return Response.status(Response.Status.BAD_REQUEST).entity("Bad path").build();
}
}
try {
Date fileDate = new Date(file.lastModified());
String type;
switch (Files.getFileExtension(file.getName())) {
case "md":
return parseMarkdown(uriInfo, file);
case "htm":
case "html":
return parseHtml(uriInfo, file);
case "png":
type = PNG;
break;
case "jpg":
type = JPG;
break;
case "gif":
type = GIF;
break;
case "css":
type = CSS;
break;
case "pdf":
type = PDF;
break;
// no js, this is a simple content server.
default:
return Response.status(Response.Status.BAD_REQUEST).entity("Unknown file type").build();
}
return Response.ok(new FileInputStream(file)).lastModified(fileDate).type(type).build();
} catch (IOException e) {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
/**
* Replaces resource tokens in the file with absolute url to this api.
* @param uriInfo
* @param file
* @return
* @throws IOException
*/
private Response parseHtml(UriInfo uriInfo, File file) throws IOException {
return Response.ok().entity(
parseHtml(uriInfo, Files.readLines(file, Charset.defaultCharset())))
.type(MediaType.TEXT_HTML_TYPE)
.build();
}
/**
* Converts markdown to HTML, and then parses resource tokens from the HTML.
* @param uriInfo
* @param file
* @return
* @throws IOException
*/
private Response parseMarkdown(UriInfo uriInfo, File file) throws IOException {
return Response.ok().entity(
parseHtml(uriInfo, toList(new Markdown4jProcessor().process(file))))
.type(MediaType.TEXT_HTML_TYPE)
.build();
}
private List<String> toList(String string) {
return Lists.newArrayList(string.split("\\n"));
}
private String parseHtml(UriInfo uriInfo, List<String> input) {
String baseUri = uriInfo.getBaseUri().toString() + uriInfo.getPathSegments().get(0).getPath() + "/";
StringBuilder output = new StringBuilder();
for (String line : input) {
int cursor = 0;
Matcher tokenMatcher = tokenPattern.matcher(line);
while (tokenMatcher.find()) {
int tokenStart = tokenMatcher.start();
int tokenEnd = tokenMatcher.end();
int keyStart = tokenMatcher.start(1);
int keyEnd = tokenMatcher.end(1);
output.append(line.substring(cursor, tokenStart));
String key = line.substring(keyStart, keyEnd);
// "encode" path
key = key.replace("/", "%2F");
output.append(baseUri).append(key);
cursor = tokenEnd;
}
output.append(line.substring(cursor)).append("\n");
}
return output.toString();
}
}