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