blob: 5b5d69af9df3361309bfd05484e8b501607a9ad8 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2010, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.rhelp.core.http;
import static org.eclipse.statet.internal.rhelp.core.RHelpWebapp.CAT_DOC;
import static org.eclipse.statet.internal.rhelp.core.RHelpWebapp.CAT_LIBRARY;
import static org.eclipse.statet.internal.rhelp.core.RHelpWebapp.IMAGES;
import static org.eclipse.statet.internal.rhelp.core.RHelpWebapp.LIBRARY_DOC;
import static org.eclipse.statet.internal.rhelp.core.RHelpWebapp.LIBRARY_HELP;
import static org.eclipse.statet.internal.rhelp.core.RHelpWebapp.LIBRARY_HTML;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.runtime.CommonsRuntime;
import org.eclipse.statet.jcommons.runtime.bundle.BundleEntry;
import org.eclipse.statet.jcommons.runtime.bundle.BundleSpec;
import org.eclipse.statet.jcommons.status.Status;
import org.eclipse.statet.jcommons.status.StatusException;
import org.eclipse.statet.internal.rhelp.core.REnvHelpImpl;
import org.eclipse.statet.internal.rhelp.core.REnvHelpIndex;
import org.eclipse.statet.internal.rhelp.core.RHelpWebapp;
import org.eclipse.statet.internal.rhelp.core.RHelpWebapp.RequestInfo;
import org.eclipse.statet.internal.rhelp.core.http.HttpHeaderUtils;
import org.eclipse.statet.internal.rhelp.core.index.RHelpHtmlUtils;
import org.eclipse.statet.internal.rhelp.core.server.ServerClientSupport;
import org.eclipse.statet.rhelp.core.DocResource;
import org.eclipse.statet.rhelp.core.REnvHelp;
import org.eclipse.statet.rhelp.core.REnvHelpConfiguration;
import org.eclipse.statet.rhelp.core.RHelpCore;
import org.eclipse.statet.rhelp.core.RHelpManager;
import org.eclipse.statet.rhelp.core.RHelpPage;
import org.eclipse.statet.rhelp.core.RHelpTopicEntry;
import org.eclipse.statet.rhelp.core.RPkgHelp;
import org.eclipse.statet.rhelp.core.TopicDocResource;
import org.eclipse.statet.rj.renv.core.REnv;
import org.eclipse.statet.rj.renv.core.REnvConfiguration;
import org.eclipse.statet.rj.renv.core.RLibLocation;
import org.eclipse.statet.rj.renv.core.RPkgDescription;
/**
* Abstract R help servlet.
*/
@NonNullByDefault
public abstract class RHelpHttpServlet extends HttpServlet {
private static final long serialVersionUID= 1L;
private static final String PACKAGE_INDEX_PAGE_NAME= "00Index"; //$NON-NLS-1$
private static final String ATTR_RENV_ID= "rhelp.renv.id"; //$NON-NLS-1$
private static final String ATTR_RENV_RESOLVED= "rhelp.renv.resolved"; //$NON-NLS-1$
private static final String ATTR_RENV_HELP= "rhelp.renv.help"; //$NON-NLS-1$
private static final String ATTR_RENV_CONFIG= "rhelp.renv.config"; //$NON-NLS-1$
private static final MediaTypeProvider DOC_MEDIA_TYPES;
static {
final CustomMediaTypeProvider docMediaTypes= new CustomMediaTypeProvider();
docMediaTypes.addName("README", "text/plain;charset=iso-8859-1"); //$NON-NLS-1$ //$NON-NLS-2$
docMediaTypes.addName("COPYING", "text/plain;charset=iso-8859-1"); //$NON-NLS-1$ //$NON-NLS-2$
docMediaTypes.addName("LICENSE", "text/plain;charset=iso-8859-1"); //$NON-NLS-1$ //$NON-NLS-2$
docMediaTypes.addName("AUTHORS", "text/plain;charset=iso-8859-1"); //$NON-NLS-1$ //$NON-NLS-2$
docMediaTypes.addName("THANKS", "text/plain;charset=iso-8859-1"); //$NON-NLS-1$ //$NON-NLS-2$
docMediaTypes.addName("DESCRIPTION", "text/plain"); //$NON-NLS-1$ //$NON-NLS-2$
docMediaTypes.addExt("Rnw", "text/plain"); //$NON-NLS-1$ //$NON-NLS-2$
DOC_MEDIA_TYPES= docMediaTypes;
}
@SuppressWarnings("null")
private static String getREnvId(final HttpServletRequest req) {
return (String) req.getAttribute(ATTR_RENV_ID);
}
@SuppressWarnings("null")
private static REnv getREnv(final HttpServletRequest req) {
return (REnv) req.getAttribute(ATTR_RENV_RESOLVED);
}
@SuppressWarnings("null")
private static REnvHelpImpl getREnvHelp(final HttpServletRequest req) {
return (REnvHelpImpl) req.getAttribute(ATTR_RENV_HELP);
}
@SuppressWarnings("null")
private static REnvHelpConfiguration getREnvConfig(final HttpServletRequest req) {
return (REnvHelpConfiguration) req.getAttribute(ATTR_RENV_CONFIG);
}
private static void printSaveHtml(final Writer writer, final String s) throws IOException {
final int length= s.length();
int next= 0;
for (int i= 0; i < length; ) {
final char c= s.charAt(i);
switch (c) {
case '"':
if (i > next) {
writer.write(s, next, i - next);
}
writer.write("&quot;"); //$NON-NLS-1$
next= ++i;
continue;
case '&':
if (i > next) {
writer.write(s, next, i - next);
}
writer.write("&amp;"); //$NON-NLS-1$
next= ++i;
continue;
case '\'':
if (i > next) {
writer.write(s, next, i - next);
}
// &apos; see http://www.w3.org/TR/xhtml1/#C_16
writer.write("&#39;"); //$NON-NLS-1$
next= ++i;
continue;
case '<':
if (i > next) {
writer.write(s, next, i - next);
}
writer.write("&lt;"); //$NON-NLS-1$
next= ++i;
continue;
case '>':
if (i > next) {
writer.write(s, next, i - next);
}
writer.write("&gt;"); //$NON-NLS-1$
next= ++i;
continue;
default:
i++;
continue;
}
}
if (length > next) {
writer.write(s, next, length - next);
}
}
private RHelpManager rHelpManager;
private ResourceHandler fileResourceHandler;
private @Nullable HttpForwardHandler serverForwardHandler;
private final Map<String, byte[]> images= new HashMap<>();
@SuppressWarnings("null")
public RHelpHttpServlet() {
}
protected void init(final RHelpManager rHelpManager,
final @Nullable ResourceHandler fileResourceHandler,
final @Nullable HttpForwardHandler serverForwardHandler) {
this.rHelpManager= rHelpManager;
this.fileResourceHandler= (fileResourceHandler != null) ? fileResourceHandler :
new SimpleResourceHandler(new ServletMediaTypeProvider(getServletContext()));
this.fileResourceHandler.setSpecialMediaTypes(DOC_MEDIA_TYPES);
this.fileResourceHandler.setCacheControl("max-age=600, must-revalidate"); //$NON-NLS-1$
this.serverForwardHandler= serverForwardHandler;
loadImages();
}
@Override
public void init(final ServletConfig config) throws ServletException {
super.init(config);
}
@Override
public void destroy() {
super.destroy();
}
private void loadImages() {
final List<String> names= ImCollections.newList(
"rpackage.png", //$NON-NLS-1$
"rhelp-page.png", "rhelp-page-internal.png", //$NON-NLS-1$ //$NON-NLS-2$
"rhelp-topic.png", "rhelp-topic-internal.png" ); //$NON-NLS-1$ //$NON-NLS-2$
try {
final List<BundleEntry> bundles= CommonsRuntime.getEnvironment().resolveBundles(
ImCollections.newList(new BundleSpec(RHelpCore.BUNDLE_ID, RHelpCore.class)) );
ITER_IMAGES: for (final String name : names) {
final String resourcePathString= "images/" + name; //$NON-NLS-1$
for (final BundleEntry bundle : bundles) {
final Path resourcePath= bundle.getResourcePath(resourcePathString);
if (resourcePath != null && Files.isReadable(resourcePath)) {
final byte[] bytes= Files.readAllBytes(resourcePath);
this.images.put(name, bytes);
continue ITER_IMAGES;
}
}
throw new IOException(String.format("File note found '%1$s'.", resourcePathString));
}
}
catch (final Exception e) {
log("An error occurred when loading images.", e);
this.images.clear();
}
}
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
try {
final String path= req.getPathInfo();
if (path != null) {
if (path.startsWith('/' + IMAGES + '/')) {
processImage(path.substring(IMAGES.length() + 2), req, resp);
return;
}
if (path.endsWith("/R.css")) { //$NON-NLS-1$
processCss(req, resp);
return;
}
final RequestInfo info= RHelpWebapp.extractRequestInfo(path);
if (info != null) {
if (!checkREnv(info.rEnvId, req, resp)) {
return;
}
if (info.cat == null) {
printEnvIndex(req, resp);
return;
}
else if (info.cat == RHelpWebapp.CAT_LIBRARY) {
switch (info.cmd) {
case RHelpWebapp.PKGCMD_IDX:
processPkgIndex(info.pkgName, req, resp);
return;
case RHelpWebapp.PKGCMD_HTML_PAGE:
processHelpPage(info.pkgName, info.detail, req, resp);
return;
case RHelpWebapp.PKGCMD_HTML_RESOURCE:
processPkgRes(info.pkgName, LIBRARY_HELP, info.detail, req, resp);
return;
case RHelpWebapp.PKGCMD_TOPIC:
processTopic(info.pkgName, info.detail, req, resp);
return;
case RHelpWebapp.PKGCMD_DOC_IDX:
processPkgRes(info.pkgName, LIBRARY_DOC, "index.html", req, resp); //$NON-NLS-1$
return;
case RHelpWebapp.PKGCMD_DOC_RES:
processPkgRes(info.pkgName, LIBRARY_DOC, info.detail, req, resp);
return;
case RHelpWebapp.PKGCMD_DESCRIPTION_RES:
processPkgRes(info.pkgName, null, "DESCRIPTION", req, resp); //$NON-NLS-1$
return;
}
}
else if (info.cat == RHelpWebapp.CAT_DOC) {
processEnvDoc(info.detail, req, resp);
return;
}
}
}
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
catch (final StatusException e) {
final Status status= e.getStatus();
final int httpStatus;
switch (status.getCode()) {
case REnvHelpIndex.TIMEOUT_ERROR:
case REnvHelpIndex.CONNECT_ERROR:
httpStatus= HttpServletResponse.SC_GATEWAY_TIMEOUT;
break;
default:
httpStatus= HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
break;
}
resp.sendError(httpStatus, status.getMessage());
return;
}
finally {
final REnvHelp help= (REnvHelp) req.getAttribute(ATTR_RENV_HELP);
if (help != null) {
help.unlock();
}
}
}
protected StringBuilder getServletPath(final HttpServletRequest req) {
return new StringBuilder()
.append(req.getContextPath())
.append(req.getServletPath());
}
protected void sendPathRedirect(final String path,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
// resp.sendRedirect(path.toString()); // converts relativ path to absolute
resp.resetBuffer();
resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
resp.setHeader(HttpHeaderUtils.LOCATION_NAME, path);
}
private boolean checkREnv(final String id,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
REnv rEnv= this.rHelpManager.getREnv(id);
if (rEnv != null) {
rEnv= rEnv.resolve();
}
final REnvHelpConfiguration config;
if (rEnv != null && (config= rEnv.get(REnvHelpConfiguration.class)) != null) {
req.setAttribute(ATTR_RENV_ID, id);
req.setAttribute(ATTR_RENV_RESOLVED, rEnv);
final REnvHelp help= this.rHelpManager.getHelp(rEnv);
if (help != null) {
req.setAttribute(ATTR_RENV_HELP, help);
req.setAttribute(ATTR_RENV_CONFIG, config);
return true;
}
resp.sendError(HttpServletResponse.SC_NOT_FOUND,
"The R library of the requested R environment <code>" + rEnv.getName() + "</code> " +
"is not yet indexed. Please run the indexer first to enable R help support.");
return false;
}
else {
final String message= (id.startsWith("default-")) ? //$NON-NLS-1$
"The requested default R environment is missing. " +
"Please configure an environment as default." :
"The requested R environment doesn't exist. " +
"Please change the environment.";
resp.sendError(HttpServletResponse.SC_NOT_FOUND, message);
return false;
}
}
private void processHelpPage(final String pkgName, final String detail,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException,
StatusException {
if (detail != null && detail.equalsIgnoreCase(PACKAGE_INDEX_PAGE_NAME)) {
sendPathRedirect(getServletPath(req)
.append('/').append(getREnvId(req))
.append('/' + CAT_LIBRARY +
'/').append(pkgName).append('/')
.toString(), req, resp );
return;
}
final REnvHelpImpl help= getREnvHelp(req);
final RPkgHelp pkgHelp= help.getPkgHelp(pkgName);
if (pkgHelp != null) {
final String qs= req.getParameter(RHelpWebapp.PAR_QUERY_STRING);
String html= help.getHtmlPage(pkgHelp, detail, qs);
if (html != null) {
if (qs != null) {
html= RHelpHtmlUtils.formatHtmlMatches(html);
}
printHtmlPage(html, req, resp);
return;
}
final RHelpPage page= pkgHelp.getPageForTopic(detail);
if (page != null) {
redirect(page, req, resp);
return;
}
}
resp.sendError(HttpServletResponse.SC_NOT_FOUND,
"Help page <code>" + detail + "</code> {<code>" + pkgName + "</code>} not found.");
return;
}
private void processPkgRes(final String pkgName, final @Nullable String resSub, final String path,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException {
final REnvHelpImpl help= getREnvHelp(req);
final REnvHelpConfiguration rEnvConfig= getREnvConfig(req);
Path libDirectory= null;
if (rEnvConfig.isLocal()) {
final RPkgHelp pkgHelp= help.getPkgHelp(pkgName);
if (pkgHelp != null) {
final RPkgDescription pkgDescription= pkgHelp.getPkgDescription();
final RLibLocation libLocation= pkgDescription.getLibLocation();
libDirectory= libLocation.getDirectoryPath();
}
}
else {
switch (rEnvConfig.getStateSharedType()) {
case REnvConfiguration.SHARED_SERVER:
forwardToServer(rEnvConfig, req, resp);
return;
case REnvConfiguration.SHARED_DIRECTORY:
// TODO
break;
default:
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
}
if (libDirectory == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path directory= libDirectory.resolve(pkgName);
if (resSub != null) {
directory= directory.resolve(resSub);
}
serveFileResource(directory, path, null, req, resp);
}
private void processPkgIndex(final String pkgName,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException,
StatusException {
final REnvHelpImpl help= getREnvHelp(req);
final RPkgHelp pkgHelp= help.getPkgHelp(pkgName);
if (pkgHelp != null) {
final ImList<RHelpTopicEntry> topics= pkgHelp.getTopics();
if (topics != null) {
printPackageIndex(pkgHelp, topics, req, resp);
return;
}
}
resp.sendError(HttpServletResponse.SC_NOT_FOUND,
"Help for package <code>" + pkgName + "</code> not found.");
return;
}
private void processTopic(final String pkgName, final String detail,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException,
StatusException {
final REnvHelp help= getREnvHelp(req);
final RPkgHelp pkgHelp= help.getPkgHelp(pkgName);
if (pkgHelp != null) {
final RHelpPage page= pkgHelp.getPageForTopic(detail);
if (page != null) {
redirect(page, req, resp);
return;
}
}
final List<RHelpPage> pages= help.getPagesForTopic(detail, null);
if (pages.size() == 1) {
redirect(pages.get(0), req, resp);
return;
}
else {
printTopicList(detail, pages, req, resp);
return;
}
}
private void processCss(
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final PrintWriter writer= createCssDoc(req, resp);
writer.println("span.acronym { font-size: small }\n" + //$NON-NLS-1$
"span.env { font-family: monospace }\n" + //$NON-NLS-1$
"span.file { font-family: monospace }\n" + //$NON-NLS-1$
"span.option { font-family: monospace }\n" + //$NON-NLS-1$
"span.pkg { font-weight: bold }\n" + //$NON-NLS-1$
"span.samp { font-family: monospace }"); //$NON-NLS-1$
writer.println("body { line-height: 125%; margin: 1em; padding: 0; color: black; }"); //$NON-NLS-1$
writer.println("table { margin: 0.4em 0 0.4em 0; border-collapse:collapse; border:0px; font-size: 100% }"); //$NON-NLS-1$
writer.println("td { padding: 0.2em 0.8em 0.2em 0; border:0px; }"); //$NON-NLS-1$
writer.println("h2 { font-size: 120%; font-weight: bold; margin: 0 0 0.6em 0; }"); //$NON-NLS-1$
writer.println("h3 { font-size: 110%; font-weight: bold; letter-spacing: 0.05em; margin: 1.0em 0 0.6em 0; }"); //$NON-NLS-1$
writer.println("p, pre { margin: 0.6em 0 0.6em 0; }"); //$NON-NLS-1$
writer.println("td { vertical-align: top; }"); //$NON-NLS-1$
writer.println("hr { margin-top: 0.8em; clear: both; }"); //$NON-NLS-1$
writer.println("div.toc { display: none; font-size: 80%; line-height: 125%; padding: 0.2em 0.8em 0.4em; }"); //$NON-NLS-1$
writer.println("div.toc ul { list-style: none; padding: 0; margin: 0 }"); //$NON-NLS-1$
writer.println("div.toc pre { margin: 0 0 0.4em; }"); //$NON-NLS-1$
writer.println("div.toc a { text-decoration: none; color: black; }"); //$NON-NLS-1$
writer.println("div.toc a:visited { text-decoration: none; color: black; }"); //$NON-NLS-1$
writer.println("a.action { text-decoration: none; }"); //$NON-NLS-1$
writer.println("a.action small { padding-left: 1px; padding-right: 1px; }"); //$NON-NLS-1$
writer.println("a.action:hover small { background-color: lightgrey; color: black; }"); //$NON-NLS-1$
writer.println("img.icon { vertical-align: text-top; padding-top: 1px; padding-right: 2px; margin-right: 2px; }"); //$NON-NLS-1$
customizeCss(writer);
}
private void processImage(final String imgName,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final byte[] content= this.images.get(imgName);
if (content != null) {
resp.setContentType("image/png"); //$NON-NLS-1$
resp.setHeader("Cache-Control", "max-age=600, public"); //$NON-NLS-1$
final ServletOutputStream out= resp.getOutputStream();
out.write(content);
out.close();
return;
}
else {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
}
private void processEnvDoc(final String path,
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
final REnvHelpImpl help= getREnvHelp(req);
final REnvHelpConfiguration rEnvConfig= getREnvConfig(req);
Path directory= null;
if (rEnvConfig.isLocal()) {
final String docDir= help.getDocDir();
if (docDir != null) {
directory= Path.of(docDir);
}
}
else {
switch (rEnvConfig.getStateSharedType()) {
case REnvConfiguration.SHARED_SERVER:
forwardToServer(rEnvConfig, req, resp);
return;
case REnvConfiguration.SHARED_DIRECTORY:
// TODO
break;
default:
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
}
if (directory == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "R doc directory not found.");
return;
}
serveFileResource(directory, path, req.getParameter(RHelpWebapp.PAR_ACTION), req, resp);
}
private PrintWriter createHtmlDoc(final String title,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=UTF-8"); //$NON-NLS-1$
resp.setHeader("Cache-Control", "max-age=30, must-revalidate"); //$NON-NLS-1$ //$NON-NLS-2$
final PrintWriter writer= resp.getWriter();
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$
writer.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); //$NON-NLS-1$
writer.println("<html><head>"); //$NON-NLS-1$
writer.write("<title>"); //$NON-NLS-1$
printSaveHtml(writer, title);
writer.write("</title>"); //$NON-NLS-1$
writer.write("<link rel=\"stylesheet\" type=\"text/css\" href=\""); //$NON-NLS-1$
writer.write(getServletPath(req)
.append("/R.css") //$NON-NLS-1$
.toString() );
writer.println("\"/>"); //$NON-NLS-1$
return writer;
}
private void redirect(final RHelpPage page,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
sendPathRedirect(getServletPath(req)
.append('/').append(getREnvId(req))
.append('/' + CAT_LIBRARY +
'/').append(page.getPackage().getName())
.append('/' + LIBRARY_HTML +
'/').append(page.getName()).append(".html") //$NON-NLS-1$
.toString(), req, resp );
}
private PrintWriter createCssDoc(
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
resp.setContentType("text/css;charset=UTF-8"); //$NON-NLS-1$
final PrintWriter writer= resp.getWriter();
writer.println("@charset \"UTF-8\";"); //$NON-NLS-1$
return writer;
}
private void printHtmlPage(final String html,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=UTF-8"); //$NON-NLS-1$
resp.setHeader("Cache-Control", "max-age=30, must-revalidate"); //$NON-NLS-1$ //$NON-NLS-2$
final PrintWriter writer= resp.getWriter();
final int idxHead= html.indexOf("</head>"); //$NON-NLS-1$
if (idxHead > 0) {
writer.write(html, 0, idxHead);
customizePageHtmlHeader(req, writer);
int idxEndExamples= html.lastIndexOf(RHelpWebapp.HTML_END_EXAMPLES);
if (idxEndExamples > 0) {
final int idxBeginExamples= html.lastIndexOf(RHelpWebapp.HTML_BEGIN_EXAMPLES, idxEndExamples);
writer.write(html, idxHead, idxBeginExamples - idxHead);
customizeExamples(writer, html.substring(
idxBeginExamples + RHelpWebapp.HTML_BEGIN_EXAMPLES.length(), idxEndExamples));
idxEndExamples+= RHelpWebapp.HTML_END_EXAMPLES.length();
writer.write(html, idxEndExamples, html.length() - idxEndExamples);
}
else {
writer.write(html, idxHead, html.length() - idxHead);
}
}
else {
writer.write(html);
}
}
private void printTopicList(final String topic, final List<RHelpPage> pages,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final PrintWriter writer= createHtmlDoc(String.format("Help on topic '%1$s'", topic),
req, resp );
final String codeTopic= "<code>" + topic + "</code>"; //$NON-NLS-1$ //$NON-NLS-2$
customizeIndexHtmlHeader(req, writer);
writer.println("</head><body>"); //$NON-NLS-1$
writer.write("<h2>"); //$NON-NLS-1$
writer.write(String.format("Help on topic %1$s", codeTopic));
writer.write("</h2>"); //$NON-NLS-1$
final String basePath= "../../"; //$NON-NLS-1$
final String baseImagesPath= "../../../" + IMAGES + '/';
if (pages != null && !pages.isEmpty()) {
writer.write("<p>"); //$NON-NLS-1$
writer.write(String.format("Help on topic %1$s was found in the following pages:", codeTopic));
writer.println("</p>"); //$NON-NLS-1$
Collections.sort(pages);
writer.write("<table>"); //$NON-NLS-1$
for (final RHelpPage page : pages) {
writer.write("<tr><td style=\"white-space: nowrap;\">"); //$NON-NLS-1$
printIcon(writer, baseImagesPath,
(page.isInternal()) ? "rhelp-topic-internal.png" : "rhelp-topic.png" ); //$NON-NLS-1$ //$NON-NLS-2$
writer.write("<a href=\""); //$NON-NLS-1$
writer.write(basePath);
writer.write(page.getPackage().getName());
writer.write('/' + LIBRARY_HTML + '/');
writer.write(page.getName());
writer.write(".html" + "\"><code>"); //$NON-NLS-1$
writer.write(page.getName());
writer.write("</code></a> {"); //$NON-NLS-1$
writer.write("<a href=\""); //$NON-NLS-1$
writer.write(basePath);
writer.write(page.getPackage().getName());
writer.write("/" + "\" title=\""); //$NON-NLS-1$
writer.write(page.getPackage().getName());
writer.write(" ["); //$NON-NLS-1$
writer.write(page.getPackage().getVersion().toString());
writer.write("]\n"); //$NON-NLS-1$
printSaveHtml(writer, page.getPackage().getTitle());
writer.write("\"><code>"); //$NON-NLS-1$
writer.write(page.getPackage().getName());
writer.write("</code></a>}</td>"); //$NON-NLS-1$
writer.write("<td>"); //$NON-NLS-1$
printSaveHtml(writer, page.getTitle());
writer.write("</td></tr>"); //$NON-NLS-1$
}
writer.write("</table>"); //$NON-NLS-1$
}
else {
writer.write(String.format("No help found on topic %1$s in any package in the R library.", codeTopic));
}
writer.println("</body></html>"); //$NON-NLS-1$
}
private void printPackageIndex(final RPkgHelp pkgHelp, final List<RHelpTopicEntry> packageTopics,
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final RPkgDescription pkgDescription= pkgHelp.getPkgDescription();
final List<TopicDocResource> vignettes= ImCollections.emptyList();
final boolean showInternal= getShowInternal();
final PrintWriter writer= createHtmlDoc(String.format("Package '%1$s' - %2$s", pkgHelp.getName(), pkgHelp.getTitle()), req,
resp );
customizeIndexHtmlHeader(req, writer);
writer.println("</head><body>"); //$NON-NLS-1$
writer.write("<table class=\"header\"><tr><td>"); //$NON-NLS-1$
writer.write(pkgHelp.getName());
writer.write(" ["); //$NON-NLS-1$
writer.write(pkgHelp.getVersion().toString());
writer.write("]"); //$NON-NLS-1$
writer.println("</td></tr></table>"); //$NON-NLS-1$
writer.println("<div class=\"toc\"><ul>"); //$NON-NLS-1$
// writer.println("<li><a href=\"#description\">Description</a></li>");
writer.write("<li><a href=\"#topics\">Help Topics</a><pre>"); //$NON-NLS-1$
TOC: for (int i= 'A', j= 0; i <= 'Z'; i++) {
if ((i - 'A') % 7 == 0) {
writer.println();
}
writer.print(' ');
for (; j < packageTopics.size(); j++) {
final RHelpTopicEntry topic= packageTopics.get(j);
if (topic.getTopic().length() > 0) {
final char c= Character.toUpperCase(topic.getTopic().charAt(0));
if (c >= 'A' && c <= 'Z') {
if (c > i) {
break;
}
if (c == i && (showInternal || !topic.getPage().isInternal())) {
writer.write("<a href=\"#idx"); //$NON-NLS-1$
writer.print((char) (32 + c)); // lowercase
writer.write("\" class=\"mnemonic\">"); //$NON-NLS-1$
writer.print(c);
writer.write("</a>"); //$NON-NLS-1$
continue TOC;
}
}
}
}
writer.print((char) i);
}
writer.println("</pre></li>"); //$NON-NLS-1$
if (!vignettes.isEmpty()) {
writer.println("<li><a href=\"#vignettes\">Other Documentation</a></li>"); //$NON-NLS-1$
}
writer.println("<li><a href=\"#about\">About</a></li>"); //$NON-NLS-1$
writer.println("</ul></div>"); //$NON-NLS-1$
writer.write("<h2>"); //$NON-NLS-1$
printSaveHtml(writer, pkgHelp.getTitle());
writer.write("</h2>"); //$NON-NLS-1$
if (pkgDescription != null) {
final String description= pkgDescription.getDescription();
if (description.length() > 0) {
writer.write("<h3 id=\"description\">Description</h3>"); //$NON-NLS-1$
writer.write("<p>"); //$NON-NLS-1$
printSaveHtml(writer, description);
if (description.charAt(description.length() - 1) != '.') {
writer.print('.');
}
writer.write("</p>"); //$NON-NLS-1$
}
}
writer.write("<h3 id=\"topics\">Help Topics</h3>"); //$NON-NLS-1$
writer.write("<table>"); //$NON-NLS-1$
final String baseTopicsPath= LIBRARY_HTML + '/';
final String baseImagesPath= "../../../" + IMAGES + '/';
int lastChar= 0;
for (final RHelpTopicEntry topic : packageTopics) {
final RHelpPage page= topic.getPage();
if (showInternal || !page.isInternal()) {
final String alias= topic.getTopic();
writer.write("<tr><td style=\"white-space: nowrap;\">"); //$NON-NLS-1$
writer.write("<a href=\""); //$NON-NLS-1$
writer.write(baseTopicsPath);
writer.write(page.getName());
writer.write(".html"); //$NON-NLS-1$
writer.write('"');
if (alias.length() > 0) {
final char c= Character.toUpperCase(alias.charAt(0));
if (c >= 'A' && c <= 'Z' && c > lastChar) {
lastChar= c;
writer.write(" id=\"idx"); //$NON-NLS-1$
writer.print((char) (32 + c)); // lowercase
writer.print('"');
}
}
writer.write(" title=\""); //$NON-NLS-1$
writer.write(page.getName());
writer.write(" {"); //$NON-NLS-1$
writer.write(pkgHelp.getName());
writer.write("}\n"); //$NON-NLS-1$
printSaveHtml(writer, page.getTitle());
writer.write("\">"); //$NON-NLS-1$
printIcon(writer, baseImagesPath,
((page.isInternal()) ? "rhelp-topic-internal.png" : "rhelp-topic.png") ); //$NON-NLS-1$ //$NON-NLS-2$
writer.write("<code>"); //$NON-NLS-1$
writer.write(alias);
writer.write("</code></a>"); //$NON-NLS-1$
writer.write("</td><td>"); //$NON-NLS-1$
printSaveHtml(writer, page.getTitle());
writer.write("</td></tr>"); //$NON-NLS-1$
}
}
writer.write("</table>"); //$NON-NLS-1$
if (!vignettes.isEmpty()) {
writer.write("<h3 id=\"vignettes\">Vignettes and Other Documentation</h3>"); //$NON-NLS-1$
}
writer.write("<h3 id=\"about\">About</h3>"); //$NON-NLS-1$
if (pkgDescription != null) {
writer.write("<table>");
if (pkgDescription.getAuthor() != null && pkgDescription.getAuthor().length() > 0) {
writer.write("<tr><td>Author(s):</td>");
writer.write("<td>"); //$NON-NLS-1$
printSaveHtml(writer, pkgDescription.getAuthor());
writer.write("</td>"); //$NON-NLS-1$
}
if (pkgDescription.getMaintainer() != null && pkgDescription.getMaintainer().length() > 0) {
writer.write("<tr><td>Maintainer:</td>"); //$NON-NLS-1$
writer.write("<td>"); //$NON-NLS-1$
printSaveHtml(writer, pkgDescription.getMaintainer());
writer.write("</td>"); //$NON-NLS-1$
}
final ImList<String> urls= pkgDescription.getUrls();
if (!urls.isEmpty()) {
writer.write("<tr><td>URL:</td>"); //$NON-NLS-1$
writer.write("<td>"); //$NON-NLS-1$
for (final Iterator<String> iter= urls.iterator();;) {
final String url= iter.next();
writer.write("<a href=\""); //$NON-NLS-1$
printSaveHtml(writer, url);
writer.write("\"><code>"); //$NON-NLS-1$
printSaveHtml(writer, url);
writer.write("</code></a>"); //$NON-NLS-1$
if (iter.hasNext()) {
writer.write(", "); //$NON-NLS-1$
continue;
}
else {
break;
}
}
writer.write("</td>"); //$NON-NLS-1$
}
writer.write("</table>"); //$NON-NLS-1$
}
writer.write("<p><a href=\"description\">DESCRIPTION file</a></p>"); //$NON-NLS-1$
writer.println("</body></html>"); //$NON-NLS-1$
}
private void printEnvIndex(
final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final REnvHelpImpl help= getREnvHelp(req);
final REnvHelpConfiguration rEnvConfig= getREnvConfig(req);
final REnv rEnv= help.getREnv();
final List<RPkgHelp> packages= help.getPkgs();
final PrintWriter writer= createHtmlDoc(String.format("R Environment '%1$s'", rEnv.getName()), req,
resp );
final String basePath= getServletPath(req)
.append('/').append(rEnv.getId())
.toString();
final String baseLibPath= basePath + '/' + CAT_LIBRARY + '/';
final String baseDocPath= basePath + '/' + CAT_DOC + '/';
final String baseImagesPath= basePath + "../../" + IMAGES + '/'; //$NON-NLS-1$
customizeIndexHtmlHeader(req, writer);
writer.println("</head><body>"); //$NON-NLS-1$
// toc
writer.println("<div class=\"toc\"><ul>"); //$NON-NLS-1$
writer.write("<li><a href=\"#manuals\">Manuals</a></li>"); //$NON-NLS-1$
writer.write("<li><a href=\"#packages\">Packages</a><pre>"); //$NON-NLS-1$
TOC: for (int i= 'A', j= 0; i <= 'Z'; i++) {
if ((i - 'A') % 7 == 0) {
writer.println();
}
writer.print(' ');
String name;
while (j < packages.size() &&
(name= packages.get(j).getName()) != null && name.length() > 0) {
final char c= Character.toUpperCase(name.charAt(0));
if (c >= 'A' && c <= 'Z') {
if (c > i) {
break;
}
if (c == i) {
writer.write("<a href=\"#idx"); //$NON-NLS-1$
writer.print((char) (32 + c)); // lowercase
writer.write("\" class=\"mnemonic\">"); //$NON-NLS-1$
writer.print(c);
writer.write("</a>"); //$NON-NLS-1$
continue TOC;
}
}
j++;
}
writer.print((char) i);
}
writer.println("</pre></li>"); //$NON-NLS-1$
if (!help.getMiscResources().isEmpty()) {
writer.write("<li><a href=\"#misc\">Misc. Material</a></li>"); //$NON-NLS-1$
}
writer.println("</ul></div>"); //$NON-NLS-1$
writer.write("<h2>"); //$NON-NLS-1$
writer.write(rEnv.getName());
writer.write("</h2>"); //$NON-NLS-1$
writer.write("<h3 id=\"manuals\">Manuals</h3>"); //$NON-NLS-1$
if (!help.getManuals().isEmpty()) {
printDocTable(writer, help.getManuals(), baseDocPath, rEnvConfig.isLocal());
}
else {
writer.write("<p>No manuals available for this R installation.</p>");
}
writer.write("<h3 id=\"packages\">Packages</h3>"); //$NON-NLS-1$
writer.write("<table>"); //$NON-NLS-1$
char lastChar= 0;
for (final RPkgHelp pkgHelp : packages) {
final String name= pkgHelp.getName();
writer.write("<tr><td style=\"white-space: nowrap;\">"); //$NON-NLS-1$
writer.write("<a href=\""); //$NON-NLS-1$
writer.write(baseLibPath);
writer.write(name);
writer.write("/" + "\" title=\""); //$NON-NLS-1$
writer.write(name);
writer.write(" ["); //$NON-NLS-1$
printSaveHtml(writer, pkgHelp.getVersion().toString());
writer.print(']');
writer.print('"');
if (name.length() > 0) {
final char c= Character.toUpperCase(name.charAt(0));
if (c >= 'A' && c <= 'Z' && c > lastChar) {
lastChar= c;
writer.write(" id=\"idx"); //$NON-NLS-1$
writer.print((char) (32 + c)); // lowercase
writer.print('"');
}
}
writer.write(">"); //$NON-NLS-1$
printIcon(writer, baseImagesPath, "rpackage.png"); //$NON-NLS-1$
writer.write("<code>"); //$NON-NLS-1$
writer.write(pkgHelp.getName());
writer.write("</code></a>"); //$NON-NLS-1$
writer.write("</td><td>"); //$NON-NLS-1$
printSaveHtml(writer, pkgHelp.getTitle());
writer.write("</td></tr>"); //$NON-NLS-1$
}
writer.write("</table>"); //$NON-NLS-1$
if (!help.getMiscResources().isEmpty()) {
writer.write("<h3 id=\"misc\">Miscellaneous Material</h3>"); //$NON-NLS-1$
printDocTable(writer, help.getMiscResources(), baseDocPath, rEnvConfig.isLocal());
}
writer.write("<hr/>"); //$NON-NLS-1$
writer.println("</body></html>"); //$NON-NLS-1$
}
private void printDocTable(final PrintWriter writer, final List<DocResource> docs,
final String baseUrl, final boolean local) {
writer.write("<table>"); //$NON-NLS-1$
for (final DocResource doc : docs) {
writer.write("<tr><td>"); //$NON-NLS-1$
writer.write("<a href=\""); //$NON-NLS-1$
writer.write(baseUrl);
writer.write(doc.getPath());
writer.write("\">"); //$NON-NLS-1$
writer.write(doc.getTitle());
writer.write("</a>"); //$NON-NLS-1$
if (doc.getPdfPath() != null) {
writer.write("&emsp;[&#8239;<a"); //$NON-NLS-1$
writer.write(" href=\""); //$NON-NLS-1$
writer.write(baseUrl);
writer.write(doc.getPdfPath());
writer.write("\">PDF</a>");
if (local && canOpenFile("pdf")) { //$NON-NLS-1$
writer.write("&#8239;<a class=\"action\""); //$NON-NLS-1$
writer.write(" href=\""); //$NON-NLS-1$
writer.write(baseUrl);
writer.write(doc.getPdfPath());
writer.write("?action=open");
writer.write("\" title=\"Open PDF with Eclipse\"><small>(open)</small></a>");
}
writer.write("&#8239;]"); //$NON-NLS-1$
}
writer.write("</td></tr>"); //$NON-NLS-1$
}
writer.write("</table>"); //$NON-NLS-1$
}
private void printIcon(final PrintWriter writer, final String imageBasePath,
final String imageName) {
if (this.images.isEmpty()) {
return;
}
writer.write("<img class=\"icon\" src=\""); //$NON-NLS-1$
writer.write(imageBasePath);
writer.write(imageName);
writer.write("\"/>"); //$NON-NLS-1$
}
private void customizeExamples(final PrintWriter writer, final String html) {
int idx= 0;
while (idx < html.length()) {
int begin= html.indexOf("<pre", idx); //$NON-NLS-1$
if (begin >= 0) {
begin= html.indexOf('>', begin + 4);
if (begin >= 0) {
begin ++;
final int end= html.indexOf("</pre", begin); //$NON-NLS-1$
if (end >= 0) {
writer.write(html, idx, begin - idx);
printRCode(writer, html.substring(begin, end));
idx= end;
continue;
}
}
}
break;
}
writer.write(html, idx, html.length() - idx);
}
protected boolean getShowInternal() {
return true;
}
protected void customizeCss(final PrintWriter writer) {
}
protected void customizePageHtmlHeader(final HttpServletRequest req, final PrintWriter writer) {
}
protected void customizeIndexHtmlHeader(final HttpServletRequest req, final PrintWriter writer) {
}
protected void printRCode(final PrintWriter writer, final String html) {
writer.write(html);
}
private @Nullable Path checkPath(final Path directory, final String path) {
try {
final Path file= directory.resolve(path).toRealPath(LinkOption.NOFOLLOW_LINKS);
if (file.getNameCount() >= directory.getNameCount() && file.startsWith(directory)) {
return file;
}
}
catch (final Exception e) {}
return null;
}
private void serveFileResource(final Path directory, final String path,
final @Nullable String action,
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
final Path file= checkPath(directory, path);
if (file == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (!Files.isRegularFile(file)) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
return;
}
if (action != null && action.equals(RHelpWebapp.ACTION_OPEN)) {
doOpenFile(file);
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
this.fileResourceHandler.doGet(file, req, resp);
}
private void forwardToServer(final REnvHelpConfiguration rEnvConfig,
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
if (this.serverForwardHandler == null) {
resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return;
}
try {
final ServerClientSupport serverSupport= ServerClientSupport.getInstance();
final String localId= getREnvId(req);
final URI serverUrl= serverSupport.toServerBrowseUrl(rEnvConfig,
req.getPathInfo().substring(localId.length() + 1) );
this.serverForwardHandler.forward(serverUrl, req, resp);
}
catch (final StatusException | URISyntaxException e) {
throw new ServletException(e);
}
}
protected boolean canOpenFile(final String ext) {
return false;
}
protected void doOpenFile(final Path file) {
}
}