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 &#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 * <?xml version='1.0' encoding='UTF-8'?> 621 * <!ELEMENT Title (#PCDATA|Title)*> 622 * <!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 * > 634 * <!ELEMENT Bookmark (Title)*> 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 &#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 &#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}