/**
* Copyright 2013 the original author or authors.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 io.neba.core.logviewer;
import org.apache.felix.webconsole.AbstractWebConsolePlugin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static io.neba.core.util.ZipFileUtil.toZipFileEntryName;
import static java.lang.Thread.currentThread;
import static org.apache.commons.io.IOUtils.closeQuietly;
import static org.apache.commons.io.IOUtils.copy;
import static org.apache.commons.lang.StringUtils.*;
/**
* A web console plugin for tailing and downloading the CQ log files placed within the sling log directory as configured in the
* Apache Sling Logging Configuration.
*
* @author Olaf Otto
*/
@Service
public class LogfileViewerConsolePlugin extends AbstractWebConsolePlugin {
private static final String LABEL = "logviewer";
private static final String RESOURCES_ROOT = "/META-INF/consoleplugin/logviewer";
@Autowired
private TailServlet tailServlet;
@Autowired
private LogFiles logFiles;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
final ClassLoader ccl = currentThread().getContextClassLoader();
try {
currentThread().setContextClassLoader(getClass().getClassLoader());
this.tailServlet.init(config);
} catch (RuntimeException e) {
throw new ServletException("Unable to initialize the tail servlet - the log viewer will not be available", e);
} finally {
currentThread().setContextClassLoader(ccl);
}
}
@Override
public void destroy() {
super.destroy();
this.tailServlet.destroy();
}
@SuppressWarnings("unused")
public String getCategory() {
return "NEBA";
}
/**
* This method follows a felix naming convention and is automatically used
* by felix to retrieve resources for this plugin, e.g. when retrieving script resources.
*
* @param path must not be <code>null</code>.
* @return the corresponding resource, or <code>null</code>.
*/
public URL getResource(String path) {
URL url = null;
String internalPath = substringAfter(path, "/" + getLabel());
if (startsWith(internalPath, "/static/")) {
url = getClass().getResource(RESOURCES_ROOT + internalPath);
}
return url;
}
@Override
protected void renderContent(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
writeScriptIncludes(res);
writeHead(res);
}
private void writeHead(HttpServletResponse res) throws IOException {
StringBuilder options = new StringBuilder(1024);
this.logFiles.resolveLogFiles().forEach(file ->
options.append("<option value=\"").append(file.getAbsolutePath()).append("\" ")
.append("title=\"").append(file.getAbsolutePath()).append("\">")
.append(file.getParentFile().getName()).append('/').append(file.getName())
.append("</option>"));
writeFromTemplate(res, "head.html", options.toString());
writeFromTemplate(res, "body.html");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String suffix = substringAfter(req.getRequestURI(), req.getServletPath() + "/" + getLabel());
if (!isBlank(suffix) && suffix.startsWith("/tail")) {
this.tailServlet.service(req, res);
return;
}
if (!isBlank(suffix) && suffix.equals("/download")) {
download(res, req);
return;
}
super.doGet(req, res);
}
/**
* Streams the contents of the log directory as a zip file.
*/
private void download(HttpServletResponse res, HttpServletRequest req) throws IOException {
res.setContentType("application/zip");
res.setHeader("Content-Disposition", "attachment;filename=logfiles-" + req.getServerName() + ".zip");
ZipOutputStream zos = new ZipOutputStream(res.getOutputStream());
try {
for (File file : this.logFiles.resolveLogFiles()) {
ZipEntry ze = new ZipEntry(toZipFileEntryName(file));
zos.putNextEntry(ze);
FileInputStream in = new FileInputStream(file);
try {
copy(in, zos);
zos.closeEntry();
} finally {
closeQuietly(in);
}
}
zos.finish();
} finally {
closeQuietly(zos);
}
}
private void writeFromTemplate(HttpServletResponse response, String templateName, Object... templateArgs) throws IOException {
String template = readTemplate(templateName);
response.getWriter().printf(template, templateArgs);
}
private void writeFromTemplate(HttpServletResponse response, String templateName) throws IOException {
String template = readTemplate(templateName);
response.getWriter().write(template);
}
private String readTemplate(String templateName) {
return readTemplateFile(RESOURCES_ROOT + "/templates/" + templateName);
}
private void writeScriptIncludes(HttpServletResponse response) throws IOException {
response.getWriter().write("<script src=\"" + getLabel() + "/static/script.js\"></script>");
response.getWriter().write("<script src=\"" + getLabel() + "/static/encoding-indexes.js\"></script>");
response.getWriter().write("<script src=\"" + getLabel() + "/static/encoding.js\"></script>");
}
@Override
public String getTitle() {
return "View logfiles";
}
@Override
public String getLabel() {
return LABEL;
}
}