1 /*******************************************************************************
2  * Copyright (c) 2006, 2017 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.help.internal.webapp.servlet;
15 
16 import java.io.IOException;
17 import java.util.Locale;
18 import java.util.Map;
19 import java.util.WeakHashMap;
20 
21 import javax.servlet.ServletException;
22 import javax.servlet.http.HttpServlet;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
25 
26 import org.eclipse.help.IToc;
27 import org.eclipse.help.ITopic;
28 import org.eclipse.help.base.AbstractHelpScope;
29 import org.eclipse.help.internal.Topic;
30 import org.eclipse.help.internal.base.scope.ScopeUtils;
31 import org.eclipse.help.internal.toc.Toc;
32 import org.eclipse.help.internal.webapp.WebappResources;
33 import org.eclipse.help.internal.webapp.data.IconFinder;
34 import org.eclipse.help.internal.webapp.data.RequestScope;
35 import org.eclipse.help.internal.webapp.data.TocData;
36 import org.eclipse.help.internal.webapp.data.UrlUtil;
37 
38 /*
39  * Creates xml representing selected parts of one or more TOCs  depending on the parameters
40  * With no parameters the head of each toc is included
41  * With parameter "href" the node and all its ancestors and siblings is included, corresponds to show in toc
42  * With parameter "toc" and optionally "path" the node, its ancestors and children are included
43  */
44 public class TocFragmentServlet extends HttpServlet {
45 
46 	private static final long serialVersionUID = 1L;
47 	private static Map<String, String> locale2Response = new WeakHashMap<>();
48 	private boolean isErrorSuppress;
49 
50 	@Override
doGet(HttpServletRequest req, HttpServletResponse resp)51 	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
52 			throws ServletException, IOException {
53 		// set the character-set to UTF-8 before calling resp.getWriter()
54 		resp.setContentType("application/xml; charset=UTF-8"); //$NON-NLS-1$
55 		resp.getWriter().write(processRequest(req, resp));
56 	}
57 
processRequest(HttpServletRequest req, HttpServletResponse resp)58 	protected String processRequest(HttpServletRequest req, HttpServletResponse resp)
59 			throws ServletException, IOException {
60 		String locale = UrlUtil.getLocale(req, resp);
61 		req.setCharacterEncoding("UTF-8"); //$NON-NLS-1$
62 		resp.setHeader("Cache-Control","no-cache");   //$NON-NLS-1$//$NON-NLS-2$
63 		resp.setHeader("Pragma","no-cache");  //$NON-NLS-1$ //$NON-NLS-2$
64 		resp.setDateHeader ("Expires", 0); 	 //$NON-NLS-1$
65 		TocData data = new TocData(this.getServletContext(), req, resp);
66 
67 		readParameters(req);
68 
69 		AbstractHelpScope scope = RequestScope.getScope(req, resp, false);
70 		Serializer serializer = new Serializer(data, UrlUtil.getLocaleObj(req, resp), scope);
71 		String response = serializer.generateTreeXml();
72 		locale2Response.put(locale, response);
73 
74 		return response;
75 	}
76 
readParameters(HttpServletRequest req)77 	private void readParameters(HttpServletRequest req) {
78 		String errorSuppressParam = req.getParameter("errorSuppress"); //$NON-NLS-1$
79 		isErrorSuppress = "true".equalsIgnoreCase(errorSuppressParam); //$NON-NLS-1$
80 	}
81 
82 	/*
83 	 * Class which creates the xml file based upon the request parameters
84 	 */
85 	private class Serializer {
86 
87 		private TocData tocData;
88 		private StringBuilder buf;
89 		private int requestKind;
90 		private Locale locale;
91 		private AbstractHelpScope scope;
92 		private static final int REQUEST_SHOW_IN_TOC = 1;      // Get the path to an element an element based on its href
93 		private static final int REQUEST_SHOW_TOCS = 2;        // Show all the tocs but not their children
94 		private static final int REQUEST_SHOW_CHILDREN = 3;    // Show the children of a node
95 		private static final int REQUEST_EXPAND_PATH = 4;      // Get all the nodes requires to expand a path in the tree
96 
Serializer(TocData data, Locale locale, AbstractHelpScope scope)97 		public Serializer(TocData data, Locale locale, AbstractHelpScope scope) {
98 			tocData = data;
99 			buf = new StringBuilder();
100 			this.locale = locale;
101 			this.scope = scope;
102 			if (tocData.isExpandPath()) {
103 				requestKind = REQUEST_EXPAND_PATH;
104 			} else if (tocData.getTopicHref() != null) {
105 				requestKind = REQUEST_SHOW_IN_TOC;
106 			} else if (tocData.getSelectedToc() == -1) {
107 				requestKind = REQUEST_SHOW_TOCS;
108 			} else {
109 				requestKind = REQUEST_SHOW_CHILDREN;
110 			}
111 		}
112 
generateTreeXml()113 		public String generateTreeXml() {
114 			buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); //$NON-NLS-1$
115 			buf.append("<tree_data>\n"); //$NON-NLS-1$
116 
117 			// Return an error for show in toc if topic was not found in toc
118 			if ((requestKind == REQUEST_SHOW_IN_TOC || requestKind == REQUEST_EXPAND_PATH) && tocData.getTopicPath() == null) {
119 				addError(WebappResources.getString("CannotSync", locale)); //$NON-NLS-1$
120 			} else if (requestKind == REQUEST_SHOW_IN_TOC) {
121 				generateNumericPath();
122 			} else {
123 				serializeTocs();
124 			}
125 			buf.append("</tree_data>\n"); //$NON-NLS-1$
126 			return buf.toString();
127 		}
128 
129 
generateNumericPath()130 		private void generateNumericPath() {
131 			int selectedToc = tocData.getSelectedToc();
132 			if (selectedToc < 0) {
133 				addError(WebappResources.getString("CannotSync", locale)); //$NON-NLS-1$
134 			} else {
135 				// Count the number of enabled tocs
136 				int enabled = 0;
137 				for (int i = 0; i <= selectedToc; i++) {
138 					if (ScopeUtils.showInTree(tocData.getTocs()[i], scope)) {
139 						enabled++;
140 					}
141 				}
142 				String fullNumericPath = "" + (enabled - 1); //$NON-NLS-1$
143 				String numericPath = tocData.getNumericPath();
144 				if (numericPath != null) {
145 					fullNumericPath = fullNumericPath +  '_' + numericPath;
146 				}
147 				buf.append("<numeric_path path=\"" + fullNumericPath + "\"/>\n"); //$NON-NLS-1$ //$NON-NLS-2$
148 			}
149 		}
150 
addError(String message)151 		private void addError(String message) {
152 			if (!isErrorSuppress) {
153 				buf.append("<error>"); //$NON-NLS-1$
154 				buf.append(XMLGenerator.xmlEscape(message));
155 				buf.append("</error>"); //$NON-NLS-1$
156 			}
157 		}
158 
serializeTocs()159 		private void serializeTocs() {
160 			ITopic[] topicPath = tocData.getTopicPath();
161 
162 			int selectedToc = tocData.getSelectedToc();
163 			// Iterate over all tocs - if there is a selected toc only generate that
164 			// toc, otherwise generate the root of every toc.
165 			for (int toc=0; toc< tocData.getTocCount(); toc++) {
166 				boolean shouldLoad = requestKind == REQUEST_SHOW_TOCS || toc == selectedToc;
167 				if (shouldLoad) {
168 					boolean isSelected = false; // Should this node be selected in the tree
169 					if (requestKind == REQUEST_SHOW_TOCS) {
170 						isSelected = toc == 0;
171 					} else if (requestKind == REQUEST_SHOW_CHILDREN) {
172 						isSelected = tocData.getRootPath() == null;
173 					}
174 					serializeToc(tocData.getTocs()[toc], toc, topicPath, isSelected);
175 				}
176 			}
177 		}
178 
serializeToc(IToc toc, int tocIndex, ITopic[] topicPath, boolean isSelected)179 		private void serializeToc(IToc toc, int tocIndex, ITopic[] topicPath, boolean isSelected) {
180 
181 			if (!ScopeUtils.showInTree(toc, scope)) {
182 				// do not generate toc when there are no leaf topics or if it is filtered out
183 				return;
184 			}
185 			ITopic[] topics = toc.getTopics();
186 
187 			if (requestKind == REQUEST_SHOW_CHILDREN) {
188 				topicPath = tocData.getTopicPathFromRootPath(toc);
189 			}
190 
191 			buf.append("<node"); //$NON-NLS-1$
192 			if (toc.getLabel() != null) {
193 				buf.append('\n' + "      title=\"" + XMLGenerator.xmlEscape(toc.getLabel()) + '"'); //$NON-NLS-1$
194 			}
195 			buf.append('\n' + "      id=\"" + XMLGenerator.xmlEscape(toc.getHref()) + "\""); //$NON-NLS-1$ //$NON-NLS-2$
196 
197 			String href = fixupHref(toc.getTopic(null).getHref(), "" + tocIndex); //$NON-NLS-1$
198 			buf.append('\n' + "      href=\"" + XMLGenerator.xmlEscape(UrlUtil.getHelpURL(href)) + "\""); //$NON-NLS-1$ //$NON-NLS-2$
199 
200 			buf.append(createTocImageTag(toc));
201 
202 			boolean serializeChildren = true;
203 			if (requestKind == REQUEST_SHOW_TOCS) {
204 				serializeChildren = false;
205 			}
206 			if (requestKind == REQUEST_EXPAND_PATH && topicPath.length == 0) {
207 				serializeChildren = false;
208 				buf.append('\n' + "      is_selected=\"true\"" ); //$NON-NLS-1$
209 				buf.append('\n' + "      is_highlighted=\"true\"" ); //$NON-NLS-1$
210 			}
211 			buf.append(">\n"); //$NON-NLS-1$
212 			if (serializeChildren) {
213 				serializeChildTopics(topics, topicPath, "", isSelected); //$NON-NLS-1$
214 			}
215 			buf.append("</node>\n"); //$NON-NLS-1$
216 
217 		}
218 
serializeTopic(ITopic topic, ITopic[] topicPath, boolean isSelected, String parentPath)219 		private void serializeTopic(ITopic topic, ITopic[] topicPath, boolean isSelected, String parentPath)  {
220 			ITopic[] subtopics = topic.getSubtopics();
221 			boolean isLeaf = !ScopeUtils.hasInScopeDescendent(topic, scope);
222 			buf.append("<node"); //$NON-NLS-1$
223 			if (topic.getLabel() != null) {
224 				buf.append('\n'	+ "      title=\"" + XMLGenerator.xmlEscape(topic.getLabel()) + '"'); //$NON-NLS-1$
225 			}
226 
227 			buf.append('\n' + "      id=\"" + parentPath + "\""); //$NON-NLS-1$ //$NON-NLS-2$
228 
229 			String href = topic.getHref();
230 			href = fixupHref(href, "" + tocData.getSelectedToc() + '_' + parentPath); //$NON-NLS-1$
231 			buf.append('\n' + "      href=\"" + XMLGenerator.xmlEscape( //$NON-NLS-1$
232 					UrlUtil.getHelpURL(href)) + '"');
233 			if (isLeaf ) {
234 				buf.append('\n' + "      is_leaf=\"true\"" ); //$NON-NLS-1$
235 			}
236 			if (isSelected && requestKind == REQUEST_EXPAND_PATH) {
237 				buf.append('\n' + "      is_selected=\"true\"" ); //$NON-NLS-1$
238 				buf.append('\n' + "      is_highlighted=\"true\"" ); //$NON-NLS-1$
239 			}
240 			String imageTags = createTopicImageTags(topic, isLeaf);
241 			buf.append(imageTags);
242 
243 			buf.append(">\n"); //$NON-NLS-1$
244 			serializeChildTopics(subtopics, topicPath, parentPath, isSelected);
245 			buf.append("</node>\n"); //$NON-NLS-1$
246 		}
247 
createTocImageTag(IToc toc)248 		private String createTocImageTag(IToc toc) {
249 			if (toc instanceof Toc) {
250 				String icon = ((Toc) toc).getIcon();
251 
252 				if (IconFinder.isIconDefined(icon)) {
253 					String openIcon = IconFinder.getImagePathFromId(icon, IconFinder.TYPEICON_OPEN);
254 					String closedIcon = IconFinder.getImagePathFromId(icon, IconFinder.TYPEICON_CLOSED);
255 					String imageTags = '\n' + "      openImage=\"/"+ openIcon + "\""; //$NON-NLS-1$ //$NON-NLS-2$
256 					if (!openIcon.equals(closedIcon)) {
257 						imageTags += '\n' + "      closedImage=\"/" + closedIcon + "\""; //$NON-NLS-1$ //$NON-NLS-2$
258 					}
259 					String altText = IconFinder.getIconAltFromId(icon);
260 					if(altText != null) {
261 						imageTags += '\n' + "      imageAlt=\""+ altText + "\""; //$NON-NLS-1$ //$NON-NLS-2$
262 					}
263 					return imageTags;
264 				}
265 			}
266 			return '\n' + "      image=\"toc_closed\""; //$NON-NLS-1$
267 		}
268 
createTopicImageTags(ITopic topic, boolean isLeaf)269 		private String createTopicImageTags(ITopic topic, boolean isLeaf) {
270 			if (topic instanceof Topic) {
271 				String icon = ((Topic) topic).getIcon();
272 				String altText = IconFinder.getIconAltFromId(icon);
273 
274 				if (IconFinder.isIconDefined(icon)) {
275 					String imageTags;
276 					if (isLeaf) {
277 						imageTags = '\n' + "      openImage=\"/" +IconFinder.getImagePathFromId(icon, IconFinder.TYPEICON_LEAF) + "\"";   //$NON-NLS-1$//$NON-NLS-2$
278 					} else {
279 						String openIcon = IconFinder.getImagePathFromId(icon, IconFinder.TYPEICON_OPEN);
280 						String closedIcon = IconFinder.getImagePathFromId(icon, IconFinder.TYPEICON_CLOSED);
281 						imageTags = '\n' + "      openImage=\"/" + openIcon+ "\""; //$NON-NLS-1$ //$NON-NLS-2$
282 						if (!openIcon.equals(closedIcon)) {
283 							imageTags += '\n' + "      closedImage=\"/" +  closedIcon + "\""; //$NON-NLS-1$ //$NON-NLS-2$
284 						}
285 					}
286 					if(altText != null) {
287 						imageTags += '\n' + "      imageAlt=\""+ altText + "\""; //$NON-NLS-1$ //$NON-NLS-2$
288 					}
289 					return imageTags;
290 				}
291 			}
292 			String icon;
293 			if (isLeaf) {
294 				icon = "topic"; //$NON-NLS-1$
295 			} else if (topic.getHref() == null) {
296 				icon = "container_obj"; //$NON-NLS-1$
297 			} else {
298 				icon = "container_topic"; //$NON-NLS-1$
299 			}
300 			String imageTags = '\n' + "      image=\"" + icon + "\""; //$NON-NLS-1$ //$NON-NLS-2$
301 			return imageTags;
302 		}
303 
serializeChildTopics(ITopic[] childTopics, ITopic[] topicPath, String parentPath, boolean parentIsSelected)304 		private void serializeChildTopics(ITopic[] childTopics, ITopic[] topicPath, String parentPath, boolean parentIsSelected) {
305 			if (parentIsSelected && requestKind == REQUEST_SHOW_CHILDREN) {
306 				// Show the children of this node
307 				for (int subtopic = 0; subtopic < childTopics.length; subtopic++) {
308 					ITopic childTopic = childTopics[subtopic];
309 					if (ScopeUtils.showInTree(childTopic, scope)) {
310 						serializeTopic(childTopic, null, false, addSuffix(parentPath, subtopic));
311 					}
312 				}
313 			} else if (topicPath != null) {
314 				for (int subtopic = 0; subtopic < childTopics.length; subtopic++) {
315 					ITopic childTopic = childTopics[subtopic];
316 					if (ScopeUtils.showInTree(childTopic, scope)) {
317 						if (topicPath[0].getLabel().equals(childTopic.getLabel())) {
318 							ITopic[] newPath = null;
319 							if (topicPath.length > 1) {
320 								newPath = new ITopic[topicPath.length - 1];
321 								System.arraycopy(topicPath, 1, newPath, 0, topicPath.length - 1);
322 							}
323 							serializeTopic(childTopic, newPath, topicPath.length == 1, addSuffix(parentPath, subtopic));
324 						} else {
325 							serializeTopic(childTopic, null, false, addSuffix(parentPath, subtopic));
326 						}
327 					}
328 				}
329 			}
330 		}
331 
addSuffix(String parentPath, int subtopic)332 		private String addSuffix(String parentPath, int subtopic) {
333 			if (parentPath.length() == 0) {
334 				return parentPath + subtopic;
335 			}
336 			return parentPath + '_' + subtopic;
337 		}
338 	}
339 
340 	/*
341 	 * Add an extra parameter which represents the path within the tree. This enables show
342 	 * in Toc, print selected topic and search selected topic and all subtopics to work
343 	 * correctly even if the same page appears more than once in the table of contents, Bug 330868
344 	 * Static for testing purposes
345 	 */
fixupHref(String href, String path)346 	public static String  fixupHref(String href, String path) {
347 		if (href == null) {
348 			return "/../nav/" + path; //$NON-NLS-1$
349 		}
350 		int aIndex = href.indexOf('#');
351 		String anchorPart = ""; //$NON-NLS-1$
352 		String hrefPart = href;
353 		if (aIndex > 0) {
354 			anchorPart = href.substring(aIndex);
355 			hrefPart = href.substring(0, aIndex);
356 		}
357 
358 		int questionIndex = href.indexOf('?');
359 		if  (questionIndex > 0 ) {
360 			return hrefPart + "&" + TocData.COMPLETE_PATH_PARAM + '=' + path + anchorPart; //$NON-NLS-1$
361 		} else {
362 			return hrefPart + "?" + TocData.COMPLETE_PATH_PARAM + '=' + path + anchorPart; //$NON-NLS-1$
363 		}
364 
365 	}
366 
367 }
368