blob: 5afd97520f7470be7aebd83f7dd2c42af30c66fc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2017 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Yaroslav Nikolaiko <nikolaiko.yaroslav@gmail.com> - [webapp][base] Bugs related to Search Scope for filtering content in The Eclipse platform's help infocenter - http://bugs.eclipse.org/441407
*******************************************************************************/
package org.eclipse.help.internal.webapp.servlet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.help.IIndex;
import org.eclipse.help.IIndexEntry;
import org.eclipse.help.IIndexEntry2;
import org.eclipse.help.IIndexSee;
import org.eclipse.help.IIndexSubpath;
import org.eclipse.help.ITopic;
import org.eclipse.help.base.AbstractHelpScope;
import org.eclipse.help.internal.HelpPlugin;
import org.eclipse.help.internal.base.scope.ScopeUtils;
import org.eclipse.help.internal.webapp.WebappResources;
import org.eclipse.help.internal.webapp.data.ActivitiesData;
import org.eclipse.help.internal.webapp.data.RequestScope;
import org.eclipse.help.internal.webapp.data.UrlUtil;
import org.eclipse.osgi.util.NLS;
import com.ibm.icu.text.Collator;
/*
* Creates xml representing selected parts of the index
* Parameter "start" represents the part of the index to start reading from
* Parameter "size" indicates the number of entries to read, no size parameter
* or a negative size parameter indicates that all entries which match the start
* letters should be displayed.
* Parameter "offset" represents the starting point relative to the start
*/
public class IndexFragmentServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static Map<String, String> locale2Response = new WeakHashMap<>();
private String startParameter;
private String sizeParameter;
private String entryParameter;
private String modeParameter;
private String showAllParameter;
private int size;
private int entry;
private static final String NEXT = "next"; //$NON-NLS-1$
private static final String PREVIOUS = "previous"; //$NON-NLS-1$
private static final String SIZE = "size"; //$NON-NLS-1$
private static final String MODE = "mode"; //$NON-NLS-1$
private static final String ENTRY = "entry"; //$NON-NLS-1$
private static final String SHOW_ALL = "showAll"; //$NON-NLS-1$
private Collator collator = Collator.getInstance();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// set the character-set to UTF-8 before calling resp.getWriter()
resp.setContentType("application/xml; charset=UTF-8"); //$NON-NLS-1$
resp.getWriter().write(processRequest(req, resp));
}
protected String processRequest(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String locale = UrlUtil.getLocale(req, resp);
startParameter = req.getParameter("start"); //$NON-NLS-1$
if (startParameter != null) {
startParameter = startParameter.toLowerCase();
}
size = 30;
sizeParameter = req.getParameter(SIZE);
if (sizeParameter != null) {
try {
size = Integer.parseInt(sizeParameter);
} catch (NumberFormatException n) {
}
}
entry = -1;
entryParameter = req.getParameter(ENTRY);
if (entryParameter != null) {
try {
entry = Integer.parseInt(entryParameter);
} catch (NumberFormatException n) {
}
}
modeParameter = req.getParameter(MODE);
showAllParameter = req.getParameter(SHOW_ALL);
if (showAllParameter != null) {
// Use activities data to toggle the show all state
new ActivitiesData(this.getServletContext(), req, resp);
}
req.setCharacterEncoding("UTF-8"); //$NON-NLS-1$
// Cache suppression required because the set of in scope
// topics could change between requests
resp.setHeader("Cache-Control","no-cache"); //$NON-NLS-1$//$NON-NLS-2$
resp.setHeader("Pragma","no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
resp.setDateHeader ("Expires", 0); //$NON-NLS-1$
AbstractHelpScope scope = RequestScope.getScope(req, resp, false);
Serializer serializer = new Serializer(locale, scope);
String response = serializer.generateIndexXml();
locale2Response.put(locale, response);
return response;
}
/*
* Class which creates the xml file based upon the request parameters
*/
private class Serializer {
private IIndex index;
private StringBuilder buf;
private int count = 0;
private String locale;
private List<Integer> entryList;
private IIndexEntry[] entries;
private boolean enablePrevious = true;
private boolean enableNext = true;
private AbstractHelpScope scope;
public Serializer(String locale, AbstractHelpScope scope) {
this.locale = locale;
this.scope = scope;
index = HelpPlugin.getIndexManager().getIndex(locale);
buf = new StringBuilder();
}
/*
* There are three modes of generation, current page, next page and previous page.
* Current page returns a screenful of entries starting at the startParameter.
* Next page returns a screenful of entries starting after but not including the start parameter.
* Previous page returns a screenful of entries going back from the start parameter
*/
private String generateIndexXml() {
entries = index.getEntries();
if (entries.length == 0) {
generateEmptyIndexMessage();
} else {
entryList = new ArrayList<>();
int nextEntry = findFirstEntry(entries);
if (PREVIOUS.equals(modeParameter)) {
int remaining = getPreviousEntries(nextEntry, size);
getNextEntries(nextEntry, remaining);
} else {
int remaining = getNextEntries(nextEntry, size);
if (remaining == size) {
// Generate just the last entry
size = 1;
getPreviousEntries(nextEntry, 1);
}
}
for (Integer entryId : entryList) {
generateEntry(entries[entryId.intValue()], 0, "e" + entryId.intValue()); //$NON-NLS-1$
}
}
String header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tree_data enableNext = \"" //$NON-NLS-1$
+ Boolean.toString(enableNext) + "\" enablePrevious = \"" + Boolean.toString(enablePrevious) + "\">\n"; //$NON-NLS-1$ //$NON-NLS-2$
buf.append("</tree_data>\n"); //$NON-NLS-1$
return header + buf.toString();
}
private int getCategory(String keyword) {
if (keyword != null && keyword.length() > 0) {
char c = keyword.charAt(0);
if (Character.isDigit(c)) {
return 2;
} else if (Character.isLetter(c)) {
return 3;
}
return 1;
}
return 4;
}
private int compare (String left, String right) {
int catLeft = getCategory(left);
int catRight = getCategory(right);
if (catLeft != catRight) {
return catLeft - catRight;
} else {
return collator.compare(left, right);
}
}
private int findFirstEntry(IIndexEntry[] entries) {
if (NEXT.equals(modeParameter)) {
if (entry >= entries.length - 1) {
return entries.length - 1;
} else {
return entry + 1;
}
}
if (PREVIOUS.equals(modeParameter)) {
if (entry <= 0) {
return 0;
} else {
return entry - 1;
}
}
if (startParameter == null) {
return 0;
}
int nextEntry = 0;
while (nextEntry < entries.length) {
String keyword = entries[nextEntry].getKeyword().toLowerCase();
if (keyword != null) {
if (compare(startParameter, keyword) <= 0) {
break;
}
}
nextEntry++;
}
return nextEntry;
}
private int getNextEntries(int nextEntry, int remaining) {
while (nextEntry < entries.length) {
int entrySize = enabledEntryCount(entries[nextEntry]);
if (remaining == size || remaining > entrySize) {
entryList.add(Integer.valueOf(nextEntry));
setFlags(nextEntry);
remaining -= entrySize;
} else {
break;
}
nextEntry++;
}
return remaining;
}
private int getPreviousEntries(int nextEntry, int remaining) {
nextEntry--;
while (nextEntry >= 0) {
int entrySize = enabledEntryCount(entries[nextEntry]);
if (remaining == size || remaining > entrySize) {
entryList.add(0, Integer.valueOf(nextEntry));
setFlags(nextEntry);
remaining -= entrySize;
} else {
break;
}
nextEntry--;
}
return remaining;
}
private void setFlags(int nextEntry) {
if (nextEntry == 0) {
enablePrevious = false;
}
if (nextEntry == entries.length - 1) {
enableNext = false;
}
}
private int enabledEntryCount(IIndexEntry entry) {
if (!ScopeUtils.showInTree(entry, scope)) return 0;
if (entry.getKeyword() == null || entry.getKeyword().length() == 0) {
return 0;
}
int count = 1;
int topicCount = enabledTopicCount(entry);
IIndexEntry[] subentries = entry.getSubentries();
int subentryCount = 0;
for (IIndexEntry subentrie : subentries) {
count += enabledEntryCount(subentrie);
}
int seeCount = 0;
IIndexSee[] sees = entry instanceof IIndexEntry2 ? ((IIndexEntry2)entry).getSees() : new IIndexSee[0];
for (IIndexSee see : sees) {
if (ScopeUtils.showInTree(see, scope)) {
seeCount++;
}
}
if (topicCount + subentryCount + seeCount > 1) {
count += topicCount;
}
count += subentryCount;
count += seeCount;
return count;
}
private int enabledTopicCount(IIndexEntry entry) {
int topicCount = 0;
ITopic[] topics = entry.getTopics();
for (ITopic topic : topics) {
if (scope.inScope(topic)) {
topicCount++;
}
}
return topicCount;
}
private void generateEmptyIndexMessage() {
buf.append("<node"); //$NON-NLS-1$
buf.append('\n' + " title=\"" + XMLGenerator.xmlEscape(WebappResources.getString("IndexEmpty", UrlUtil.getLocale(locale))) + '"'); //$NON-NLS-1$ //$NON-NLS-2$
buf.append('\n' + " id=\"no_index\""); //$NON-NLS-1$
buf.append(">\n"); //$NON-NLS-1$
buf.append("</node>\n"); //$NON-NLS-1$
enableNext = false;
enablePrevious = false;
}
private void generateEntry(IIndexEntry entry, int level, String id) {
if (!ScopeUtils.showInTree(entry, scope)) return;
if (entry.getKeyword() != null && entry.getKeyword().length() > 0) {
ITopic[] topics = ScopeUtils.inScopeTopics(entry.getTopics(), scope);
IIndexEntry[] subentries = ScopeUtils.inScopeEntries(entry.getSubentries(), scope);
IIndexSee[] sees;
if (entry instanceof IIndexEntry2) {
sees = ((IIndexEntry2)entry).getSees();
} else {
sees = new IIndexSee[0];
}
boolean multipleTopics = topics.length > 1;
boolean singleTopic = topics.length == 1;
buf.append("<node"); //$NON-NLS-1$
if (entry.getKeyword() != null) {
buf.append('\n' + " title=\"" + XMLGenerator.xmlEscape(entry.getKeyword()) + '"'); //$NON-NLS-1$
}
buf.append('\n' + " id=\"" + id + '"'); //$NON-NLS-1$
String href;
if (singleTopic) {
href = UrlUtil.getHelpURL((topics[0]).getHref());
buf.append('\n' + " href=\"" + //$NON-NLS-1$
XMLGenerator.xmlEscape(href) + "\""); //$NON-NLS-1$
}
buf.append(">\n"); //$NON-NLS-1$
if (multipleTopics || subentries.length > 0 || sees.length > 0) {
if (multipleTopics) generateTopicList(entry);
generateSubentries(entry, level + 1);
generateSees(sees);
}
buf.append("</node>\n"); //$NON-NLS-1$
}
}
private void generateSubentries(IIndexEntry entry, int level) {
IIndexEntry[] subentries = entry.getSubentries();
for (IIndexEntry subentrie : subentries) {
generateEntry(subentrie, level, "s" + count++); //$NON-NLS-1$
}
}
private void generateTopicList(IIndexEntry entry) {
ITopic[] topics = entry.getTopics();
for (ITopic topic : topics) {
if (ScopeUtils.showInTree(topic, scope)) {
//
String label = UrlUtil.htmlEncode(topic.getLabel());
if (label == null) {
label = UrlUtil.htmlEncode(topic.getLabel());
}
buf.append("<node"); //$NON-NLS-1$
if (entry.getKeyword() != null) {
buf.append('\n' + " title=\"" + label + '"'); //$NON-NLS-1$
}
count++;
buf.append('\n' + " id=\"i" + count + '"'); //$NON-NLS-1$
String href = UrlUtil.getHelpURL(topic.getHref());
buf.append('\n' + " href=\"" //$NON-NLS-1$
+ XMLGenerator.xmlEscape(href) + "\""); //$NON-NLS-1$
buf.append(">\n"); //$NON-NLS-1$
buf.append("</node>\n"); //$NON-NLS-1$
}
}
}
private void generateSees(IIndexSee[] sees) {
for (IIndexSee see : sees) {
if (ScopeUtils.showInTree(see, scope)) {
//
String key = see.isSeeAlso() ? "SeeAlso" : "See"; //$NON-NLS-1$ //$NON-NLS-2$
String seePrefix = WebappResources.getString(key, UrlUtil
.getLocale(locale));
String seeTarget = see.getKeyword();
IIndexSubpath[] subpathElements = see.getSubpathElements();
for (IIndexSubpath subpathElement : subpathElements) {
seeTarget += ", "; //$NON-NLS-1$
seeTarget += subpathElement.getKeyword();
}
String label = NLS.bind(seePrefix, seeTarget);
String encodedLabel = UrlUtil.htmlEncode(label);
buf.append("<node"); //$NON-NLS-1$
buf.append('\n' + " title=\"" + encodedLabel + '"'); //$NON-NLS-1$
count++;
buf.append('\n' + " id=\"i" + count + '"'); //$NON-NLS-1$
String href = "see:" + seeTarget; //$NON-NLS-1$
buf.append('\n' + " href=\"" //$NON-NLS-1$
+ XMLGenerator.xmlEscape(href) + "\""); //$NON-NLS-1$
buf.append(">\n"); //$NON-NLS-1$
buf.append("</node>\n"); //$NON-NLS-1$
}
}
}
}
}