/* * * This file is part of the iText (R) project. Copyright (c) 1998-2017 iText Group NV * Authors: Bruno Lowagie, Paulo Soares, et al. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License version 3 * as published by the Free Software Foundation with the addition of the * following permission added to Section 15 as permitted in Section 7(a): * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT * OF THIRD PARTY RIGHTS * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA, 02110-1301 USA, or download the license from the following URL: * http://itextpdf.com/terms-of-use/ * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License. * * In accordance with Section 7(b) of the GNU Affero General Public License, * a covered work must retain the producer line in every PDF that is created * or manipulated using iText. * * You can be released from the requirements of the license by purchasing * a commercial license. Buying such a license is mandatory as soon as you * develop commercial activities involving the iText software without * disclosing the source code of your own applications. * These activities include: offering paid services to customers as an ASP, * serving PDFs on the fly in a web application, shipping iText with a closed * source product. * * For more information, please contact iText Software Corp. at this * address: sales@itextpdf.com */ package com.itextpdf.text.pdf; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.StringTokenizer; import com.itextpdf.text.error_messages.MessageLocalization; import com.itextpdf.text.xml.XMLUtil; import com.itextpdf.text.xml.simpleparser.IanaEncodings; import com.itextpdf.text.xml.simpleparser.SimpleXMLDocHandler; import com.itextpdf.text.xml.simpleparser.SimpleXMLParser; /** * Bookmark processing in a simple way. It has some limitations, mainly the only * action types supported are GoTo, GoToR, URI and Launch. * <p> * The list structure is composed by a number of HashMap, keyed by strings, one HashMap * for each bookmark. * The element values are all strings with the exception of the key "Kids" that has * another list for the child bookmarks. * <p> * All the bookmarks have a "Title" with the * bookmark title and optionally a "Style" that can be "bold", "italic" or a * combination of both. They can also have a "Color" key with a value of three * floats with values in the range 0.0 to 1.0 separated by spaces. * The key "Open" can have the values "true" or "false" and * signals the open status of the children. It's "true" by default. * <p> * The actions and the parameters can be: * <ul> * <li>"Action" = "GoTo" - "Page" | "Named" * <ul> * <li>"Page" = "3 XYZ 70 400 null" - page number followed by a destination (/XYZ is also accepted) * <li>"Named" = "named_destination" * </ul> * <li>"Action" = "GoToR" - "Page" | "Named" | "NamedN", "File", ["NewWindow"] * <ul> * <li>"Page" = "3 XYZ 70 400 null" - page number followed by a destination (/XYZ is also accepted) * <li>"Named" = "named_destination_as_a_string" * <li>"NamedN" = "named_destination_as_a_name" * <li>"File" - "the_file_to_open" * <li>"NewWindow" - "true" or "false" * </ul> * <li>"Action" = "URI" - "URI" * <ul> * <li>"URI" = "http://sf.net" - URI to jump to * </ul> * <li>"Action" = "Launch" - "File" * <ul> * <li>"File" - "the_file_to_open_or_execute" * </ul> * @author Paulo Soares */ public final class SimpleBookmark implements SimpleXMLDocHandler { private ArrayList<HashMap<String, Object>> topList; private final Stack<HashMap<String, Object>> attr = new Stack<HashMap<String, Object>>(); /** Creates a new instance of SimpleBookmark */ private SimpleBookmark() { } private static List<HashMap<String, Object>> bookmarkDepth(PdfReader reader, PdfDictionary outline, IntHashtable pages, boolean processCurrentOutlineOnly) { ArrayList<HashMap<String, Object>> list = new ArrayList<HashMap<String, Object>>(); while (outline != null) { HashMap<String, Object> map = new HashMap<String, Object>(); PdfString title = (PdfString)PdfReader.getPdfObjectRelease(outline.get(PdfName.TITLE)); map.put("Title", title.toUnicodeString()); PdfArray color = (PdfArray)PdfReader.getPdfObjectRelease(outline.get(PdfName.C)); if (color != null && color.size() == 3) { ByteBuffer out = new ByteBuffer(); out.append(color.getAsNumber(0).floatValue()).append(' '); out.append(color.getAsNumber(1).floatValue()).append(' '); out.append(color.getAsNumber(2).floatValue()); map.put("Color", PdfEncodings.convertToString(out.toByteArray(), null)); } PdfNumber style = (PdfNumber)PdfReader.getPdfObjectRelease(outline.get(PdfName.F)); if (style != null) { int f = style.intValue(); String s = ""; if ((f & 1) != 0) s += "italic "; if ((f & 2) != 0) s += "bold "; s = s.trim(); if (s.length() != 0) map.put("Style", s); } PdfNumber count = (PdfNumber)PdfReader.getPdfObjectRelease(outline.get(PdfName.COUNT)); if (count != null && count.intValue() < 0) map.put("Open", "false"); try { PdfObject dest = PdfReader.getPdfObjectRelease(outline.get(PdfName.DEST)); if (dest != null) { mapGotoBookmark(map, dest, pages); //changed by ujihara 2004-06-13 } else { PdfDictionary action = (PdfDictionary)PdfReader.getPdfObjectRelease(outline.get(PdfName.A)); if (action != null) { if (PdfName.GOTO.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) { dest = PdfReader.getPdfObjectRelease(action.get(PdfName.D)); if (dest != null) { mapGotoBookmark(map, dest, pages); } } else if (PdfName.URI.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) { map.put("Action", "URI"); map.put("URI", ((PdfString)PdfReader.getPdfObjectRelease(action.get(PdfName.URI))).toUnicodeString()); } else if (PdfName.JAVASCRIPT.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) { map.put("Action", "JS"); map.put("Code", PdfReader.getPdfObjectRelease(action.get(PdfName.JS)).toString()); } else if (PdfName.GOTOR.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) { dest = PdfReader.getPdfObjectRelease(action.get(PdfName.D)); if (dest != null) { if (dest.isString()) map.put("Named", dest.toString()); else if (dest.isName()) map.put("NamedN", PdfName.decodeName(dest.toString())); else if (dest.isArray()) { PdfArray arr = (PdfArray)dest; StringBuffer s = new StringBuffer(); s.append(arr.getPdfObject(0).toString()); s.append(' ').append(arr.getPdfObject(1).toString()); for (int k = 2; k < arr.size(); ++k) s.append(' ').append(arr.getPdfObject(k).toString()); map.put("Page", s.toString()); } } map.put("Action", "GoToR"); PdfObject file = PdfReader.getPdfObjectRelease(action.get(PdfName.F)); if (file != null) { if (file.isString()) map.put("File", ((PdfString)file).toUnicodeString()); else if (file.isDictionary()) { file = PdfReader.getPdfObject(((PdfDictionary)file).get(PdfName.F)); if (file.isString()) map.put("File", ((PdfString)file).toUnicodeString()); } } PdfObject newWindow = PdfReader.getPdfObjectRelease(action.get(PdfName.NEWWINDOW)); if (newWindow != null) map.put("NewWindow", newWindow.toString()); } else if (PdfName.LAUNCH.equals(PdfReader.getPdfObjectRelease(action.get(PdfName.S)))) { map.put("Action", "Launch"); PdfObject file = PdfReader.getPdfObjectRelease(action.get(PdfName.F)); if (file == null) file = PdfReader.getPdfObjectRelease(action.get(PdfName.WIN)); if (file != null) { if (file.isString()) map.put("File", ((PdfString)file).toUnicodeString()); else if (file.isDictionary()) { file = PdfReader.getPdfObjectRelease(((PdfDictionary)file).get(PdfName.F)); if (file.isString()) map.put("File", ((PdfString)file).toUnicodeString()); } } } } } } catch (Exception e) { //empty on purpose } PdfDictionary first = (PdfDictionary)PdfReader.getPdfObjectRelease(outline.get(PdfName.FIRST)); if (first != null) { map.put("Kids", bookmarkDepth(reader, first, pages, false)); } list.add(map); if (!processCurrentOutlineOnly) outline = (PdfDictionary)PdfReader.getPdfObjectRelease(outline.get(PdfName.NEXT)); else outline = null; } return list; } private static void mapGotoBookmark(HashMap<String, Object> map, PdfObject dest, IntHashtable pages) { if (dest.isString()) map.put("Named", dest.toString()); else if (dest.isName()) map.put("Named", PdfName.decodeName(dest.toString())); else if (dest.isArray()) map.put("Page", makeBookmarkParam((PdfArray)dest, pages)); //changed by ujihara 2004-06-13 map.put("Action", "GoTo"); } private static String makeBookmarkParam(PdfArray dest, IntHashtable pages) { StringBuffer s = new StringBuffer(); PdfObject obj = dest.getPdfObject(0); if (obj.isNumber()) s.append(((PdfNumber)obj).intValue() + 1); else s.append(pages.get(getNumber((PdfIndirectReference)obj))); //changed by ujihara 2004-06-13 s.append(' ').append(dest.getPdfObject(1).toString().substring(1)); for (int k = 2; k < dest.size(); ++k) s.append(' ').append(dest.getPdfObject(k).toString()); return s.toString(); } /** * Gets number of indirect. If type of directed indirect is PAGES, it refers PAGE object through KIDS. * (Contributed by Kazuya Ujihara) * @param indirect * 2004-06-13 */ private static int getNumber(PdfIndirectReference indirect) { PdfDictionary pdfObj = (PdfDictionary)PdfReader.getPdfObjectRelease(indirect); if (pdfObj.contains(PdfName.TYPE) && pdfObj.get(PdfName.TYPE).equals(PdfName.PAGES) && pdfObj.contains(PdfName.KIDS)) { PdfArray kids = (PdfArray)pdfObj.get(PdfName.KIDS); indirect = (PdfIndirectReference)kids.getPdfObject(0); } return indirect.getNumber(); } /** * Gets a <CODE>List</CODE> with the bookmarks. It returns <CODE>null</CODE> if * the document doesn't have any bookmarks. * @param reader the document * @return a <CODE>List</CODE> with the bookmarks or <CODE>null</CODE> if the * document doesn't have any */ public static List<HashMap<String, Object>> getBookmark(PdfReader reader) { PdfDictionary catalog = reader.getCatalog(); PdfObject obj = PdfReader.getPdfObjectRelease(catalog.get(PdfName.OUTLINES)); if (obj == null || !obj.isDictionary()) return null; PdfDictionary outlines = (PdfDictionary)obj; return SimpleBookmark.getBookmark(reader, outlines, false); } /** * Gets a <CODE>List</CODE> with the bookmarks that are children of <CODE>outline</CODE>. It returns <CODE>null</CODE> if * the document doesn't have any bookmarks. * @param reader the document * @param outline the outline dictionary to get bookmarks from * @param includeRoot indicates if to include <CODE>outline</CODE> parameter itself into returned list of bookmarks * @return a <CODE>List</CODE> with the bookmarks or <CODE>null</CODE> if the * document doesn't have any */ public static List<HashMap<String, Object>> getBookmark(PdfReader reader, PdfDictionary outline, boolean includeRoot) { PdfDictionary catalog = reader.getCatalog(); if (outline == null) return null; IntHashtable pages = new IntHashtable(); int numPages = reader.getNumberOfPages(); for (int k = 1; k <= numPages; ++k) { pages.put(reader.getPageOrigRef(k).getNumber(), k); reader.releasePage(k); } if (includeRoot) return bookmarkDepth(reader, outline, pages, true); else return bookmarkDepth(reader, (PdfDictionary)PdfReader.getPdfObjectRelease(outline.get(PdfName.FIRST)), pages, false); } /** * Removes the bookmark entries for a number of page ranges. The page ranges * consists of a number of pairs with the start/end page range. The page numbers * are inclusive. * @param list the bookmarks * @param pageRange the page ranges, always in pairs. */ @SuppressWarnings("unchecked") public static void eliminatePages(List<HashMap<String, Object>> list, int pageRange[]) { if (list == null) return; for (Iterator<HashMap<String, Object>> it = list.listIterator(); it.hasNext();) { HashMap<String, Object> map = it.next(); boolean hit = false; if ("GoTo".equals(map.get("Action"))) { String page = (String)map.get("Page"); if (page != null) { page = page.trim(); int idx = page.indexOf(' '); int pageNum; if (idx < 0) pageNum = Integer.parseInt(page); else pageNum = Integer.parseInt(page.substring(0, idx)); int len = pageRange.length & 0xfffffffe; for (int k = 0; k < len; k += 2) { if (pageNum >= pageRange[k] && pageNum <= pageRange[k + 1]) { hit = true; break; } } } } List<HashMap<String, Object>> kids = (List<HashMap<String, Object>>)map.get("Kids"); if (kids != null) { eliminatePages(kids, pageRange); if (kids.isEmpty()) { map.remove("Kids"); kids = null; } } if (hit) { if (kids == null) it.remove(); else { map.remove("Action"); map.remove("Page"); map.remove("Named"); } } } } /** * For the pages in range add the <CODE>pageShift</CODE> to the page number. * The page ranges * consists of a number of pairs with the start/end page range. The page numbers * are inclusive. * @param list the bookmarks * @param pageShift the number to add to the pages in range * @param pageRange the page ranges, always in pairs. It can be <CODE>null</CODE> * to include all the pages */ @SuppressWarnings("unchecked") public static void shiftPageNumbers(List<HashMap<String, Object>> list, int pageShift, int pageRange[]) { if (list == null) return; for (Iterator<HashMap<String, Object>> it = list.listIterator(); it.hasNext();) { HashMap<String, Object> map = it.next(); if ("GoTo".equals(map.get("Action"))) { String page = (String)map.get("Page"); if (page != null) { page = page.trim(); int idx = page.indexOf(' '); int pageNum; if (idx < 0) pageNum = Integer.parseInt(page); else pageNum = Integer.parseInt(page.substring(0, idx)); boolean hit = false; if (pageRange == null) hit = true; else { int len = pageRange.length & 0xfffffffe; for (int k = 0; k < len; k += 2) { if (pageNum >= pageRange[k] && pageNum <= pageRange[k + 1]) { hit = true; break; } } } if (hit) { if (idx < 0) page = Integer.toString(pageNum + pageShift); else page = pageNum + pageShift + page.substring(idx); } map.put("Page", page); } } List<HashMap<String, Object>> kids = (List<HashMap<String, Object>>)map.get("Kids"); if (kids != null) shiftPageNumbers(kids, pageShift, pageRange); } } static void createOutlineAction(PdfDictionary outline, HashMap<String, Object> map, PdfWriter writer, boolean namedAsNames) { try { String action = (String)map.get("Action"); if ("GoTo".equals(action)) { String p; if ((p = (String)map.get("Named")) != null) { if (namedAsNames) outline.put(PdfName.DEST, new PdfName(p)); else outline.put(PdfName.DEST, new PdfString(p, null)); } else if ((p = (String)map.get("Page")) != null) { PdfArray ar = new PdfArray(); StringTokenizer tk = new StringTokenizer(p); int n = Integer.parseInt(tk.nextToken()); ar.add(writer.getPageReference(n)); if (!tk.hasMoreTokens()) { ar.add(PdfName.XYZ); ar.add(new float[]{0, 10000, 0}); } else { String fn = tk.nextToken(); if (fn.startsWith("/")) fn = fn.substring(1); ar.add(new PdfName(fn)); for (int k = 0; k < 4 && tk.hasMoreTokens(); ++k) { fn = tk.nextToken(); if (fn.equals("null")) ar.add(PdfNull.PDFNULL); else ar.add(new PdfNumber(fn)); } } outline.put(PdfName.DEST, ar); } } else if ("GoToR".equals(action)) { String p; PdfDictionary dic = new PdfDictionary(); if ((p = (String)map.get("Named")) != null) dic.put(PdfName.D, new PdfString(p, null)); else if ((p = (String)map.get("NamedN")) != null) dic.put(PdfName.D, new PdfName(p)); else if ((p = (String)map.get("Page")) != null){ PdfArray ar = new PdfArray(); StringTokenizer tk = new StringTokenizer(p); ar.add(new PdfNumber(tk.nextToken())); if (!tk.hasMoreTokens()) { ar.add(PdfName.XYZ); ar.add(new float[]{0, 10000, 0}); } else { String fn = tk.nextToken(); if (fn.startsWith("/")) fn = fn.substring(1); ar.add(new PdfName(fn)); for (int k = 0; k < 4 && tk.hasMoreTokens(); ++k) { fn = tk.nextToken(); if (fn.equals("null")) ar.add(PdfNull.PDFNULL); else ar.add(new PdfNumber(fn)); } } dic.put(PdfName.D, ar); } String file = (String)map.get("File"); if (dic.size() > 0 && file != null) { dic.put(PdfName.S, PdfName.GOTOR); dic.put(PdfName.F, new PdfString(file)); String nw = (String)map.get("NewWindow"); if (nw != null) { if (nw.equals("true")) dic.put(PdfName.NEWWINDOW, PdfBoolean.PDFTRUE); else if (nw.equals("false")) dic.put(PdfName.NEWWINDOW, PdfBoolean.PDFFALSE); } outline.put(PdfName.A, dic); } } else if ("URI".equals(action)) { String uri = (String)map.get("URI"); if (uri != null) { PdfDictionary dic = new PdfDictionary(); dic.put(PdfName.S, PdfName.URI); dic.put(PdfName.URI, new PdfString(uri)); outline.put(PdfName.A, dic); } } else if ("JS".equals(action)) { String code = (String)map.get("Code"); if(code != null) { outline.put(PdfName.A, PdfAction.javaScript(code, writer)); } } else if ("Launch".equals(action)) { String file = (String)map.get("File"); if (file != null) { PdfDictionary dic = new PdfDictionary(); dic.put(PdfName.S, PdfName.LAUNCH); dic.put(PdfName.F, new PdfString(file)); outline.put(PdfName.A, dic); } } } catch (Exception e) { // empty on purpose } } @SuppressWarnings("unchecked") public static Object[] iterateOutlines(PdfWriter writer, PdfIndirectReference parent, List<HashMap<String, Object>> kids, boolean namedAsNames) throws IOException { PdfIndirectReference refs[] = new PdfIndirectReference[kids.size()]; for (int k = 0; k < refs.length; ++k) refs[k] = writer.getPdfIndirectReference(); int ptr = 0; int count = 0; for (Iterator<HashMap<String, Object>> it = kids.listIterator(); it.hasNext(); ++ptr) { HashMap<String, Object> map = it.next(); Object lower[] = null; List<HashMap<String, Object>> subKid = (List<HashMap<String, Object>>)map.get("Kids"); if (subKid != null && !subKid.isEmpty()) lower = iterateOutlines(writer, refs[ptr], subKid, namedAsNames); PdfDictionary outline = new PdfDictionary(); ++count; if (lower != null) { outline.put(PdfName.FIRST, (PdfIndirectReference)lower[0]); outline.put(PdfName.LAST, (PdfIndirectReference)lower[1]); int n = ((Integer)lower[2]).intValue(); if ("false".equals(map.get("Open"))) { outline.put(PdfName.COUNT, new PdfNumber(-n)); } else { outline.put(PdfName.COUNT, new PdfNumber(n)); count += n; } } outline.put(PdfName.PARENT, parent); if (ptr > 0) outline.put(PdfName.PREV, refs[ptr - 1]); if (ptr < refs.length - 1) outline.put(PdfName.NEXT, refs[ptr + 1]); outline.put(PdfName.TITLE, new PdfString((String)map.get("Title"), PdfObject.TEXT_UNICODE)); String color = (String)map.get("Color"); if (color != null) { try { PdfArray arr = new PdfArray(); StringTokenizer tk = new StringTokenizer(color); for (int k = 0; k < 3; ++k) { float f = Float.parseFloat(tk.nextToken()); if (f < 0) f = 0; if (f > 1) f = 1; arr.add(new PdfNumber(f)); } outline.put(PdfName.C, arr); } catch(Exception e){} //in case it's malformed } String style = (String)map.get("Style"); if (style != null) { style = style.toLowerCase(); int bits = 0; if (style.indexOf("italic") >= 0) bits |= 1; if (style.indexOf("bold") >= 0) bits |= 2; if (bits != 0) outline.put(PdfName.F, new PdfNumber(bits)); } createOutlineAction(outline, map, writer, namedAsNames); writer.addToBody(outline, refs[ptr]); } return new Object[]{refs[0], refs[refs.length - 1], Integer.valueOf(count)}; } /** * Exports the bookmarks to XML. Only of use if the generation is to be include in * some other XML document. * @param list the bookmarks * @param out the export destination. The writer is not closed * @param indent the indentation level. Pretty printing significant only. Use <CODE>-1</CODE> for no indents. * @param onlyASCII codes above 127 will always be escaped with &#nn; if <CODE>true</CODE>, * whatever the encoding * @throws IOException on error * @since 5.0.1 (generic type in signature) */ @SuppressWarnings("unchecked") public static void exportToXMLNode(List<HashMap<String, Object>> list, Writer out, int indent, boolean onlyASCII) throws IOException { String dep = ""; if (indent != -1) { for (int k = 0; k < indent; ++k) dep += " "; } for (HashMap<String, Object> map : list) { String title = null; out.write(dep); out.write("<Title "); List<HashMap<String, Object>> kids = null; for (Map.Entry<String, Object> entry : map.entrySet()) { String key = entry.getKey(); if (key.equals("Title")) { title = (String) entry.getValue(); continue; } else if (key.equals("Kids")) { kids = (List<HashMap<String, Object>>) entry.getValue(); continue; } else { out.write(key); out.write("=\""); String value = (String) entry.getValue(); if (key.equals("Named") || key.equals("NamedN")) value = SimpleNamedDestination.escapeBinaryString(value); out.write(XMLUtil.escapeXML(value, onlyASCII)); out.write("\" "); } } out.write(">"); if (title == null) title = ""; out.write(XMLUtil.escapeXML(title, onlyASCII)); if (kids != null) { out.write("\n"); exportToXMLNode(kids, out, indent == -1 ? indent : indent + 1, onlyASCII); out.write(dep); } out.write("</Title>\n"); } } /** * Exports the bookmarks to XML. The DTD for this XML is: * <p> * <pre> * <?xml version='1.0' encoding='UTF-8'?> * <!ELEMENT Title (#PCDATA|Title)*> * <!ATTLIST Title * Action CDATA #IMPLIED * Open CDATA #IMPLIED * Page CDATA #IMPLIED * URI CDATA #IMPLIED * File CDATA #IMPLIED * Named CDATA #IMPLIED * NamedN CDATA #IMPLIED * NewWindow CDATA #IMPLIED * Style CDATA #IMPLIED * Color CDATA #IMPLIED * > * <!ELEMENT Bookmark (Title)*> * </pre> * @param list the bookmarks * @param out the export destination. The stream is not closed * @param encoding the encoding according to IANA conventions * @param onlyASCII codes above 127 will always be escaped with &#nn; if <CODE>true</CODE>, * whatever the encoding * @throws IOException on error * @since 5.0.1 (generic type in signature) */ public static void exportToXML(List<HashMap<String, Object>> list, OutputStream out, String encoding, boolean onlyASCII) throws IOException { String jenc = IanaEncodings.getJavaEncoding(encoding); Writer wrt = new BufferedWriter(new OutputStreamWriter(out, jenc)); exportToXML(list, wrt, encoding, onlyASCII); } /** * Exports the bookmarks to XML. * @param list the bookmarks * @param wrt the export destination. The writer is not closed * @param encoding the encoding according to IANA conventions * @param onlyASCII codes above 127 will always be escaped with &#nn; if <CODE>true</CODE>, * whatever the encoding * @throws IOException on error * @since 5.0.1 (generic type in signature) */ public static void exportToXML(List<HashMap<String, Object>> list, Writer wrt, String encoding, boolean onlyASCII) throws IOException { wrt.write("<?xml version=\"1.0\" encoding=\""); wrt.write(XMLUtil.escapeXML(encoding, onlyASCII)); wrt.write("\"?>\n<Bookmark>\n"); exportToXMLNode(list, wrt, 1, onlyASCII); wrt.write("</Bookmark>\n"); wrt.flush(); } /** * Import the bookmarks from XML. * @param in the XML source. The stream is not closed * @throws IOException on error * @return the bookmarks */ public static List<HashMap<String, Object>> importFromXML(InputStream in) throws IOException { SimpleBookmark book = new SimpleBookmark(); SimpleXMLParser.parse(book, in); return book.topList; } /** * Import the bookmarks from XML. * @param in the XML source. The reader is not closed * @throws IOException on error * @return the bookmarks */ public static List<HashMap<String, Object>> importFromXML(Reader in) throws IOException { SimpleBookmark book = new SimpleBookmark(); SimpleXMLParser.parse(book, in); return book.topList; } public void endDocument() { } @SuppressWarnings("unchecked") public void endElement(String tag) { if (tag.equals("Bookmark")) { if (attr.isEmpty()) return; else throw new RuntimeException(MessageLocalization.getComposedMessage("bookmark.end.tag.out.of.place")); } if (!tag.equals("Title")) throw new RuntimeException(MessageLocalization.getComposedMessage("invalid.end.tag.1", tag)); HashMap<String, Object> attributes = attr.pop(); String title = (String)attributes.get("Title"); attributes.put("Title", title.trim()); String named = (String)attributes.get("Named"); if (named != null) attributes.put("Named", SimpleNamedDestination.unEscapeBinaryString(named)); named = (String)attributes.get("NamedN"); if (named != null) attributes.put("NamedN", SimpleNamedDestination.unEscapeBinaryString(named)); if (attr.isEmpty()) topList.add(attributes); else { HashMap<String, Object> parent = attr.peek(); List<HashMap<String, Object>> kids = (List<HashMap<String, Object>>)parent.get("Kids"); if (kids == null) { kids = new ArrayList<HashMap<String, Object>>(); parent.put("Kids", kids); } kids.add(attributes); } } public void startDocument() { } public void startElement(String tag, Map<String, String> h) { if (topList == null) { if (tag.equals("Bookmark")) { topList = new ArrayList<HashMap<String, Object>>(); return; } else throw new RuntimeException(MessageLocalization.getComposedMessage("root.element.is.not.bookmark.1", tag)); } if (!tag.equals("Title")) throw new RuntimeException(MessageLocalization.getComposedMessage("tag.1.not.allowed", tag)); HashMap<String, Object> attributes = new HashMap<String, Object>(h); attributes.put("Title", ""); attributes.remove("Kids"); attr.push(attributes); } public void text(String str) { if (attr.isEmpty()) return; HashMap<String, Object> attributes = attr.peek(); String title = (String)attributes.get("Title"); title += str; attributes.put("Title", title); } }