/** * Copyright (c) 2012-2015 Edgar Espina * * This file is part of Handlebars.java. * * 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.github.jknack.handlebars.server; import static org.apache.commons.io.FilenameUtils.removeExtension; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.Writer; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; import com.fasterxml.jackson.core.JsonLocation; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser.Feature; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.jknack.handlebars.Context; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.HandlebarsError; import com.github.jknack.handlebars.HandlebarsException; import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.context.FieldValueResolver; import com.github.jknack.handlebars.context.JavaBeanValueResolver; import com.github.jknack.handlebars.context.MapValueResolver; import com.github.jknack.handlebars.helper.StringHelpers; import com.github.jknack.handlebars.server.HbsServer.Options; /** * Prepare, compile and merge handlebars templates. * * @author edgar.espina */ public class HbsServlet extends HttpServlet { /** * The default serial uid. */ private static final long serialVersionUID = 1L; /** * The logging system. */ private static final Logger logger = LoggerFactory.getLogger(HbsServlet.class); /** * The handlebars object. */ private final Handlebars handlebars; /** * The object mapper. */ private final ObjectMapper mapper = new ObjectMapper(); /** * A yaml parser. */ private final Yaml yaml = new Yaml(); /** * The server options. */ private final Options args; /** * Creates a new {@link HbsServlet}. * * @param handlebars The handlebars object. * @param args The server options. */ public HbsServlet(final Handlebars handlebars, final Options args) { this.handlebars = handlebars; this.args = args; mapper.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); mapper.configure(Feature.ALLOW_COMMENTS, true); } @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { Writer writer = null; try { Template template = handlebars.compile(removeExtension(requestURI(request))); Object model = model(request); String output = template.apply(model); response.setCharacterEncoding(args.encoding); response.setContentType(args.contentType); writer = response.getWriter(); writer.write(output); } catch (HandlebarsException ex) { handlebarsError(ex, response); } catch (JsonParseException ex) { logger.error("Unexpected error", ex); jsonError(ex, request, response); } catch (FileNotFoundException ex) { response.sendError(HttpServletResponse.SC_NOT_FOUND); } catch (IOException ex) { logger.error("Unexpected error", ex); throw ex; } catch (RuntimeException ex) { logger.error("Unexpected error", ex); throw ex; } catch (Exception ex) { logger.error("Unexpected error", ex); throw new ServletException(ex); } finally { IOUtils.closeQuietly(writer); } } /** * Attempt to load a json or yml file. * * @param request The original request. * @return The associated model. * @throws IOException If something goes wrong. */ private Object model(final HttpServletRequest request) throws IOException { String jsonFilename = jsonFilename(request); String ymlFilename = ymlFilename(request); Object data = json(jsonFilename); if (data == null) { data = yml(ymlFilename); } if (data == null) { String errorMessage = "file not found: {}"; logger.error(errorMessage, jsonFilename); logger.error(errorMessage, ymlFilename); return Collections.emptyMap(); } return data; } /** * Determines the data file to use from the requested URI or from the 'data' * HTTP parameter. * * @param request The current request. * @return The data file to use from the requested URI or from the 'data' * HTTP parameter. */ private String dataFile(final HttpServletRequest request) { String data = request.getParameter("data"); String uri = StringUtils.isEmpty(data) ? request.getRequestURI().replace(request.getContextPath(), "") : data; if (!HbsServer.CONTEXT.equals(args.prefix)) { uri = args.prefix + uri; } if (!uri.startsWith("/")) { uri = "/" + uri; } return uri; } /** * Remove context path from the request's URI. * * @param request The current request. * @return Same as {@link HttpServletRequest#getRequestURI()} without context * path. */ private String requestURI(final HttpServletRequest request) { String requestURI = request.getRequestURI().replace(request.getContextPath(), ""); return requestURI; } /** * Deal with a {@link HandlebarsException}. * * @param ex The handlebars exception. * @param response The http response. * @throws IOException If something goes wrong. */ private void handlebarsError(final HandlebarsException ex, final HttpServletResponse response) throws IOException { HandlebarsError error = ex.getError(); int firstLine = 1; if (error != null) { if (ex.getCause() != null) { firstLine = error.line; } else { firstLine = Math.max(1, error.line - 1); } } fancyError(ex, firstLine, "Xml", response); } /** * Deal with a {@link HandlebarsException}. * * @param ex The handlebars exception. * @param request The http request. * @param response The http response. * @throws IOException If something goes wrong. */ private void jsonError(final JsonParseException ex, final HttpServletRequest request, final HttpServletResponse response) throws IOException { Map<String, Object> root = new HashMap<String, Object>(); Map<String, Object> error = new HashMap<String, Object>(); String filename = jsonFilename(request); JsonLocation location = ex.getLocation(); String reason = ex.getMessage(); int atIdx = reason.lastIndexOf(" at "); if (atIdx > 0) { reason = reason.substring(0, atIdx); } error.put("filename", filename); error.put("line", location.getLineNr()); error.put("column", location.getColumnNr()); error.put("reason", reason); error.put("type", "JSON error"); String json = read(filename); StringBuilder evidence = new StringBuilder(); int i = (int) location.getCharOffset(); int nl = 0; while (i >= 0 && nl < 2) { char ch = json.charAt(i); if (ch == '\n') { nl++; } evidence.insert(0, ch); i--; } i = (int) location.getCharOffset() + 1; nl = 0; while (i < json.length() && nl < 2) { char ch = json.charAt(i); if (ch == '\n') { nl++; } evidence.append(ch); i++; } error.put("evidence", evidence); root.put("error", error); int firstLine = Math.max(1, ex.getLocation().getLineNr() - 1); fancyError(root, firstLine, "JScript", response); } /** * Deal with a fancy errors. * * @param error An error. * @param firstLine The first line to report. * @param lang The lang to use. * @param response The http response. * @throws IOException If something goes wrong. */ private void fancyError(final Object error, final int firstLine, final String lang, final HttpServletResponse response) throws IOException { Handlebars handlebars = new Handlebars(); StringHelpers.register(handlebars); Template template = handlebars.compile("/error-pages/error"); PrintWriter writer = null; writer = response.getWriter(); template.apply( Context .newBuilder(error) .resolver(MapValueResolver.INSTANCE, FieldValueResolver.INSTANCE, JavaBeanValueResolver.INSTANCE) .combine("lang", lang) .combine("version", HbsServer.version) .combine("firstLine", firstLine).build() , writer); IOUtils.closeQuietly(writer); } /** * Try to load a <code>json</code> file that matches the given request. * * @return The associated data. * @throws IOException If the file isn't found. * @param filename the filename to read */ private Object json(final String filename) throws IOException { try { String json = read(filename); if (json.trim().startsWith("[")) { return mapper.readValue(json, List.class); } return mapper.readValue(json, Map.class); } catch (FileNotFoundException ex) { return null; } } /** * Try to load a <code>yml</code> file that matches the given request. * * @return A yaml map. * @throws IOException If the file isn't found. * @param filename the filename to read */ private Object yml(final String filename) throws IOException { try { String yml = read(filename); Object data = yaml.load(yml); return data; } catch (FileNotFoundException ex) { return null; } } /** * Construct the filename to parse json data from. * @param request the current request * @return filename to load json from */ private String jsonFilename(final HttpServletRequest request) { return dataFilename(request, ".json"); } /** * Construct the filename to parse yml data from. * @param request the current request * @return filename to load yml from */ private String ymlFilename(final HttpServletRequest request) { return dataFilename(request, ".yml"); } /** * Construct the filename to parse data from. * @param request the current request * @param extension the file extension to use, e.g. ".json" * @return filename to load data from */ private String dataFilename(final HttpServletRequest request, final String extension) { return removeExtension(dataFile(request)) + extension; } /** * Read a file from the servlet context. * * @param uri The requested file. * @return The string content. * @throws IOException If the file is not found. */ private String read(final String uri) throws IOException { InputStream input = null; try { input = getServletContext().getResourceAsStream(uri); if (input == null) { throw new FileNotFoundException(args.dir + uri); } return IOUtils.toString(input); } finally { IOUtils.closeQuietly(input); } } @Override protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }