| /*=============================================================================# |
| # 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; |
| |
| |
| @SuppressWarnings("null") |
| 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(); |
| } |
| } |
| |
| } |