1 /*
2 * Copyright 2003 by Paulo Soares.
3 *
4 * The contents of this file are subject to the Mozilla Public License Version 1.1
5 * (the "License"); you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at http://www.mozilla.org/MPL/
7 *
8 * Software distributed under the License is distributed on an "AS IS" basis,
9 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
10 * for the specific language governing rights and limitations under the License.
11 *
12 * The Original Code is 'iText, a free JAVA-PDF library'.
13 *
14 * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
15 * the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
16 * All Rights Reserved.
17 * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
18 * are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
19 *
20 * Contributor(s): all the names of the contributors are added in the source code
21 * where applicable.
22 *
23 * Alternatively, the contents of this file may be used under the terms of the
24 * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
25 * provisions of LGPL are applicable instead of those above. If you wish to
26 * allow use of your version of this file only under the terms of the LGPL
27 * License and not to allow others to use your version of this file under
28 * the MPL, indicate your decision by deleting the provisions above and
29 * replace them with the notice and other provisions required by the LGPL.
30 * If you do not delete the provisions above, a recipient may use your version
31 * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
32 *
33 * This library is free software; you can redistribute it and/or modify it
34 * under the terms of the MPL as stated above or under the terms of the GNU
35 * Library General Public License as published by the Free Software Foundation;
36 * either version 2 of the License, or any later version.
37 *
38 * This library is distributed in the hope that it will be useful, but WITHOUT
39 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
40 * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
41 * details.
42 *
43 * If you didn't download this code from the following link, you should check if
44 * you aren't using an obsolete version:
45 * http://www.lowagie.com/iText/
46 */
47
48 package com.lowagie.text.pdf;
49
50 import java.io.BufferedWriter;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.io.OutputStream;
54 import java.io.OutputStreamWriter;
55 import java.io.Reader;
56 import java.io.Writer;
57 import java.util.ArrayList;
58 import java.util.HashMap;
59 import java.util.Iterator;
60 import java.util.List;
61 import java.util.Map;
62 import java.util.Stack;
63 import java.util.StringTokenizer;
64
65 import com.lowagie.text.xml.simpleparser.IanaEncodings;
66 import com.lowagie.text.xml.simpleparser.SimpleXMLDocHandler;
67 import com.lowagie.text.xml.simpleparser.SimpleXMLParser;
68 /**
69 * Bookmark processing in a simple way. It has some limitations, mainly the only
70 * action types supported are GoTo, GoToR, URI and Launch.
71 * <p>
72 * The list structure is composed by a number of HashMap, keyed by strings, one HashMap
73 * for each bookmark.
74 * The element values are all strings with the exception of the key "Kids" that has
75 * another list for the child bookmarks.
76 * <p>
77 * All the bookmarks have a "Title" with the
78 * bookmark title and optionally a "Style" that can be "bold", "italic" or a
79 * combination of both. They can also have a "Color" key with a value of three
80 * floats separated by spaces. The key "Open" can have the values "true" or "false" and
81 * signals the open status of the children. It's "true" by default.
82 * <p>
83 * The actions and the parameters can be:
84 * <ul>
85 * <li>"Action" = "GoTo" - "Page" | "Named"
86 * <ul>
87 * <li>"Page" = "3 XYZ 70 400 null" - page number followed by a destination (/XYZ is also accepted)
88 * <li>"Named" = "named_destination"
89 * </ul>
90 * <li>"Action" = "GoToR" - "Page" | "Named" | "NamedN", "File", ["NewWindow"]
91 * <ul>
92 * <li>"Page" = "3 XYZ 70 400 null" - page number followed by a destination (/XYZ is also accepted)
93 * <li>"Named" = "named_destination_as_a_string"
94 * <li>"NamedN" = "named_destination_as_a_name"
95 * <li>"File" - "the_file_to_open"
96 * <li>"NewWindow" - "true" or "false"
97 * </ul>
98 * <li>"Action" = "URI" - "URI"
99 * <ul>
100 * <li>"URI" = "http://sf.net" - URI to jump to
101 * </ul>
102 * <li>"Action" = "Launch" - "File"
103 * <ul>
104 * <li>"File" - "the_file_to_open_or_execute"
105 * </ul>
106 * @author Paulo Soares (psoares@consiste.pt)
107 */
108 public final class SimpleBookmark implements SimpleXMLDocHandler {
109
110 private ArrayList topList;
111 private Stack attr = new Stack();
112
113 /** Creates a new instance of SimpleBookmark */
114 private SimpleBookmark() {
115 }
116
117 private static List bookmarkDepth(PdfReader reader, PdfDictionary outline, IntHashtable pages) {
118 ArrayList list = new ArrayList();
119 while (outline != null) {
120 HashMap map = new HashMap();
121 PdfString title = (PdfString)PdfReader.getPdfObjectRelease(outline.get(PdfName.TITLE));
122 map.put("Title", title.toUnicodeString());
123 PdfArray color = (PdfArray)PdfReader.getPdfObjectRelease(outline.get(PdfName.C));
124 if (color != null && color.getArrayList().size() == 3) {
125 ByteBuffer out = new ByteBuffer();
126 ArrayList arr = color.getArrayList();
127 out.append(((PdfNumber)arr.get(0)).floatValue()).append(' ');
128 out.append(((PdfNumber)arr.get(1)).floatValue()).append(' ');
129 out.append(((PdfNumber)arr.get(2)).floatValue());
130 map.put("Color", PdfEncodings.convertToString(out.toByteArray(), null));
131 }
132 PdfNumber style = (PdfNumber)PdfReader.getPdfObjectRelease(outline.get(PdfName.F));
133 if (style != null) {
134 int f = style.intValue();
135 String s = "";
136 if ((f & 1) != 0)
137 s += "italic ";
138 if ((f & 2) != 0)
139 s += "bold ";
140 s = s.trim();
141 if (s.length() != 0)
142 map.put("Style", s);
143 }
144 PdfNumber count = (PdfNumber)PdfReader.getPdfObjectRelease(outline.get(PdfName.COUNT));
145 if (count != null && count.intValue() < 0)
146 map.put("Open", "false");
147 try {
148 PdfObject dest = PdfReader.getPdfObjectRelease(outline.get(PdfName.DEST));
149 if (dest != null) {
150 mapGotoBookmark(map, dest, pages); //changed by ujihara 2004-06-13
151 }
152 else {
153 PdfDictionary action = (PdfDictionary)PdfReader.getPdfObjectRelease(outline.get(PdfName.A));
154 if (action != null) {
155 if (PdfName.GOTO.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) {
156 dest = PdfReader.getPdfObjectRelease(action.get(PdfName.D));
157 if (dest != null) {
158 mapGotoBookmark(map, dest, pages);
159 }
160 }
161 else if (PdfName.URI.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) {
162 map.put("Action", "URI");
163 map.put("URI", ((PdfString)PdfReader.getPdfObjectRelease(action.get(PdfName.URI))).toUnicodeString());
164 }
165 else if (PdfName.GOTOR.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) {
166 dest = PdfReader.getPdfObjectRelease(action.get(PdfName.D));
167 if (dest != null) {
168 if (dest.isString())
169 map.put("Named", dest.toString());
170 else if (dest.isName())
171 map.put("NamedN", PdfName.decodeName(dest.toString()));
172 else if (dest.isArray()) {
173 ArrayList arr = ((PdfArray)dest).getArrayList();
174 StringBuffer s = new StringBuffer();
175 s.append(arr.get(0).toString());
176 s.append(' ').append(arr.get(1).toString());
177 for (int k = 2; k < arr.size(); ++k)
178 s.append(' ').append(arr.get(k).toString());
179 map.put("Page", s.toString());
180 }
181 }
182 map.put("Action", "GoToR");
183 PdfObject file = PdfReader.getPdfObjectRelease(action.get(PdfName.F));
184 if (file != null) {
185 if (file.isString())
186 map.put("File", ((PdfString)file).toUnicodeString());
187 else if (file.isDictionary()) {
188 file = PdfReader.getPdfObject(((PdfDictionary)file).get(PdfName.F));
189 if (file.isString())
190 map.put("File", ((PdfString)file).toUnicodeString());
191 }
192 }
193 PdfObject newWindow = PdfReader.getPdfObjectRelease(action.get(PdfName.NEWWINDOW));
194 if (newWindow != null)
195 map.put("NewWindow", newWindow.toString());
196 }
197 else if (PdfName.LAUNCH.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) {
198 map.put("Action", "Launch");
199 PdfObject file = PdfReader.getPdfObjectRelease(action.get(PdfName.F));
200 if (file == null)
201 file = PdfReader.getPdfObjectRelease(action.get(PdfName.WIN));
202 if (file != null) {
203 if (file.isString())
204 map.put("File", ((PdfString)file).toUnicodeString());
205 else if (file.isDictionary()) {
206 file = PdfReader.getPdfObjectRelease(((PdfDictionary)file).get(PdfName.F));
207 if (file.isString())
208 map.put("File", ((PdfString)file).toUnicodeString());
209 }
210 }
211 }
212 }
213 }
214 }
215 catch (Exception e) {
216 //empty on purpose
217 }
218 PdfDictionary first = (PdfDictionary)PdfReader.getPdfObjectRelease(outline.get(PdfName.FIRST));
219 if (first != null) {
220 map.put("Kids", bookmarkDepth(reader, first, pages));
221 }
222 list.add(map);
223 outline = (PdfDictionary)PdfReader.getPdfObjectRelease(outline.get(PdfName.NEXT));
224 }
225 return list;
226 }
227
228 private static void mapGotoBookmark(HashMap map, PdfObject dest, IntHashtable pages)
229 {
230 if (dest.isString())
231 map.put("Named", dest.toString());
232 else if (dest.isName())
233 map.put("Named", PdfName.decodeName(dest.toString()));
234 else if (dest.isArray())
235 map.put("Page", makeBookmarkParam((PdfArray)dest, pages)); //changed by ujihara 2004-06-13
236 map.put("Action", "GoTo");
237 }
238
239 private static String makeBookmarkParam(PdfArray dest, IntHashtable pages)
240 {
241 ArrayList arr = dest.getArrayList();
242 StringBuffer s = new StringBuffer();
243 if (((PdfObject)arr.get(0)).isNumber())
244 s.append(((PdfNumber)arr.get(0)).intValue() + 1);
245 else
246 s.append(pages.get(getNumber((PdfIndirectReference)arr.get(0)))); //changed by ujihara 2004-06-13
247 s.append(' ').append(arr.get(1).toString().substring(1));
248 for (int k = 2; k < arr.size(); ++k)
249 s.append(' ').append(arr.get(k).toString());
250 return s.toString();
251 }
252
253 /**
254 * Gets number of indirect. If type of directed indirect is PAGES, it refers PAGE object through KIDS.
255 * (Contributed by Kazuya Ujihara)
256 * @param indirect
257 * 2004-06-13
258 */
259 private static int getNumber(PdfIndirectReference indirect)
260 {
261 PdfDictionary pdfObj = (PdfDictionary)PdfReader.getPdfObjectRelease(indirect);
262 if (pdfObj.contains(PdfName.TYPE) && pdfObj.get(PdfName.TYPE).equals(PdfName.PAGES) && pdfObj.contains(PdfName.KIDS))
263 {
264 PdfArray kids = (PdfArray)pdfObj.get(PdfName.KIDS);
265 indirect = (PdfIndirectReference)kids.arrayList.get(0);
266 }
267 return indirect.getNumber();
268 }
269
270 /**
271 * Gets a <CODE>List</CODE> with the bookmarks. It returns <CODE>null</CODE> if
272 * the document doesn't have any bookmarks.
273 * @param reader the document
274 * @return a <CODE>List</CODE> with the bookmarks or <CODE>null</CODE> if the
275 * document doesn't have any
276 */
277 public static List getBookmark(PdfReader reader) {
278 PdfDictionary catalog = reader.getCatalog();
279 PdfObject obj = PdfReader.getPdfObjectRelease(catalog.get(PdfName.OUTLINES));
280 if (obj == null || !obj.isDictionary())
281 return null;
282 PdfDictionary outlines = (PdfDictionary)obj;
283 IntHashtable pages = new IntHashtable();
284 int numPages = reader.getNumberOfPages();
285 for (int k = 1; k <= numPages; ++k) {
286 pages.put(reader.getPageOrigRef(k).getNumber(), k);
287 reader.releasePage(k);
288 }
289 return bookmarkDepth(reader, (PdfDictionary)PdfReader.getPdfObjectRelease(outlines.get(PdfName.FIRST)), pages);
290 }
291
292 /**
293 * Removes the bookmark entries for a number of page ranges. The page ranges
294 * consists of a number of pairs with the start/end page range. The page numbers
295 * are inclusive.
296 * @param list the bookmarks
297 * @param pageRange the page ranges, always in pairs.
298 */
299 public static void eliminatePages(List list, int pageRange[]) {
300 if (list == null)
301 return;
302 for (Iterator it = list.listIterator(); it.hasNext();) {
303 HashMap map = (HashMap)it.next();
304 boolean hit = false;
305 if ("GoTo".equals(map.get("Action"))) {
306 String page = (String)map.get("Page");
307 if (page != null) {
308 page = page.trim();
309 int idx = page.indexOf(' ');
310 int pageNum;
311 if (idx < 0)
312 pageNum = Integer.parseInt(page);
313 else
314 pageNum = Integer.parseInt(page.substring(0, idx));
315 int len = pageRange.length & 0xfffffffe;
316 for (int k = 0; k < len; k += 2) {
317 if (pageNum >= pageRange[k] && pageNum <= pageRange[k + 1]) {
318 hit = true;
319 break;
320 }
321 }
322 }
323 }
324 List kids = (List)map.get("Kids");
325 if (kids != null) {
326 eliminatePages(kids, pageRange);
327 if (kids.isEmpty()) {
328 map.remove("Kids");
329 kids = null;
330 }
331 }
332 if (hit) {
333 if (kids == null)
334 it.remove();
335 else {
336 map.remove("Action");
337 map.remove("Page");
338 map.remove("Named");
339 }
340 }
341 }
342 }
343
344 /**
345 * For the pages in range add the <CODE>pageShift</CODE> to the page number.
346 * The page ranges
347 * consists of a number of pairs with the start/end page range. The page numbers
348 * are inclusive.
349 * @param list the bookmarks
350 * @param pageShift the number to add to the pages in range
351 * @param pageRange the page ranges, always in pairs. It can be <CODE>null</CODE>
352 * to include all the pages
353 */
354 public static void shiftPageNumbers(List list, int pageShift, int pageRange[]) {
355 if (list == null)
356 return;
357 for (Iterator it = list.listIterator(); it.hasNext();) {
358 HashMap map = (HashMap)it.next();
359 if ("GoTo".equals(map.get("Action"))) {
360 String page = (String)map.get("Page");
361 if (page != null) {
362 page = page.trim();
363 int idx = page.indexOf(' ');
364 int pageNum;
365 if (idx < 0)
366 pageNum = Integer.parseInt(page);
367 else
368 pageNum = Integer.parseInt(page.substring(0, idx));
369 boolean hit = false;
370 if (pageRange == null)
371 hit = true;
372 else {
373 int len = pageRange.length & 0xfffffffe;
374 for (int k = 0; k < len; k += 2) {
375 if (pageNum >= pageRange[k] && pageNum <= pageRange[k + 1]) {
376 hit = true;
377 break;
378 }
379 }
380 }
381 if (hit) {
382 if (idx < 0)
383 page = Integer.toString(pageNum + pageShift);
384 else
385 page = (pageNum + pageShift) + page.substring(idx);
386 }
387 map.put("Page", page);
388 }
389 }
390 List kids = (List)map.get("Kids");
391 if (kids != null)
392 shiftPageNumbers(kids, pageShift, pageRange);
393 }
394 }
395
396 static void createOutlineAction(PdfDictionary outline, HashMap map, PdfWriter writer, boolean namedAsNames) {
397 try {
398 String action = (String)map.get("Action");
399 if ("GoTo".equals(action)) {
400 String p;
401 if ((p = (String)map.get("Named")) != null) {
402 if (namedAsNames)
403 outline.put(PdfName.DEST, new PdfName(p));
404 else
405 outline.put(PdfName.DEST, new PdfString(p, null));
406 }
407 else if ((p = (String)map.get("Page")) != null) {
408 PdfArray ar = new PdfArray();
409 StringTokenizer tk = new StringTokenizer(p);
410 int n = Integer.parseInt(tk.nextToken());
411 ar.add(writer.getPageReference(n));
412 if (!tk.hasMoreTokens()) {
413 ar.add(PdfName.XYZ);
414 ar.add(new float[]{0, 10000, 0});
415 }
416 else {
417 String fn = tk.nextToken();
418 if (fn.startsWith("/"))
419 fn = fn.substring(1);
420 ar.add(new PdfName(fn));
421 for (int k = 0; k < 4 && tk.hasMoreTokens(); ++k) {
422 fn = tk.nextToken();
423 if (fn.equals("null"))
424 ar.add(PdfNull.PDFNULL);
425 else
426 ar.add(new PdfNumber(fn));
427 }
428 }
429 outline.put(PdfName.DEST, ar);
430 }
431 }
432 else if ("GoToR".equals(action)) {
433 String p;
434 PdfDictionary dic = new PdfDictionary();
435 if ((p = (String)map.get("Named")) != null)
436 dic.put(PdfName.D, new PdfString(p, null));
437 else if ((p = (String)map.get("NamedN")) != null)
438 dic.put(PdfName.D, new PdfName(p));
439 else if ((p = (String)map.get("Page")) != null){
440 PdfArray ar = new PdfArray();
441 StringTokenizer tk = new StringTokenizer(p);
442 ar.add(new PdfNumber(tk.nextToken()));
443 if (!tk.hasMoreTokens()) {
444 ar.add(PdfName.XYZ);
445 ar.add(new float[]{0, 10000, 0});
446 }
447 else {
448 String fn = tk.nextToken();
449 if (fn.startsWith("/"))
450 fn = fn.substring(1);
451 ar.add(new PdfName(fn));
452 for (int k = 0; k < 4 && tk.hasMoreTokens(); ++k) {
453 fn = tk.nextToken();
454 if (fn.equals("null"))
455 ar.add(PdfNull.PDFNULL);
456 else
457 ar.add(new PdfNumber(fn));
458 }
459 }
460 dic.put(PdfName.D, ar);
461 }
462 String file = (String)map.get("File");
463 if (dic.size() > 0 && file != null) {
464 dic.put(PdfName.S, PdfName.GOTOR);
465 dic.put(PdfName.F, new PdfString(file));
466 String nw = (String)map.get("NewWindow");
467 if (nw != null) {
468 if (nw.equals("true"))
469 dic.put(PdfName.NEWWINDOW, PdfBoolean.PDFTRUE);
470 else if (nw.equals("false"))
471 dic.put(PdfName.NEWWINDOW, PdfBoolean.PDFFALSE);
472 }
473 outline.put(PdfName.A, dic);
474 }
475 }
476 else if ("URI".equals(action)) {
477 String uri = (String)map.get("URI");
478 if (uri != null) {
479 PdfDictionary dic = new PdfDictionary();
480 dic.put(PdfName.S, PdfName.URI);
481 dic.put(PdfName.URI, new PdfString(uri));
482 outline.put(PdfName.A, dic);
483 }
484 }
485 else if ("Launch".equals(action)) {
486 String file = (String)map.get("File");
487 if (file != null) {
488 PdfDictionary dic = new PdfDictionary();
489 dic.put(PdfName.S, PdfName.LAUNCH);
490 dic.put(PdfName.F, new PdfString(file));
491 outline.put(PdfName.A, dic);
492 }
493 }
494 }
495 catch (Exception e) {
496 // empty on purpose
497 }
498 }
499
500 public static Object[] iterateOutlines(PdfWriter writer, PdfIndirectReference parent, List kids, boolean namedAsNames) throws IOException {
501 PdfIndirectReference refs[] = new PdfIndirectReference[kids.size()];
502 for (int k = 0; k < refs.length; ++k)
503 refs[k] = writer.getPdfIndirectReference();
504 int ptr = 0;
505 int count = 0;
506 for (Iterator it = kids.listIterator(); it.hasNext(); ++ptr) {
507 HashMap map = (HashMap)it.next();
508 Object lower[] = null;
509 List subKid = (List)map.get("Kids");
510 if (subKid != null && !subKid.isEmpty())
511 lower = iterateOutlines(writer, refs[ptr], subKid, namedAsNames);
512 PdfDictionary outline = new PdfDictionary();
513 ++count;
514 if (lower != null) {
515 outline.put(PdfName.FIRST, (PdfIndirectReference)lower[0]);
516 outline.put(PdfName.LAST, (PdfIndirectReference)lower[1]);
517 int n = ((Integer)lower[2]).intValue();
518 if ("false".equals(map.get("Open"))) {
519 outline.put(PdfName.COUNT, new PdfNumber(-n));
520 }
521 else {
522 outline.put(PdfName.COUNT, new PdfNumber(n));
523 count += n;
524 }
525 }
526 outline.put(PdfName.PARENT, parent);
527 if (ptr > 0)
528 outline.put(PdfName.PREV, refs[ptr - 1]);
529 if (ptr < refs.length - 1)
530 outline.put(PdfName.NEXT, refs[ptr + 1]);
531 outline.put(PdfName.TITLE, new PdfString((String)map.get("Title"), PdfObject.TEXT_UNICODE));
532 String color = (String)map.get("Color");
533 if (color != null) {
534 try {
535 PdfArray arr = new PdfArray();
536 StringTokenizer tk = new StringTokenizer(color);
537 for (int k = 0; k < 3; ++k) {
538 float f = Float.parseFloat(tk.nextToken());
539 if (f < 0) f = 0;
540 if (f > 1) f = 1;
541 arr.add(new PdfNumber(f));
542 }
543 outline.put(PdfName.C, arr);
544 } catch(Exception e){} //in case it's malformed
545 }
546 String style = (String)map.get("Style");
547 if (style != null) {
548 style = style.toLowerCase();
549 int bits = 0;
550 if (style.indexOf("italic") >= 0)
551 bits |= 1;
552 if (style.indexOf("bold") >= 0)
553 bits |= 2;
554 if (bits != 0)
555 outline.put(PdfName.F, new PdfNumber(bits));
556 }
557 createOutlineAction(outline, map, writer, namedAsNames);
558 writer.addToBody(outline, refs[ptr]);
559 }
560 return new Object[]{refs[0], refs[refs.length - 1], new Integer(count)};
561 }
562
563 /**
564 * Exports the bookmarks to XML. Only of use if the generation is to be include in
565 * some other XML document.
566 * @param list the bookmarks
567 * @param out the export destination. The writer is not closed
568 * @param indent the indentation level. Pretty printing significant only
569 * @param onlyASCII codes above 127 will always be escaped with &#nn; if <CODE>true</CODE>,
570 * whatever the encoding
571 * @throws IOException on error
572 */
573 public static void exportToXMLNode(List list, Writer out, int indent, boolean onlyASCII) throws IOException {
574 String dep = "";
575 for (int k = 0; k < indent; ++k)
576 dep += " ";
577 for (Iterator it = list.iterator(); it.hasNext();) {
578 HashMap map = (HashMap)it.next();
579 String title = null;
580 out.write(dep);
581 out.write("<Title ");
582 List kids = null;
583 for (Iterator e = map.entrySet().iterator(); e.hasNext();) {
584 Map.Entry entry = (Map.Entry) e.next();
585 String key = (String) entry.getKey();
586 if (key.equals("Title")) {
587 title = (String) entry.getValue();
588 continue;
589 }
590 else if (key.equals("Kids")) {
591 kids = (List) entry.getValue();
592 continue;
593 }
594 else {
595 out.write(key);
596 out.write("=\"");
597 String value = (String) entry.getValue();
598 if (key.equals("Named") || key.equals("NamedN"))
599 value = SimpleNamedDestination.escapeBinaryString(value);
600 out.write(SimpleXMLParser.escapeXML(value, onlyASCII));
601 out.write("\" ");
602 }
603 }
604 out.write(">");
605 if (title == null)
606 title = "";
607 out.write(SimpleXMLParser.escapeXML(title, onlyASCII));
608 if (kids != null) {
609 out.write("\n");
610 exportToXMLNode(kids, out, indent + 1, onlyASCII);
611 out.write(dep);
612 }
613 out.write("</Title>\n");
614 }
615 }
616
617 /**
618 * Exports the bookmarks to XML. The DTD for this XML is:
619 * <p>
620 * <pre>
621 * <?xml version='1.0' encoding='UTF-8'?>
622 * <!ELEMENT Title (#PCDATA|Title)*>
623 * <!ATTLIST Title
624 * Action CDATA #IMPLIED
625 * Open CDATA #IMPLIED
626 * Page CDATA #IMPLIED
627 * URI CDATA #IMPLIED
628 * File CDATA #IMPLIED
629 * Named CDATA #IMPLIED
630 * NamedN CDATA #IMPLIED
631 * NewWindow CDATA #IMPLIED
632 * Style CDATA #IMPLIED
633 * Color CDATA #IMPLIED
634 * >
635 * <!ELEMENT Bookmark (Title)*>
636 * </pre>
637 * @param list the bookmarks
638 * @param out the export destination. The stream is not closed
639 * @param encoding the encoding according to IANA conventions
640 * @param onlyASCII codes above 127 will always be escaped with &#nn; if <CODE>true</CODE>,
641 * whatever the encoding
642 * @throws IOException on error
643 */
644 public static void exportToXML(List list, OutputStream out, String encoding, boolean onlyASCII) throws IOException {
645 String jenc = IanaEncodings.getJavaEncoding(encoding);
646 Writer wrt = new BufferedWriter(new OutputStreamWriter(out, jenc));
647 exportToXML(list, wrt, encoding, onlyASCII);
648 }
649
650 /**
651 * Exports the bookmarks to XML.
652 * @param list the bookmarks
653 * @param wrt the export destination. The writer is not closed
654 * @param encoding the encoding according to IANA conventions
655 * @param onlyASCII codes above 127 will always be escaped with &#nn; if <CODE>true</CODE>,
656 * whatever the encoding
657 * @throws IOException on error
658 */
659 public static void exportToXML(List list, Writer wrt, String encoding, boolean onlyASCII) throws IOException {
660 wrt.write("<?xml version=\"1.0\" encoding=\"");
661 wrt.write(SimpleXMLParser.escapeXML(encoding, onlyASCII));
662 wrt.write("\"?>\n<Bookmark>\n");
663 exportToXMLNode(list, wrt, 1, onlyASCII);
664 wrt.write("</Bookmark>\n");
665 wrt.flush();
666 }
667
668 /**
669 * Import the bookmarks from XML.
670 * @param in the XML source. The stream is not closed
671 * @throws IOException on error
672 * @return the bookmarks
673 */
674 public static List importFromXML(InputStream in) throws IOException {
675 SimpleBookmark book = new SimpleBookmark();
676 SimpleXMLParser.parse(book, in);
677 return book.topList;
678 }
679
680 /**
681 * Import the bookmarks from XML.
682 * @param in the XML source. The reader is not closed
683 * @throws IOException on error
684 * @return the bookmarks
685 */
686 public static List importFromXML(Reader in) throws IOException {
687 SimpleBookmark book = new SimpleBookmark();
688 SimpleXMLParser.parse(book, in);
689 return book.topList;
690 }
691
692 public void endDocument() {
693 }
694
695 public void endElement(String tag) {
696 if (tag.equals("Bookmark")) {
697 if (attr.isEmpty())
698 return;
699 else
700 throw new RuntimeException("Bookmark end tag out of place.");
701 }
702 if (!tag.equals("Title"))
703 throw new RuntimeException("Invalid end tag - " + tag);
704 HashMap attributes = (HashMap)attr.pop();
705 String title = (String)attributes.get("Title");
706 attributes.put("Title", title.trim());
707 String named = (String)attributes.get("Named");
708 if (named != null)
709 attributes.put("Named", SimpleNamedDestination.unEscapeBinaryString(named));
710 named = (String)attributes.get("NamedN");
711 if (named != null)
712 attributes.put("NamedN", SimpleNamedDestination.unEscapeBinaryString(named));
713 if (attr.isEmpty())
714 topList.add(attributes);
715 else {
716 HashMap parent = (HashMap)attr.peek();
717 List kids = (List)parent.get("Kids");
718 if (kids == null) {
719 kids = new ArrayList();
720 parent.put("Kids", kids);
721 }
722 kids.add(attributes);
723 }
724 }
725
726 public void startDocument() {
727 }
728
729 public void startElement(String tag, HashMap h) {
730 if (topList == null) {
731 if (tag.equals("Bookmark")) {
732 topList = new ArrayList();
733 return;
734 }
735 else
736 throw new RuntimeException("Root element is not Bookmark: " + tag);
737 }
738 if (!tag.equals("Title"))
739 throw new RuntimeException("Tag " + tag + " not allowed.");
740 HashMap attributes = new HashMap(h);
741 attributes.put("Title", "");
742 attributes.remove("Kids");
743 attr.push(attributes);
744 }
745
746 public void text(String str) {
747 if (attr.isEmpty())
748 return;
749 HashMap attributes = (HashMap)attr.peek();
750 String title = (String)attributes.get("Title");
751 title += str;
752 attributes.put("Title", title);
753 }
754 }