Merge commit '7a455971fed716123933d0f685a0d6eebcf3282b'
[nikiroo-utils.git] / src / jexer / help / HelpFile.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.help;
30
31 import java.io.InputStream;
32 import java.io.IOException;
33 import java.text.MessageFormat;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.ResourceBundle;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41 import javax.xml.parsers.DocumentBuilderFactory;
42 import javax.xml.parsers.DocumentBuilder;
43 import javax.xml.parsers.ParserConfigurationException;
44 import org.xml.sax.SAXException;
45 import org.w3c.dom.Document;
46 import org.w3c.dom.Element;
47 import org.w3c.dom.NamedNodeMap;
48 import org.w3c.dom.Node;
49 import org.w3c.dom.NodeList;
50
51 /**
52 * A HelpFile is a collection of Topics with a table of contents and index of
53 * relevant terms.
54 */
55 public class HelpFile {
56
57 /**
58 * Translated strings.
59 */
60 private static final ResourceBundle i18n = ResourceBundle.getBundle(HelpFile.class.getName());
61
62 // ------------------------------------------------------------------------
63 // Variables --------------------------------------------------------------
64 // ------------------------------------------------------------------------
65
66 /**
67 * The XML factory.
68 */
69 private static DocumentBuilder domBuilder;
70
71 /**
72 * The map of topics by title.
73 */
74 private HashMap<String, Topic> topicsByTitle;
75
76 /**
77 * The map of topics by index key term.
78 */
79 private HashMap<String, Topic> topicsByTerm;
80
81 /**
82 * The special "table of contents" topic.
83 */
84 private Topic tableOfContents;
85
86 /**
87 * The special "index" topic.
88 */
89 private Topic index;
90
91 /**
92 * The name of this help file.
93 */
94 private String name = "";
95
96 /**
97 * The version of this help file.
98 */
99 private String version = "";
100
101 /**
102 * The help file author.
103 */
104 private String author = "";
105
106 /**
107 * The help file copyright/written by date.
108 */
109 private String date = "";
110
111 // ------------------------------------------------------------------------
112 // Constructors -----------------------------------------------------------
113 // ------------------------------------------------------------------------
114
115 // ------------------------------------------------------------------------
116 // HelpFile ---------------------------------------------------------------
117 // ------------------------------------------------------------------------
118
119 /**
120 * Load a help file from an input stream.
121 *
122 * @param input the input strem
123 * @throws IOException if an I/O error occurs
124 * @throws ParserConfigurationException if no XML parser is available
125 * @throws SAXException if XML parsing fails
126 */
127 public void load(final InputStream input) throws IOException,
128 ParserConfigurationException, SAXException {
129
130 topicsByTitle = new HashMap<String, Topic>();
131 topicsByTerm = new HashMap<String, Topic>();
132
133 try {
134 loadTopics(input);
135 } finally {
136 // Always generate the TOC and Index from what was read.
137 generateTableOfContents();
138 generateIndex();
139 }
140 }
141
142 /**
143 * Get a topic by title.
144 *
145 * @param title the title for the topic
146 * @return the topic, or the "not found" topic if title is not found
147 */
148 public Topic getTopic(final String title) {
149 Topic topic = topicsByTitle.get(title);
150 if (topic == null) {
151 return Topic.NOT_FOUND;
152 }
153 return topic;
154 }
155
156 /**
157 * Get the special "search results" topic.
158 *
159 * @param searchString a regular expression search string
160 * @return an index topic containing topics with text that matches the
161 * search string
162 */
163 public Topic getSearchResults(final String searchString) {
164 List<Topic> allTopics = new ArrayList<Topic>();
165 allTopics.addAll(topicsByTitle.values());
166 Collections.sort(allTopics);
167
168 List<Topic> results = new ArrayList<Topic>();
169 Pattern pattern = Pattern.compile(searchString);
170 Pattern patternLower = Pattern.compile(searchString.toLowerCase());
171
172 for (Topic topic: allTopics) {
173 Matcher match = pattern.matcher(topic.getText().toLowerCase());
174 if (match.find()) {
175 results.add(topic);
176 continue;
177 }
178 match = pattern.matcher(topic.getTitle().toLowerCase());
179 if (match.find()) {
180 results.add(topic);
181 continue;
182 }
183 match = patternLower.matcher(topic.getText().toLowerCase());
184 if (match.find()) {
185 results.add(topic);
186 continue;
187 }
188 match = patternLower.matcher(topic.getTitle().toLowerCase());
189 if (match.find()) {
190 results.add(topic);
191 continue;
192 }
193 }
194
195 StringBuilder text = new StringBuilder();
196 int wordIndex = 0;
197 List<Link> links = new ArrayList<Link>();
198 for (Topic topic: results) {
199 text.append(topic.getTitle());
200 text.append("\n\n");
201
202 Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
203 wordIndex += link.getWordCount();
204 links.add(link);
205 }
206
207 return new Topic(MessageFormat.format(i18n.getString("searchResults"),
208 searchString), text.toString(), links);
209 }
210
211 /**
212 * Get the special "table of contents" topic.
213 *
214 * @return the table of contents topic
215 */
216 public Topic getTableOfContents() {
217 return tableOfContents;
218 }
219
220 /**
221 * Get the special "index" topic.
222 *
223 * @return the index topic
224 */
225 public Topic getIndex() {
226 return index;
227 }
228
229 /**
230 * Generate the table of contents topic.
231 */
232 private void generateTableOfContents() {
233 List<Topic> allTopics = new ArrayList<Topic>();
234 allTopics.addAll(topicsByTitle.values());
235 Collections.sort(allTopics);
236
237 StringBuilder text = new StringBuilder();
238 int wordIndex = 0;
239 List<Link> links = new ArrayList<Link>();
240 for (Topic topic: allTopics) {
241 text.append(topic.getTitle());
242 text.append("\n\n");
243
244 Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
245 wordIndex += link.getWordCount();
246 links.add(link);
247 }
248
249 tableOfContents = new Topic(i18n.getString("tableOfContents"),
250 text.toString(), links);
251 }
252
253 /**
254 * Generate the index topic.
255 */
256 private void generateIndex() {
257 List<Topic> allTopics = new ArrayList<Topic>();
258 allTopics.addAll(topicsByTitle.values());
259
260 HashMap<String, ArrayList<Topic>> allKeys;
261 allKeys = new HashMap<String, ArrayList<Topic>>();
262 for (Topic topic: allTopics) {
263 for (String key: topic.getIndexKeys()) {
264 key = key.toLowerCase();
265 ArrayList<Topic> topics = allKeys.get(key);
266 if (topics == null) {
267 topics = new ArrayList<Topic>();
268 allKeys.put(key, topics);
269 }
270 topics.add(topic);
271 }
272 }
273 List<String> keys = new ArrayList<String>();
274 keys.addAll(allKeys.keySet());
275 Collections.sort(keys);
276
277 StringBuilder text = new StringBuilder();
278 int wordIndex = 0;
279 List<Link> links = new ArrayList<Link>();
280
281 for (String key: keys) {
282 List<Topic> topics = allKeys.get(key);
283 assert (topics != null);
284 for (Topic topic: topics) {
285 String line = String.format("%15s %15s", key, topic.getTitle());
286 text.append(line);
287 text.append("\n\n");
288
289 wordIndex += key.split("\\s+").length;
290 Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
291 wordIndex += link.getWordCount();
292 links.add(link);
293 }
294 }
295
296 index = new Topic(i18n.getString("index"), text.toString(), links);
297 }
298
299 /**
300 * Load topics from a help file into the topics pool.
301 *
302 * @param input the input strem
303 * @throws IOException if an I/O error occurs
304 * @throws ParserConfigurationException if no XML parser is available
305 * @throws SAXException if XML parsing fails
306 */
307 private void loadTopics(final InputStream input) throws IOException,
308 ParserConfigurationException, SAXException {
309
310 if (domBuilder == null) {
311 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.
312 newInstance();
313 domBuilder = dbFactory.newDocumentBuilder();
314 }
315 Document doc = domBuilder.parse(input);
316
317 // Get the document's root XML node
318 Node root = doc.getChildNodes().item(0);
319 NodeList level1 = root.getChildNodes();
320 for (int i = 0; i < level1.getLength(); i++) {
321 Node node = level1.item(i);
322 String name = node.getNodeName();
323 String value = node.getTextContent();
324
325 if (name.equals("name")) {
326 this.name = value;
327 }
328 if (name.equals("version")) {
329 this.version = value;
330 }
331 if (name.equals("author")) {
332 this.author = value;
333 }
334 if (name.equals("date")) {
335 this.date = value;
336 }
337 if (name.equals("topics")) {
338 NodeList topics = node.getChildNodes();
339 for (int j = 0; j < topics.getLength(); j++) {
340 Node topic = topics.item(j);
341 addTopic(topic);
342 }
343 }
344 }
345 }
346
347 /**
348 * Add a topic to this help file.
349 *
350 * @param xmlNode the topic XML node
351 * @throws IOException if a java.io operation throws
352 */
353 private void addTopic(final Node xmlNode) throws IOException {
354 String title = "";
355 String text = "";
356
357 NamedNodeMap attributes = xmlNode.getAttributes();
358 if (attributes != null) {
359 for (int i = 0; i < attributes.getLength(); i++) {
360 Node attr = attributes.item(i);
361 if (attr.getNodeName().equals("title")) {
362 title = attr.getNodeValue().trim();
363 }
364 }
365 }
366 NodeList level2 = xmlNode.getChildNodes();
367 for (int i = 0; i < level2.getLength(); i++) {
368 Node node = level2.item(i);
369 String nodeName = node.getNodeName();
370 String nodeValue = node.getTextContent();
371 if (nodeName.equals("text")) {
372 text = nodeValue.trim();
373 }
374 }
375 if (title.length() > 0) {
376 Topic topic = new Topic(title, text);
377 topicsByTitle.put(title, topic);
378 }
379 }
380
381 }