blob: c595c5ca13aac708e8569eca0cd33dcf7f61bef7 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2010, 2020 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.jcommons.lang.ObjectUtils.nonNullAssert;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Matcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
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.status.StatusException;
import org.eclipse.statet.internal.rhelp.core.DataStream;
import org.eclipse.statet.internal.rhelp.core.REnvHelpImpl;
import org.eclipse.statet.internal.rhelp.core.SerUtil;
import org.eclipse.statet.internal.rhelp.core.http.HttpHeaderUtils;
import org.eclipse.statet.internal.rhelp.core.http.HttpHeaderUtils.MediaTypeEntry;
import org.eclipse.statet.internal.rhelp.core.server.ServerApi;
import org.eclipse.statet.internal.rhelp.core.server.ServerApi.RequestInfo;
import org.eclipse.statet.rhelp.core.REnvHelp;
import org.eclipse.statet.rhelp.core.REnvHelpConfiguration;
import org.eclipse.statet.rhelp.core.RHelpManager;
import org.eclipse.statet.rhelp.core.RHelpPage;
import org.eclipse.statet.rhelp.core.RHelpSearchMatch;
import org.eclipse.statet.rhelp.core.RHelpSearchMatch.MatchFragment;
import org.eclipse.statet.rhelp.core.RHelpSearchQuery;
import org.eclipse.statet.rhelp.core.RHelpSearchRequestor;
import org.eclipse.statet.rhelp.core.RPkgHelp;
import org.eclipse.statet.rj.renv.core.REnv;
@NonNullByDefault
public abstract class RHelpApi1Servlet extends HttpServlet {
private static final long serialVersionUID= 1L;
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$
@SuppressWarnings("null")
private static String getEnvId(final HttpServletRequest req) {
return (String) req.getAttribute(ATTR_RENV_ID);
}
@SuppressWarnings("null")
private static REnv getEnv(final HttpServletRequest req) {
return (REnv) req.getAttribute(ATTR_RENV_RESOLVED);
}
@SuppressWarnings("null")
private static REnvHelpImpl getEnvHelp(final HttpServletRequest req) {
return (REnvHelpImpl) req.getAttribute(ATTR_RENV_HELP);
}
private static final MediaTypeProvider API_MEDIA_TYPE_PROVIDER;
static {
final CustomMediaTypeProvider apiMediaTypes= new CustomMediaTypeProvider();
apiMediaTypes.addExt("ser", ServerApi.DS_MEDIA_TYPE_STRING); //$NON-NLS-1$
API_MEDIA_TYPE_PROVIDER= apiMediaTypes;
}
private static class WrappedIOException extends RuntimeException {
private static final long serialVersionUID= 1L;
public WrappedIOException(final IOException cause) {
super(cause);
}
@Override
@SuppressWarnings("null")
public IOException getCause() {
return (IOException) super.getCause();
}
}
private RHelpManager rHelpManager;
private ResourceHandler resourceHandler;
public RHelpApi1Servlet() {
}
protected void init(final RHelpManager rHelpManager,
final @Nullable ResourceHandler resourceHandler) {
this.rHelpManager= rHelpManager;
this.resourceHandler= (resourceHandler != null) ? resourceHandler :
new SimpleResourceHandler(new ServletMediaTypeProvider(getServletContext()));
this.resourceHandler.setSpecialMediaTypes(API_MEDIA_TYPE_PROVIDER);
this.resourceHandler.setCacheControl("max-age=100, must-revalidate"); //$NON-NLS-1$
}
@Override
public void init(final ServletConfig config) throws ServletException {
super.init(config);
}
@Override
public void destroy() {
super.destroy();
}
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
final String path= req.getPathInfo();
try {
if (path != null) {
final RequestInfo info= ServerApi.extractRequestInfo(path);
if (info != null) {
if (!checkREnv(info.rEnvId, req, resp)) {
return;
}
switch (info.segments[0]) {
case ServerApi.STAMP:
if (info.segmentCount == 1) {
processStamp(req, resp);
return;
}
break;
case ServerApi.BASIC_DATA:
if (info.segmentCount == 1) {
processBasicData(req, resp);
return;
}
break;
case ServerApi.PKGS:
if (info.segmentCount >= 2) {
processPkgs(info, req, resp);
return;
}
break;
case ServerApi.PAGES:
if (info.segmentCount == 1) {
processPages(req, resp);
return;
}
break;
}
}
}
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
catch (final StatusException e) {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "R Help Server Error - " + e.getMessage());
return;
}
finally {
final REnvHelp help= (REnvHelp) req.getAttribute(ATTR_RENV_HELP);
if (help != null) {
help.unlock();
}
}
}
@Override
protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
final String path= req.getPathInfo();
try {
if (path != null) {
final RequestInfo info= ServerApi.extractRequestInfo(path);
if (info != null) {
if (!checkREnv(info.rEnvId, req, resp)) {
return;
}
switch (info.segments[0]) {
case ServerApi.SEARCH:
if (info.segmentCount == 1) {
processSearch(req, resp);
return;
}
break;
}
}
}
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
catch (final StatusException e) {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "R Help Server Error - " + e.getMessage());
return;
}
finally {
final REnvHelp help= (REnvHelp) req.getAttribute(ATTR_RENV_HELP);
if (help != null) {
help.unlock();
}
}
}
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, "Not Found - " +
"The R library of the requested R environment <code>" + rEnv.getName() + "</code> " +
"is not yet indexed. Please retry later.");
return false;
}
else {
final String message= (id.startsWith("default-")) ? //$NON-NLS-1$
"The requested default R environment is missing." :
"The requested R environment doesn't exist.";
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not Found - " + message);
return false;
}
}
private void processStamp(
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
final REnvHelpImpl help= getEnvHelp(req);
resp.setContentType(ServerApi.DS_MEDIA_TYPE_STRING);
try (final OutputStream out= resp.getOutputStream()) {
out.write(DataStream.encodeLong(help.getStamp()));
}
}
protected int checkAcceptDS(final HttpServletRequest req) throws ServletException {
final List<MediaTypeEntry> entries= HttpHeaderUtils.readAcceptHeaderEntries(req,
(final String type, final String subtype) ->
(type.equals(ServerApi.APPLICATION_MEDIA_TYPE)
&& subtype.equals(ServerApi.DS_MEDIA_SUBTYPE) ));
return HttpHeaderUtils.findFirstValid(entries, ServerApi.DS_SER_VERSION,
(final int v) -> (v == SerUtil.CURRENT_VERSION) );
}
private void processBasicData(
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
final REnvHelpConfiguration config= (REnvHelpConfiguration) req.getAttribute(ATTR_RENV_CONFIG);
final String eTag= req.getHeader("If-None-Match"); //$NON-NLS-1$
if (eTag != null) {
final Matcher matcher= ServerApi.ETAG_PATTERN.matcher(eTag);
if (matcher.matches()) {
try {
final long stamp= Long.parseUnsignedLong(matcher.group(1));
if (stamp == getEnvHelp(req).getStamp()) {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
}
catch (final NumberFormatException e) {}
}
}
final int serVersion= checkAcceptDS(req);
if (serVersion < 0) {
resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
return;
}
final Path file= SerUtil.getBasicDataFilePath(config);
if (file != null && Files.isRegularFile(file)) {
this.resourceHandler.doGet(file, req, resp);
}
else {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
private void processPkgs(final RequestInfo info,
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException,
StatusException {
final REnvHelpImpl help= getEnvHelp(req);
if (info.segmentCount >= 3) {
switch (info.segments[2]) {
// case ServerApi.TOPICS:
// processPkgTopics(pkgHelp, req, resp);
// return;
case ServerApi.PAGES:
final RPkgHelp pkgHelp= help.getPkgHelp(info.segments[1]);
if (pkgHelp == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (info.segmentCount == 4) {
processPkgPage(pkgHelp, info.segments[3], req, resp);
return;
}
processPkgPages(pkgHelp, req, resp);
return;
}
}
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
private void processPkgPages(final RPkgHelp pkgHelp,
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
final List<RHelpPage> pages;
final String topic= req.getParameter(ServerApi.TOPIC_PARAM);
if (topic != null) {
final RHelpPage page= pkgHelp.getPageForTopic(topic);
pages= (page != null) ? ImCollections.newList(page) : ImCollections.emptyList();
}
else {
pages= pkgHelp.getPages();
}
resp.setContentType(ServerApi.DS_MEDIA_TYPE_STRING);
try (final DataStream out= DataStream.get(resp.getOutputStream())) {
final int n= pages.size();
out.writeInt(n);
for (int i= 0; i < n; i++) {
final RHelpPage page= pages.get(i);
out.writeString(page.getName());
}
}
}
private void processPkgPage(final RPkgHelp pkgHelp, final String pageName,
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException,
StatusException {
final REnvHelpImpl help= getEnvHelp(req);
final String queryString= req.getParameter(ServerApi.QUERY_STRING_PARAM);
final String html= help.getHtmlPage(pkgHelp, pageName, queryString);
if (html == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
resp.setContentType(ServerApi.DS_MEDIA_TYPE_STRING);
try (final DataStream out= DataStream.get(resp.getOutputStream())) {
out.writeString(html);
}
}
private void processPages(
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException,
StatusException {
final REnvHelpImpl help= getEnvHelp(req);
final String topic= req.getParameter(ServerApi.TOPIC_PARAM);
if (topic != null) {
final List<RHelpPage> pages= help.getPagesForTopic(topic, null);
resp.setContentType(ServerApi.DS_MEDIA_TYPE_STRING);
try (final DataStream out= DataStream.get(resp.getOutputStream())) {
final int n= pages.size();
out.writeInt(n);
for (int i= 0; i < n; i++) {
final RHelpPage page= pages.get(i);
out.writeString(page.getPackage().getName());
out.writeString(page.getName());
}
}
}
}
private @Nullable RHelpSearchQuery readRHelpSearchQuery(
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
if (req.getContentType() != null && req.getContentType().equals(ServerApi.DS_MEDIA_TYPE_STRING)) {
try (final DataStream in= DataStream.get(req.getInputStream())) {
final int searchType= in.readInt();
final String searchString= in.readNonNullString();
final ImList<String >fields= ImCollections.newList(in.readNonNullStringArray());
final ImList<String >keywords= ImCollections.newList(in.readNonNullStringArray());
final ImList<String >packages= ImCollections.newList(in.readNonNullStringArray());
return new RHelpSearchQuery(searchType, searchString, fields,
keywords, packages,
getEnv(req) );
}
}
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request - Unsupported search query: 'ContentType'");
return null;
}
private int readIntParam(final HttpServletRequest req, final String name, final int defaultValue) {
final String s= req.getParameter(name);
if (s != null) {
try {
final int value= Integer.parseInt(s);
if (value >= 0) {
return value;
}
}
catch (final NumberFormatException e) {}
return -1;
}
else {
return defaultValue;
}
}
private void processSearch(
final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException,
StatusException {
final REnvHelpImpl help= getEnvHelp(req);
final RHelpSearchQuery searchQuery= readRHelpSearchQuery(req, resp);
if (searchQuery == null) {
return;
}
try {
searchQuery.validate();
}
catch (final StatusException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request - Invalid search query: " + e.getMessage());
return;
}
final int maxFragments= readIntParam(req, ServerApi.MAX_FRAGMENTS_PARAM, 10);
if (maxFragments == -1) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request - Invalid param '" + ServerApi.MAX_FRAGMENTS_PARAM + "'");
return;
}
resp.setContentType(ServerApi.DS_MEDIA_TYPE_STRING);
resp.flushBuffer();
try (final DataStream out= DataStream.get(resp.getOutputStream())) {
help.search(searchQuery, new RHelpSearchRequestor() {
@Override
public int getMaxFragments() {
return maxFragments;
}
@Override
public void matchFound(final RHelpSearchMatch match) {
try {
out.writeByte(ServerApi.PAGE_MATCH);
{ final RHelpPage page= match.getPage();
out.writeString(page.getPackage().getName());
out.writeString(page.getName());
}
out.writeFloat(match.getScore());
out.writeInt(match.getMatchCount());
if (match.getMatchCount() >= 0) {
final MatchFragment[] fragments= nonNullAssert(match.getBestFragments());
final int nFragments= fragments.length;
out.writeInt(nFragments);
for (int i= 0; i < nFragments; i++) {
final MatchFragment fragment= fragments[i];
out.writeString(fragment.getField());
out.writeString(fragment.getText());
}
}
}
catch (final IOException e) {
throw new WrappedIOException(e);
}
}
});
out.writeInt(ServerApi.END_MATCH);
}
catch (final WrappedIOException e) {
throw e.getCause();
}
}
}