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