blob: 0bfecd227c949220d9dbb294a89dcb5f7ecd881b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.help.internal.webapp.data;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.help.HelpSystem;
import org.eclipse.help.IToc;
import org.eclipse.help.ITopic;
import org.eclipse.help.base.AbstractHelpScope;
import org.eclipse.help.internal.HelpPlugin;
import org.eclipse.help.internal.base.BaseHelpSystem;
import org.eclipse.help.internal.base.scope.ScopeUtils;
import org.eclipse.help.internal.search.HTMLDocParser;
import org.eclipse.help.internal.webapp.HelpWebappPlugin;
import org.eclipse.help.internal.xhtml.DynamicXHTMLProcessor;
/*
* Used by the print jsp to access print-related data.
*/
public class PrintData extends RequestData {
// default max connections for concurrent print
private static final int defaultMaxConnections = 10;
// default max topics allowed for one print request
private static final int defaultMaxTopics = 500;
// where to inject the section numbers
private static final Pattern PATTERN_HEADING = Pattern.compile("<body.*?>[\\s]*?([^<\\s])", Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
// to normalize external links to new base href
private static final Pattern PATTERN_LINK = Pattern.compile("(src|href)=\"(.*?\")", Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
// Where to inject css
private static final Pattern PATTERN_END_HEAD = Pattern.compile("</head.*?>", Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private static boolean initialized = false;
private static int allowedConnections;
private static int allowedMaxTopics;
private boolean confirmed;
// flag right-to-left direction of text
private boolean isRTL;
private AbstractHelpScope scope;
/*
* Constructs the print data for the given request.
*/
public PrintData(ServletContext context, HttpServletRequest request, HttpServletResponse response) {
super(context, request, response);
if (!initialized) {
initPreferences(preferences);
}
isRTL = UrlUtil.isRTL(request, response);
scope = RequestScope.getScope(request, response, false);
String confirmString = request.getParameter("confirmed"); //$NON-NLS-1$
if ((confirmString != null) && ("true".equals(confirmString))) { //$NON-NLS-1$
confirmed = true;
} else {
confirmed = false;
}
}
/*
* Returns the overall topic's title.
*/
public String getTitle() {
return getTopic().getLabel();
}
/*
* Returns the href of the toc containing the topic(s) to print.
*/
public String getTocHref() {
return getToc().getHref();
}
/*
* Returns the href of the root topic to print.
*/
public String getTopicHref() {
return getTopic().getHref();
}
/*
* init properties set in base preference.ini for quick print
*/
private static synchronized void initPreferences(WebappPreferences preferences) {
if (initialized) {
return;
}
// set max connection numbers for concurrent access
String maxConnections = preferences.getQuickPrintMaxConnections();
if ((null == maxConnections) || ("".equals(maxConnections.trim()))) { //$NON-NLS-1$
allowedConnections = defaultMaxConnections;
} else {
try {
allowedConnections = Integer.parseInt(maxConnections);
} catch (NumberFormatException e) {
HelpWebappPlugin.logError("Init maxConnections error. Set to default.", e); //$NON-NLS-1$
allowedConnections = defaultMaxConnections;
}
}
// set max topics allowed to print in one request
String maxTopics = preferences.getQuickPrintMaxTopics();
if ((null == maxTopics) || ("".equals(maxTopics.trim()))) { //$NON-NLS-1$
allowedMaxTopics = defaultMaxTopics;
} else {
try {
allowedMaxTopics = Integer.parseInt(maxTopics);
} catch (NumberFormatException e) {
HelpWebappPlugin.logError("Init maxTopics error. Set to default.", e); //$NON-NLS-1$
allowedMaxTopics = defaultMaxTopics;
}
}
initialized = true;
}
/*
* Generates resources to print
*/
public void generateResources(Writer out) throws IOException, ServletException {
// check resource allocation
if (!getConnection()) {
RequestDispatcher rd = context.getRequestDispatcher("/advanced/printError.jsp"); //$NON-NLS-1$
request.setAttribute("msg", "noConnection"); //$NON-NLS-1$ //$NON-NLS-2$
rd.forward(request, response);
return;
}
ITopic topic = getTopic(); // topic selected for print
int topicRequested = topicsRequested(topic);
if (topicRequested > allowedMaxTopics) {
if (!confirmed) {
releaseConnection();
RequestDispatcher rd = context.getRequestDispatcher("/advanced/printConfirm.jsp"); //$NON-NLS-1$
request.setAttribute("topicsRequested", String.valueOf(topicRequested)); //$NON-NLS-1$
request.setAttribute("allowedMaxTopics", String.valueOf(allowedMaxTopics)); //$NON-NLS-1$
rd.forward(request, response);
return;
}
}
try {
generateToc(out);
generateContent(out);
} catch (IOException e) {
RequestDispatcher rd = context.getRequestDispatcher("/advanced/printError.jsp"); //$NON-NLS-1$
request.setAttribute("msg", "ioException"); //$NON-NLS-1$ //$NON-NLS-2$
rd.forward(request, response);
} finally {
releaseConnection();
}
}
private static synchronized boolean getConnection() {
if (allowedConnections > 0) {
allowedConnections--;
return true;
}
return false;
}
private static synchronized void releaseConnection() {
allowedConnections++;
}
/*
* Calculate the amount of topics to print in one request
*/
private int topicsRequested(ITopic topic) {
int topicsRequested = 0;
if (topic.getHref() != null && topic.getHref().length() > 0) {
topicsRequested++;
}
ITopic[] subtopics = ScopeUtils.inScopeTopics(topic.getSubtopics(), scope);
for (ITopic subtopic : subtopics) {
topicsRequested += topicsRequested(subtopic);
}
return topicsRequested;
}
/*
* Generates and outputs a table of contents div with links.
*/
private void generateToc(Writer out) throws IOException {
int tocGenerated = 0;
out.write("<html>\n"); //$NON-NLS-1$
out.write("<head>\n"); //$NON-NLS-1$
out.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n"); //$NON-NLS-1$
out.write("<title>" + UrlUtil.htmlEncode(getTitle()) +"</title>\n"); //$NON-NLS-1$ //$NON-NLS-2$
out.write("<link rel=\"stylesheet\" href=\"print.css\" charset=\"utf-8\" type=\"text/css\">\n"); //$NON-NLS-1$
out.write("</head>\n"); //$NON-NLS-1$
out.write("<body dir=\"" + (isRTL ? "right" : "left") + "\" onload=\"print()\">\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
out.write("<div id=\"toc\">\n"); //$NON-NLS-1$
out.write("<h1>"); //$NON-NLS-1$
out.write(getTitle());
out.write("</h1>\n"); //$NON-NLS-1$
out.write("<h2>"); //$NON-NLS-1$
out.write(ServletResources.getString("TocHeading", request)); //$NON-NLS-1$
out.write("</h2>\n"); //$NON-NLS-1$
out.write("<div id=\"toc_content\">\n"); //$NON-NLS-1$
ITopic topic = getTopic();
String href = topic.getHref();
if (href != null && href.length() > 0) {
tocGenerated++;
}
ITopic[] subtopics = ScopeUtils.inScopeTopics(topic.getSubtopics(), scope);
for (int i = 0; i < subtopics.length; ++i) {
tocGenerated = generateToc(subtopics[i], String.valueOf(i + 1), tocGenerated, out);
}
out.write("</div>\n"); //$NON-NLS-1$
out.write("</div>\n"); //$NON-NLS-1$
out.write("</body>\n"); //$NON-NLS-1$
out.write("</html>\n"); //$NON-NLS-1$
}
/*
* Auxiliary method for recursively generating table of contents div.
*/
private int generateToc(ITopic topic, String sectionId, int tocGenerated, Writer out) throws IOException {
if (tocGenerated < allowedMaxTopics) {
out.write("<div class=\"toc_" + (sectionId.length() > 2 ? "sub" : "") + "entry\">\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
String hrefContent = sectionId + ". " + "<a href=\"#section" + sectionId + "\">" + topic.getLabel() + "</a>\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
String href = topic.getHref();
if (href != null && href.length() > 0) {
tocGenerated++;
//If the link points to an anchor, use the anchor ID instead of using the section ID
if (href.contains("#")) //$NON-NLS-1$
hrefContent = sectionId + ". " + "<a href=\"" + getAnchor(href) + "\">" + topic.getLabel() + "</a>\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
}
out.write(hrefContent);
ITopic[] subtopics = ScopeUtils.inScopeTopics(topic.getSubtopics(), scope);
for (int i = 0; i < subtopics.length; ++i) {
String subsectionId = sectionId + "." + (i + 1); //$NON-NLS-1$
tocGenerated = generateToc(subtopics[i], subsectionId, tocGenerated, out);
}
out.write("</div>\n"); //$NON-NLS-1$
return tocGenerated;
}
return tocGenerated;
}
/*
* Generates the content to print (the merged topics).
*/
public void generateContent(Writer out) throws IOException {
int topicsGenerated = 0;
generateContent(getTopic(), null, topicsGenerated, new HashSet<String>(), out);
}
/*
* Auxiliary method for recursively generating print content.
*/
private int generateContent(ITopic topic, String sectionId, int topicsGenerated, Set<String> generated, Writer out) throws IOException {
if (topicsGenerated < allowedMaxTopics) {
String href = topic.getHref();
if (href != null && href.length() > 0) {
topicsGenerated++;
// get the topic content
href = removeAnchor(href);
String pathHref = href.substring(0, href.lastIndexOf('/') + 1);
String baseHref = "../topic" + pathHref; //$NON-NLS-1$
String content;
if (!generated.contains(href)) {
generated.add(href);
content = getContent(href, locale);
// root topic doesn't have sectionId
if (sectionId != null) {
content = injectHeading(content, sectionId);
}
content = normalizeHrefs(content, baseHref);
content = injectCss(content);
out.write(content);
}
}
ITopic[] subtopics = ScopeUtils.inScopeTopics(topic.getSubtopics(), scope);
for (int i = 0; i < subtopics.length; ++i) {
String subsectionId = (sectionId != null ? sectionId + "." : "") + (i + 1); //$NON-NLS-1$ //$NON-NLS-2$
topicsGenerated = generateContent(subtopics[i], subsectionId, topicsGenerated, generated, out);
}
return topicsGenerated;
} else {
return topicsGenerated;
}
}
/*
* Injects the sectionId into the document heading.
* public static for JUnit Testing
*/
public static String injectHeading(String content, String sectionId) {
Matcher matcher = PATTERN_HEADING.matcher(content);
if (matcher.find()) {
String heading = "<a id=\"section" + sectionId + "\">" + sectionId + ". </a>"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return content.substring(0, matcher.start(1)) + heading + content.substring(matcher.start(1));
}
return content;
}
/*
*
* Injects the sectionId into the document heading.
*/
private String injectCss(String content) {
Matcher matcher = PATTERN_END_HEAD.matcher(content);
if (matcher.find()) {
String css = getCssIncludes(); //"<link rel=\"stylesheet\" type=\"text/css\" href=\"../testbook.css\">";
return content.substring(0, matcher.start(0)) + css + content.substring(matcher.start(0));
}
return content;
}
/*
* Normalizes all external links since we're not at the same base href as the
* topics we're printing.
*/
private String normalizeHrefs(String content, String baseHref) {
StringBuilder buf = new StringBuilder();
Matcher matcher = PATTERN_LINK.matcher(content);
int prev = 0;
while (matcher.find()) {
buf.append(content.substring(prev, matcher.start(2)));
buf.append(baseHref);
buf.append(matcher.group(2));
prev = matcher.end();
}
buf.append(content.substring(prev));
return buf.toString();
}
/*
* Returns the string content of the referenced topic in UTF-8.
*/
private String getContent(String href, String locale) {
InputStream in = HelpSystem.getHelpContent(href, locale);
StringBuilder buf = new StringBuilder();
InputStream rawInput=null;
if (in != null) {
try {
String charset = HTMLDocParser.getCharsetFromHTML(in);
if (charset == null) {
charset = "UTF-8"; //$NON-NLS-1$
}
rawInput = HelpSystem.getHelpContent(href, locale);
boolean filter = BaseHelpSystem.getMode() != BaseHelpSystem.MODE_INFOCENTER;
in = DynamicXHTMLProcessor.process(href, rawInput, locale, filter);
if (in == null) {
in = HelpSystem.getHelpContent(href, locale);
}
try (Reader reader = new BufferedReader(new InputStreamReader(in, charset))) {
char[] cbuf = new char[4096];
int num;
while ((num = reader.read(cbuf)) > 0) {
buf.append(cbuf, 0, num);
}
}
}
catch (Exception e) {
String msg = "Error retrieving print preview content for " + href; //$NON-NLS-1$
HelpWebappPlugin.logError(msg, e);
}
finally {
try {
in.close();
}
catch (IOException e) {
}
try {
if (rawInput != null)
rawInput.close();
}
catch (Exception e) {}
}
}
return buf.toString();
}
/*
* Returns the toc containing the selected topic(s).
*/
private IToc getToc() {
String tocParam = request.getParameter("toc"); //$NON-NLS-1$
if (tocParam != null && tocParam.length() > 0) {
return HelpPlugin.getTocManager().getToc(tocParam, getLocale());
}
String topicParam = request.getParameter("topic"); //$NON-NLS-1$
if (topicParam != null && topicParam.length() > 0) {
if (topicParam.startsWith("/../nav/")) { //$NON-NLS-1$
String navPath = topicParam.substring(8);
StringTokenizer tok = new StringTokenizer(navPath, "_"); //$NON-NLS-1$
int index = Integer.parseInt(tok.nextToken());
return HelpPlugin.getTocManager().getTocs(getLocale())[index];
}
IToc[] tocs = HelpPlugin.getTocManager().getTocs(getLocale());
for (IToc toc : tocs) {
if (toc.getTopic(topicParam) != null) {
return toc;
}
}
}
return null;
}
/*
* Returns the selected topic.
*/
private ITopic getTopic() {
String topicParam = request.getParameter("topic"); //$NON-NLS-1$
String anchorParam = request.getParameter("anchor"); //$NON-NLS-1$
if (anchorParam!=null) {
topicParam = topicParam + '#' + anchorParam;
}
if (topicParam != null && topicParam.length() > 0) {
if (topicParam.startsWith("/../nav/")) { //$NON-NLS-1$
String navPath = topicParam.substring(8);
StringTokenizer tok = new StringTokenizer(navPath, "_"); //$NON-NLS-1$
int index = Integer.parseInt(tok.nextToken());
ITopic topic = HelpPlugin.getTocManager().getTocs(getLocale())[index].getTopic(null);
while (tok.hasMoreTokens()) {
index = Integer.parseInt(tok.nextToken());
topic = topic.getSubtopics()[index];
}
return topic;
}
else {
IToc[] tocs = HelpPlugin.getTocManager().getTocs(getLocale());
for (IToc toc : tocs) {
ITopic topic = toc.getTopic(topicParam);
if (topic != null) {
return topic;
}
// Test for root node as topic
topic = toc.getTopic(null);
if (topicParam.equals(topic.getHref())) {
return topic;
}
}
}
return null;
}
return getToc().getTopic(null);
}
private static String removeAnchor(String href) {
int index = href.indexOf('#');
if (index != -1) {
return href.substring(0, index);
}
return href;
}
private static String getAnchor(String href) {
int index = href.indexOf('#');
if (index != -1) {
return href.substring(index, href.length());
}
return ""; //$NON-NLS-1$
}
private String getCssIncludes() {
List<String> css = new ArrayList<>();
CssUtil.addCssFiles("topic_css", css); //$NON-NLS-1$
return CssUtil.createCssIncludes(css, "../"); //$NON-NLS-1$
}
}