001/* 002 * $Id: XfaForm.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.ByteArrayInputStream; 047import java.io.ByteArrayOutputStream; 048import java.io.File; 049import java.io.FileInputStream; 050import java.io.IOException; 051import java.io.InputStream; 052import java.util.ArrayList; 053import java.util.Collection; 054import java.util.EmptyStackException; 055import java.util.HashMap; 056import java.util.Map; 057 058import javax.xml.parsers.DocumentBuilder; 059import javax.xml.parsers.DocumentBuilderFactory; 060import javax.xml.parsers.ParserConfigurationException; 061 062import org.w3c.dom.Document; 063import org.w3c.dom.Node; 064import org.w3c.dom.NodeList; 065import org.xml.sax.InputSource; 066import org.xml.sax.SAXException; 067 068import com.itextpdf.text.ExceptionConverter; 069import com.itextpdf.text.xml.XmlDomWriter; 070 071/** 072 * Processes XFA forms. 073 * @author Paulo Soares 074 */ 075public class XfaForm { 076 077 private Xml2SomTemplate templateSom; 078 private Node templateNode; 079 private Xml2SomDatasets datasetsSom; 080 private Node datasetsNode; 081 private AcroFieldsSearch acroFieldsSom; 082 private PdfReader reader; 083 private boolean xfaPresent; 084 private org.w3c.dom.Document domDocument; 085 private boolean changed; 086 public static final String XFA_DATA_SCHEMA = "http://www.xfa.org/schema/xfa-data/1.0/"; 087 088 /** 089 * An empty constructor to build on. 090 */ 091 public XfaForm() { 092 } 093 094 /** 095 * Return the XFA Object, could be an array, could be a Stream. 096 * Returns null f no XFA Object is present. 097 * @param reader a PdfReader instance 098 * @return the XFA object 099 * @since 2.1.3 100 */ 101 public static PdfObject getXfaObject(PdfReader reader) { 102 PdfDictionary af = (PdfDictionary)PdfReader.getPdfObjectRelease(reader.getCatalog().get(PdfName.ACROFORM)); 103 if (af == null) { 104 return null; 105 } 106 return PdfReader.getPdfObjectRelease(af.get(PdfName.XFA)); 107 } 108 109 /** 110 * A constructor from a <CODE>PdfReader</CODE>. It basically does everything 111 * from finding the XFA stream to the XML parsing. 112 * @param reader the reader 113 * @throws java.io.IOException on error 114 * @throws javax.xml.parsers.ParserConfigurationException on error 115 * @throws org.xml.sax.SAXException on error 116 */ 117 public XfaForm(PdfReader reader) throws IOException, ParserConfigurationException, SAXException { 118 this.reader = reader; 119 PdfObject xfa = getXfaObject(reader); 120 if (xfa == null) { 121 xfaPresent = false; 122 return; 123 } 124 xfaPresent = true; 125 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 126 if (xfa.isArray()) { 127 PdfArray ar = (PdfArray)xfa; 128 for (int k = 1; k < ar.size(); k += 2) { 129 PdfObject ob = ar.getDirectObject(k); 130 if (ob instanceof PRStream) { 131 byte[] b = PdfReader.getStreamBytes((PRStream)ob); 132 bout.write(b); 133 } 134 } 135 } 136 else if (xfa instanceof PRStream) { 137 byte[] b = PdfReader.getStreamBytes((PRStream)xfa); 138 bout.write(b); 139 } 140 bout.close(); 141 DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); 142 fact.setNamespaceAware(true); 143 DocumentBuilder db = fact.newDocumentBuilder(); 144 domDocument = db.parse(new ByteArrayInputStream(bout.toByteArray())); 145 extractNodes(); 146 } 147 148 /** 149 * Extracts the nodes from the domDocument. 150 * @since 2.1.5 151 */ 152 private void extractNodes() { 153 Node n = domDocument.getFirstChild(); 154 while (n.getChildNodes().getLength() == 0) { 155 n = n.getNextSibling(); 156 } 157 n = n.getFirstChild(); 158 while (n != null) { 159 if (n.getNodeType() == Node.ELEMENT_NODE) { 160 String s = n.getLocalName(); 161 if ("template".equals(s)) { 162 templateNode = n; 163 templateSom = new Xml2SomTemplate(n); 164 } 165 else if ("datasets".equals(s)) { 166 datasetsNode = n; 167 datasetsSom = new Xml2SomDatasets(n.getFirstChild()); 168 } 169 } 170 n = n.getNextSibling(); 171 } 172 } 173 174 /** 175 * Sets the XFA key from a byte array. The old XFA is erased. 176 * @param form the data 177 * @param reader the reader 178 * @param writer the writer 179 * @throws java.io.IOException on error 180 */ 181 public static void setXfa(XfaForm form, PdfReader reader, PdfWriter writer) throws IOException { 182 PdfDictionary af = (PdfDictionary)PdfReader.getPdfObjectRelease(reader.getCatalog().get(PdfName.ACROFORM)); 183 if (af == null) { 184 return; 185 } 186 PdfObject xfa = getXfaObject(reader); 187 if (xfa.isArray()) { 188 PdfArray ar = (PdfArray)xfa; 189 int t = -1; 190 int d = -1; 191 for (int k = 0; k < ar.size(); k += 2) { 192 PdfString s = ar.getAsString(k); 193 if ("template".equals(s.toString())) { 194 t = k + 1; 195 } 196 if ("datasets".equals(s.toString())) { 197 d = k + 1; 198 } 199 } 200 if (t > -1 && d > -1) { 201 reader.killXref(ar.getAsIndirectObject(t)); 202 reader.killXref(ar.getAsIndirectObject(d)); 203 PdfStream tStream = new PdfStream(serializeDoc(form.templateNode)); 204 tStream.flateCompress(writer.getCompressionLevel()); 205 ar.set(t, writer.addToBody(tStream).getIndirectReference()); 206 PdfStream dStream = new PdfStream(serializeDoc(form.datasetsNode)); 207 dStream.flateCompress(writer.getCompressionLevel()); 208 ar.set(d, writer.addToBody(dStream).getIndirectReference()); 209 af.put(PdfName.XFA, new PdfArray(ar)); 210 return; 211 } 212 } 213 reader.killXref(af.get(PdfName.XFA)); 214 PdfStream str = new PdfStream(serializeDoc(form.domDocument)); 215 str.flateCompress(writer.getCompressionLevel()); 216 PdfIndirectReference ref = writer.addToBody(str).getIndirectReference(); 217 af.put(PdfName.XFA, ref); 218 } 219 220 /** 221 * Sets the XFA key from the instance data. The old XFA is erased. 222 * @param writer the writer 223 * @throws java.io.IOException on error 224 */ 225 public void setXfa(PdfWriter writer) throws IOException { 226 setXfa(this, reader, writer); 227 } 228 229 /** 230 * Serializes a XML document to a byte array. 231 * @param n the XML document 232 * @throws java.io.IOException on error 233 * @return the serialized XML document 234 */ 235 public static byte[] serializeDoc(Node n) throws IOException { 236 XmlDomWriter xw = new XmlDomWriter(); 237 ByteArrayOutputStream fout = new ByteArrayOutputStream(); 238 xw.setOutput(fout, null); 239 xw.setCanonical(false); 240 xw.write(n); 241 fout.close(); 242 return fout.toByteArray(); 243 } 244 245 /** 246 * Returns <CODE>true</CODE> if it is a XFA form. 247 * @return <CODE>true</CODE> if it is a XFA form 248 */ 249 public boolean isXfaPresent() { 250 return xfaPresent; 251 } 252 253 /** 254 * Gets the top level DOM document. 255 * @return the top level DOM document 256 */ 257 public org.w3c.dom.Document getDomDocument() { 258 return domDocument; 259 } 260 261 262 /** 263 * Finds the complete field name contained in the "classic" forms from a partial 264 * name. 265 * @param name the complete or partial name 266 * @param af the fields 267 * @return the complete name or <CODE>null</CODE> if not found 268 */ 269 public String findFieldName(String name, AcroFields af) { 270 Map<String, AcroFields.Item> items = af.getFields(); 271 if (items.containsKey(name)) 272 return name; 273 if (acroFieldsSom == null) { 274 if (items.isEmpty() && xfaPresent) 275 acroFieldsSom = new AcroFieldsSearch(datasetsSom.getName2Node().keySet()); 276 else 277 acroFieldsSom = new AcroFieldsSearch(items.keySet()); 278 } 279 if (acroFieldsSom.getAcroShort2LongName().containsKey(name)) 280 return acroFieldsSom.getAcroShort2LongName().get(name); 281 return acroFieldsSom.inverseSearchGlobal(Xml2Som.splitParts(name)); 282 } 283 284 /** 285 * Finds the complete SOM name contained in the datasets section from a 286 * possibly partial name. 287 * @param name the complete or partial name 288 * @return the complete name or <CODE>null</CODE> if not found 289 */ 290 public String findDatasetsName(String name) { 291 if (datasetsSom.getName2Node().containsKey(name)) 292 return name; 293 return datasetsSom.inverseSearchGlobal(Xml2Som.splitParts(name)); 294 } 295 296 /** 297 * Finds the <CODE>Node</CODE> contained in the datasets section from a 298 * possibly partial name. 299 * @param name the complete or partial name 300 * @return the <CODE>Node</CODE> or <CODE>null</CODE> if not found 301 */ 302 public Node findDatasetsNode(String name) { 303 if (name == null) 304 return null; 305 name = findDatasetsName(name); 306 if (name == null) 307 return null; 308 return datasetsSom.getName2Node().get(name); 309 } 310 311 /** 312 * Gets all the text contained in the child nodes of this node. 313 * @param n the <CODE>Node</CODE> 314 * @return the text found or "" if no text was found 315 */ 316 public static String getNodeText(Node n) { 317 if (n == null) 318 return ""; 319 return getNodeText(n, ""); 320 321 } 322 323 private static String getNodeText(Node n, String name) { 324 Node n2 = n.getFirstChild(); 325 while (n2 != null) { 326 if (n2.getNodeType() == Node.ELEMENT_NODE) { 327 name = getNodeText(n2, name); 328 } 329 else if (n2.getNodeType() == Node.TEXT_NODE) { 330 name += n2.getNodeValue(); 331 } 332 n2 = n2.getNextSibling(); 333 } 334 return name; 335 } 336 337 /** 338 * Sets the text of this node. All the child's node are deleted and a new 339 * child text node is created. 340 * @param n the <CODE>Node</CODE> to add the text to 341 * @param text the text to add 342 */ 343 public void setNodeText(Node n, String text) { 344 if (n == null) 345 return; 346 Node nc = null; 347 while ((nc = n.getFirstChild()) != null) { 348 n.removeChild(nc); 349 } 350 if (n.getAttributes().getNamedItemNS(XFA_DATA_SCHEMA, "dataNode") != null) 351 n.getAttributes().removeNamedItemNS(XFA_DATA_SCHEMA, "dataNode"); 352 n.appendChild(domDocument.createTextNode(text)); 353 changed = true; 354 } 355 356 /** 357 * Sets the XFA form flag signaling that this is a valid XFA form. 358 * @param xfaPresent the XFA form flag signaling that this is a valid XFA form 359 */ 360 public void setXfaPresent(boolean xfaPresent) { 361 this.xfaPresent = xfaPresent; 362 } 363 364 /** 365 * Sets the top DOM document. 366 * @param domDocument the top DOM document 367 */ 368 public void setDomDocument(org.w3c.dom.Document domDocument) { 369 this.domDocument = domDocument; 370 extractNodes(); 371 } 372 373 /** 374 * Gets the <CODE>PdfReader</CODE> used by this instance. 375 * @return the <CODE>PdfReader</CODE> used by this instance 376 */ 377 public PdfReader getReader() { 378 return reader; 379 } 380 381 /** 382 * Sets the <CODE>PdfReader</CODE> to be used by this instance. 383 * @param reader the <CODE>PdfReader</CODE> to be used by this instance 384 */ 385 public void setReader(PdfReader reader) { 386 this.reader = reader; 387 } 388 389 /** 390 * Checks if this XFA form was changed. 391 * @return <CODE>true</CODE> if this XFA form was changed 392 */ 393 public boolean isChanged() { 394 return changed; 395 } 396 397 /** 398 * Sets the changed status of this XFA instance. 399 * @param changed the changed status of this XFA instance 400 */ 401 public void setChanged(boolean changed) { 402 this.changed = changed; 403 } 404 405 /** 406 * A structure to store each part of a SOM name and link it to the next part 407 * beginning from the lower hierarchy. 408 */ 409 public static class InverseStore { 410 protected ArrayList<String> part = new ArrayList<String>(); 411 protected ArrayList<Object> follow = new ArrayList<Object>(); 412 413 /** 414 * Gets the full name by traversing the hierarchy using only the 415 * index 0. 416 * @return the full name 417 */ 418 public String getDefaultName() { 419 InverseStore store = this; 420 while (true) { 421 Object obj = store.follow.get(0); 422 if (obj instanceof String) 423 return (String)obj; 424 store = (InverseStore)obj; 425 } 426 } 427 428 /** 429 * Search the current node for a similar name. A similar name starts 430 * with the same name but has a different index. For example, "detail[3]" 431 * is similar to "detail[9]". The main use is to discard names that 432 * correspond to out of bounds records. 433 * @param name the name to search 434 * @return <CODE>true</CODE> if a similitude was found 435 */ 436 public boolean isSimilar(String name) { 437 int idx = name.indexOf('['); 438 name = name.substring(0, idx + 1); 439 for (int k = 0; k < part.size(); ++k) { 440 if (part.get(k).startsWith(name)) 441 return true; 442 } 443 return false; 444 } 445 } 446 447 /** 448 * Another stack implementation. The main use is to facilitate 449 * the porting to other languages. 450 */ 451 public static class Stack2<T> extends ArrayList<T> { 452 private static final long serialVersionUID = -7451476576174095212L; 453 454 /** 455 * Looks at the object at the top of this stack without removing it from the stack. 456 * @return the object at the top of this stack 457 */ 458 public T peek() { 459 if (size() == 0) 460 throw new EmptyStackException(); 461 return get(size() - 1); 462 } 463 464 /** 465 * Removes the object at the top of this stack and returns that object as the value of this function. 466 * @return the object at the top of this stack 467 */ 468 public T pop() { 469 if (size() == 0) 470 throw new EmptyStackException(); 471 T ret = get(size() - 1); 472 remove(size() - 1); 473 return ret; 474 } 475 476 /** 477 * Pushes an item onto the top of this stack. 478 * @param item the item to be pushed onto this stack 479 * @return the <CODE>item</CODE> argument 480 */ 481 public T push(T item) { 482 add(item); 483 return item; 484 } 485 486 /** 487 * Tests if this stack is empty. 488 * @return <CODE>true</CODE> if and only if this stack contains no items; <CODE>false</CODE> otherwise 489 */ 490 public boolean empty() { 491 return size() == 0; 492 } 493 } 494 495 /** 496 * A class for some basic SOM processing. 497 */ 498 public static class Xml2Som { 499 /** 500 * The order the names appear in the XML, depth first. 501 */ 502 protected ArrayList<String> order; 503 /** 504 * The mapping of full names to nodes. 505 */ 506 protected HashMap<String, Node> name2Node; 507 /** 508 * The data to do a search from the bottom hierarchy. 509 */ 510 protected HashMap<String, InverseStore> inverseSearch; 511 /** 512 * A stack to be used when parsing. 513 */ 514 protected Stack2<String> stack; 515 /** 516 * A temporary store for the repetition count. 517 */ 518 protected int anform; 519 520 /** 521 * Escapes a SOM string fragment replacing "." with "\.". 522 * @param s the unescaped string 523 * @return the escaped string 524 */ 525 public static String escapeSom(String s) { 526 if (s == null) 527 return ""; 528 int idx = s.indexOf('.'); 529 if (idx < 0) 530 return s; 531 StringBuffer sb = new StringBuffer(); 532 int last = 0; 533 while (idx >= 0) { 534 sb.append(s.substring(last, idx)); 535 sb.append('\\'); 536 last = idx; 537 idx = s.indexOf('.', idx + 1); 538 } 539 sb.append(s.substring(last)); 540 return sb.toString(); 541 } 542 543 /** 544 * Unescapes a SOM string fragment replacing "\." with ".". 545 * @param s the escaped string 546 * @return the unescaped string 547 */ 548 public static String unescapeSom(String s) { 549 int idx = s.indexOf('\\'); 550 if (idx < 0) 551 return s; 552 StringBuffer sb = new StringBuffer(); 553 int last = 0; 554 while (idx >= 0) { 555 sb.append(s.substring(last, idx)); 556 last = idx + 1; 557 idx = s.indexOf('\\', idx + 1); 558 } 559 sb.append(s.substring(last)); 560 return sb.toString(); 561 } 562 563 /** 564 * Outputs the stack as the sequence of elements separated 565 * by '.'. 566 * @return the stack as the sequence of elements separated by '.' 567 */ 568 protected String printStack() { 569 if (stack.empty()) 570 return ""; 571 StringBuffer s = new StringBuffer(); 572 for (int k = 0; k < stack.size(); ++k) 573 s.append('.').append(stack.get(k)); 574 return s.substring(1); 575 } 576 577 /** 578 * Gets the name with the <CODE>#subform</CODE> removed. 579 * @param s the long name 580 * @return the short name 581 */ 582 public static String getShortName(String s) { 583 int idx = s.indexOf(".#subform["); 584 if (idx < 0) 585 return s; 586 int last = 0; 587 StringBuffer sb = new StringBuffer(); 588 while (idx >= 0) { 589 sb.append(s.substring(last, idx)); 590 idx = s.indexOf("]", idx + 10); 591 if (idx < 0) 592 return sb.toString(); 593 last = idx + 1; 594 idx = s.indexOf(".#subform[", last); 595 } 596 sb.append(s.substring(last)); 597 return sb.toString(); 598 } 599 600 /** 601 * Adds a SOM name to the search node chain. 602 * @param unstack the SOM name 603 */ 604 public void inverseSearchAdd(String unstack) { 605 inverseSearchAdd(inverseSearch, stack, unstack); 606 } 607 608 /** 609 * Adds a SOM name to the search node chain. 610 * @param inverseSearch the start point 611 * @param stack the stack with the separated SOM parts 612 * @param unstack the full name 613 */ 614 public static void inverseSearchAdd(HashMap<String, InverseStore> inverseSearch, Stack2<String> stack, String unstack) { 615 String last = stack.peek(); 616 InverseStore store = inverseSearch.get(last); 617 if (store == null) { 618 store = new InverseStore(); 619 inverseSearch.put(last, store); 620 } 621 for (int k = stack.size() - 2; k >= 0; --k) { 622 last = stack.get(k); 623 InverseStore store2; 624 int idx = store.part.indexOf(last); 625 if (idx < 0) { 626 store.part.add(last); 627 store2 = new InverseStore(); 628 store.follow.add(store2); 629 } 630 else 631 store2 = (InverseStore)store.follow.get(idx); 632 store = store2; 633 } 634 store.part.add(""); 635 store.follow.add(unstack); 636 } 637 638 /** 639 * Searches the SOM hierarchy from the bottom. 640 * @param parts the SOM parts 641 * @return the full name or <CODE>null</CODE> if not found 642 */ 643 public String inverseSearchGlobal(ArrayList<String> parts) { 644 if (parts.isEmpty()) 645 return null; 646 InverseStore store = inverseSearch.get(parts.get(parts.size() - 1)); 647 if (store == null) 648 return null; 649 for (int k = parts.size() - 2; k >= 0; --k) { 650 String part = parts.get(k); 651 int idx = store.part.indexOf(part); 652 if (idx < 0) { 653 if (store.isSimilar(part)) 654 return null; 655 return store.getDefaultName(); 656 } 657 store = (InverseStore)store.follow.get(idx); 658 } 659 return store.getDefaultName(); 660 } 661 662 /** 663 * Splits a SOM name in the individual parts. 664 * @param name the full SOM name 665 * @return the split name 666 */ 667 public static Stack2<String> splitParts(String name) { 668 while (name.startsWith(".")) 669 name = name.substring(1); 670 Stack2<String> parts = new Stack2<String>(); 671 int last = 0; 672 int pos = 0; 673 String part; 674 while (true) { 675 pos = last; 676 while (true) { 677 pos = name.indexOf('.', pos); 678 if (pos < 0) 679 break; 680 if (name.charAt(pos - 1) == '\\') 681 ++pos; 682 else 683 break; 684 } 685 if (pos < 0) 686 break; 687 part = name.substring(last, pos); 688 if (!part.endsWith("]")) 689 part += "[0]"; 690 parts.add(part); 691 last = pos + 1; 692 } 693 part = name.substring(last); 694 if (!part.endsWith("]")) 695 part += "[0]"; 696 parts.add(part); 697 return parts; 698 } 699 700 /** 701 * Gets the order the names appear in the XML, depth first. 702 * @return the order the names appear in the XML, depth first 703 */ 704 public ArrayList<String> getOrder() { 705 return order; 706 } 707 708 /** 709 * Sets the order the names appear in the XML, depth first 710 * @param order the order the names appear in the XML, depth first 711 */ 712 public void setOrder(ArrayList<String> order) { 713 this.order = order; 714 } 715 716 /** 717 * Gets the mapping of full names to nodes. 718 * @return the mapping of full names to nodes 719 */ 720 public HashMap<String, Node> getName2Node() { 721 return name2Node; 722 } 723 724 /** 725 * Sets the mapping of full names to nodes. 726 * @param name2Node the mapping of full names to nodes 727 */ 728 public void setName2Node(HashMap<String, Node> name2Node) { 729 this.name2Node = name2Node; 730 } 731 732 /** 733 * Gets the data to do a search from the bottom hierarchy. 734 * @return the data to do a search from the bottom hierarchy 735 */ 736 public HashMap<String, InverseStore> getInverseSearch() { 737 return inverseSearch; 738 } 739 740 /** 741 * Sets the data to do a search from the bottom hierarchy. 742 * @param inverseSearch the data to do a search from the bottom hierarchy 743 */ 744 public void setInverseSearch(HashMap<String, InverseStore> inverseSearch) { 745 this.inverseSearch = inverseSearch; 746 } 747 } 748 749 /** 750 * Processes the datasets section in the XFA form. 751 */ 752 public static class Xml2SomDatasets extends Xml2Som { 753 /** 754 * Creates a new instance from the datasets node. This expects 755 * not the datasets but the data node that comes below. 756 * @param n the datasets node 757 */ 758 public Xml2SomDatasets(Node n) { 759 order = new ArrayList<String>(); 760 name2Node = new HashMap<String, Node>(); 761 stack = new Stack2<String>(); 762 anform = 0; 763 inverseSearch = new HashMap<String, InverseStore>(); 764 processDatasetsInternal(n); 765 } 766 767 /** 768 * Inserts a new <CODE>Node</CODE> that will match the short name. 769 * @param n the datasets top <CODE>Node</CODE> 770 * @param shortName the short name 771 * @return the new <CODE>Node</CODE> of the inserted name 772 */ 773 public Node insertNode(Node n, String shortName) { 774 Stack2<String> stack = splitParts(shortName); 775 org.w3c.dom.Document doc = n.getOwnerDocument(); 776 Node n2 = null; 777 n = n.getFirstChild(); 778 while (n.getNodeType() != Node.ELEMENT_NODE) 779 n = n.getNextSibling(); 780 for (int k = 0; k < stack.size(); ++k) { 781 String part = stack.get(k); 782 int idx = part.lastIndexOf('['); 783 String name = part.substring(0, idx); 784 idx = Integer.parseInt(part.substring(idx + 1, part.length() - 1)); 785 int found = -1; 786 for (n2 = n.getFirstChild(); n2 != null; n2 = n2.getNextSibling()) { 787 if (n2.getNodeType() == Node.ELEMENT_NODE) { 788 String s = escapeSom(n2.getLocalName()); 789 if (s.equals(name)) { 790 ++found; 791 if (found == idx) 792 break; 793 } 794 } 795 } 796 for (; found < idx; ++found) { 797 n2 = doc.createElementNS(null, name); 798 n2 = n.appendChild(n2); 799 Node attr = doc.createAttributeNS(XFA_DATA_SCHEMA, "dataNode"); 800 attr.setNodeValue("dataGroup"); 801 n2.getAttributes().setNamedItemNS(attr); 802 } 803 n = n2; 804 } 805 inverseSearchAdd(inverseSearch, stack, shortName); 806 name2Node.put(shortName, n2); 807 order.add(shortName); 808 return n2; 809 } 810 811 private static boolean hasChildren(Node n) { 812 Node dataNodeN = n.getAttributes().getNamedItemNS(XFA_DATA_SCHEMA, "dataNode"); 813 if (dataNodeN != null) { 814 String dataNode = dataNodeN.getNodeValue(); 815 if ("dataGroup".equals(dataNode)) 816 return true; 817 else if ("dataValue".equals(dataNode)) 818 return false; 819 } 820 if (!n.hasChildNodes()) 821 return false; 822 Node n2 = n.getFirstChild(); 823 while (n2 != null) { 824 if (n2.getNodeType() == Node.ELEMENT_NODE) { 825 return true; 826 } 827 n2 = n2.getNextSibling(); 828 } 829 return false; 830 } 831 832 private void processDatasetsInternal(Node n) { 833 HashMap<String, Integer> ss = new HashMap<String, Integer>(); 834 Node n2 = n.getFirstChild(); 835 while (n2 != null) { 836 if (n2.getNodeType() == Node.ELEMENT_NODE) { 837 String s = escapeSom(n2.getLocalName()); 838 Integer i = ss.get(s); 839 if (i == null) 840 i = Integer.valueOf(0); 841 else 842 i = Integer.valueOf(i.intValue() + 1); 843 ss.put(s, i); 844 if (hasChildren(n2)) { 845 stack.push(s + "[" + i.toString() + "]"); 846 processDatasetsInternal(n2); 847 stack.pop(); 848 } 849 else { 850 stack.push(s + "[" + i.toString() + "]"); 851 String unstack = printStack(); 852 order.add(unstack); 853 inverseSearchAdd(unstack); 854 name2Node.put(unstack, n2); 855 stack.pop(); 856 } 857 } 858 n2 = n2.getNextSibling(); 859 } 860 } 861 } 862 863 /** 864 * A class to process "classic" fields. 865 */ 866 public static class AcroFieldsSearch extends Xml2Som { 867 private HashMap<String, String> acroShort2LongName; 868 869 /** 870 * Creates a new instance from a Collection with the full names. 871 * @param items the Collection 872 */ 873 public AcroFieldsSearch(Collection<String> items) { 874 inverseSearch = new HashMap<String, InverseStore>(); 875 acroShort2LongName = new HashMap<String, String>(); 876 for (String string : items) { 877 String itemName = string; 878 String itemShort = getShortName(itemName); 879 acroShort2LongName.put(itemShort, itemName); 880 inverseSearchAdd(inverseSearch, splitParts(itemShort), itemName); 881 } 882 } 883 884 /** 885 * Gets the mapping from short names to long names. A long 886 * name may contain the #subform name part. 887 * @return the mapping from short names to long names 888 */ 889 public HashMap<String, String> getAcroShort2LongName() { 890 return acroShort2LongName; 891 } 892 893 /** 894 * Sets the mapping from short names to long names. A long 895 * name may contain the #subform name part. 896 * @param acroShort2LongName the mapping from short names to long names 897 */ 898 public void setAcroShort2LongName(HashMap<String, String> acroShort2LongName) { 899 this.acroShort2LongName = acroShort2LongName; 900 } 901 } 902 903 /** 904 * Processes the template section in the XFA form. 905 */ 906 public static class Xml2SomTemplate extends Xml2Som { 907 private boolean dynamicForm; 908 private int templateLevel; 909 910 /** 911 * Creates a new instance from the datasets node. 912 * @param n the template node 913 */ 914 public Xml2SomTemplate(Node n) { 915 order = new ArrayList<String>(); 916 name2Node = new HashMap<String, Node>(); 917 stack = new Stack2<String>(); 918 anform = 0; 919 templateLevel = 0; 920 inverseSearch = new HashMap<String, InverseStore>(); 921 processTemplate(n, null); 922 } 923 924 /** 925 * Gets the field type as described in the <CODE>template</CODE> section of the XFA. 926 * @param s the exact template name 927 * @return the field type or <CODE>null</CODE> if not found 928 */ 929 public String getFieldType(String s) { 930 Node n = name2Node.get(s); 931 if (n == null) 932 return null; 933 if ("exclGroup".equals(n.getLocalName())) 934 return "exclGroup"; 935 Node ui = n.getFirstChild(); 936 while (ui != null) { 937 if (ui.getNodeType() == Node.ELEMENT_NODE && "ui".equals(ui.getLocalName())) { 938 break; 939 } 940 ui = ui.getNextSibling(); 941 } 942 if (ui == null) 943 return null; 944 Node type = ui.getFirstChild(); 945 while (type != null) { 946 if (type.getNodeType() == Node.ELEMENT_NODE && !("extras".equals(type.getLocalName()) && "picture".equals(type.getLocalName()))) { 947 return type.getLocalName(); 948 } 949 type = type.getNextSibling(); 950 } 951 return null; 952 } 953 954 private void processTemplate(Node n, HashMap<String, Integer> ff) { 955 if (ff == null) 956 ff = new HashMap<String, Integer>(); 957 HashMap<String, Integer> ss = new HashMap<String, Integer>(); 958 Node n2 = n.getFirstChild(); 959 while (n2 != null) { 960 if (n2.getNodeType() == Node.ELEMENT_NODE) { 961 String s = n2.getLocalName(); 962 if ("subform".equals(s)) { 963 Node name = n2.getAttributes().getNamedItem("name"); 964 String nn = "#subform"; 965 boolean annon = true; 966 if (name != null) { 967 nn = escapeSom(name.getNodeValue()); 968 annon = false; 969 } 970 Integer i; 971 if (annon) { 972 i = Integer.valueOf(anform); 973 ++anform; 974 } 975 else { 976 i = ss.get(nn); 977 if (i == null) 978 i = Integer.valueOf(0); 979 else 980 i = Integer.valueOf(i.intValue() + 1); 981 ss.put(nn, i); 982 } 983 stack.push(nn + "[" + i.toString() + "]"); 984 ++templateLevel; 985 if (annon) 986 processTemplate(n2, ff); 987 else 988 processTemplate(n2, null); 989 --templateLevel; 990 stack.pop(); 991 } 992 else if ("field".equals(s) || "exclGroup".equals(s)) { 993 Node name = n2.getAttributes().getNamedItem("name"); 994 if (name != null) { 995 String nn = escapeSom(name.getNodeValue()); 996 Integer i = ff.get(nn); 997 if (i == null) 998 i = Integer.valueOf(0); 999 else 1000 i = Integer.valueOf(i.intValue() + 1); 1001 ff.put(nn, i); 1002 stack.push(nn + "[" + i.toString() + "]"); 1003 String unstack = printStack(); 1004 order.add(unstack); 1005 inverseSearchAdd(unstack); 1006 name2Node.put(unstack, n2); 1007 stack.pop(); 1008 } 1009 } 1010 else if (!dynamicForm && templateLevel > 0 && "occur".equals(s)) { 1011 int initial = 1; 1012 int min = 1; 1013 int max = 1; 1014 Node a = n2.getAttributes().getNamedItem("initial"); 1015 if (a != null) 1016 try{initial = Integer.parseInt(a.getNodeValue().trim());}catch(Exception e){} 1017 a = n2.getAttributes().getNamedItem("min"); 1018 if (a != null) 1019 try{min = Integer.parseInt(a.getNodeValue().trim());}catch(Exception e){} 1020 a = n2.getAttributes().getNamedItem("max"); 1021 if (a != null) 1022 try{max = Integer.parseInt(a.getNodeValue().trim());}catch(Exception e){} 1023 if (initial != min || min != max) 1024 dynamicForm = true; 1025 } 1026 } 1027 n2 = n2.getNextSibling(); 1028 } 1029 } 1030 1031 /** 1032 * <CODE>true</CODE> if it's a dynamic form; <CODE>false</CODE> 1033 * if it's a static form. 1034 * @return <CODE>true</CODE> if it's a dynamic form; <CODE>false</CODE> 1035 * if it's a static form 1036 */ 1037 public boolean isDynamicForm() { 1038 return dynamicForm; 1039 } 1040 1041 /** 1042 * Sets the dynamic form flag. It doesn't change the template. 1043 * @param dynamicForm the dynamic form flag 1044 */ 1045 public void setDynamicForm(boolean dynamicForm) { 1046 this.dynamicForm = dynamicForm; 1047 } 1048 } 1049 1050 /** 1051 * Gets the class that contains the template processing section of the XFA. 1052 * @return the class that contains the template processing section of the XFA 1053 */ 1054 public Xml2SomTemplate getTemplateSom() { 1055 return templateSom; 1056 } 1057 1058 /** 1059 * Sets the class that contains the template processing section of the XFA 1060 * @param templateSom the class that contains the template processing section of the XFA 1061 */ 1062 public void setTemplateSom(Xml2SomTemplate templateSom) { 1063 this.templateSom = templateSom; 1064 } 1065 1066 /** 1067 * Gets the class that contains the datasets processing section of the XFA. 1068 * @return the class that contains the datasets processing section of the XFA 1069 */ 1070 public Xml2SomDatasets getDatasetsSom() { 1071 return datasetsSom; 1072 } 1073 1074 /** 1075 * Sets the class that contains the datasets processing section of the XFA. 1076 * @param datasetsSom the class that contains the datasets processing section of the XFA 1077 */ 1078 public void setDatasetsSom(Xml2SomDatasets datasetsSom) { 1079 this.datasetsSom = datasetsSom; 1080 } 1081 1082 /** 1083 * Gets the class that contains the "classic" fields processing. 1084 * @return the class that contains the "classic" fields processing 1085 */ 1086 public AcroFieldsSearch getAcroFieldsSom() { 1087 return acroFieldsSom; 1088 } 1089 1090 /** 1091 * Sets the class that contains the "classic" fields processing. 1092 * @param acroFieldsSom the class that contains the "classic" fields processing 1093 */ 1094 public void setAcroFieldsSom(AcroFieldsSearch acroFieldsSom) { 1095 this.acroFieldsSom = acroFieldsSom; 1096 } 1097 1098 /** 1099 * Gets the <CODE>Node</CODE> that corresponds to the datasets part. 1100 * @return the <CODE>Node</CODE> that corresponds to the datasets part 1101 */ 1102 public Node getDatasetsNode() { 1103 return datasetsNode; 1104 } 1105 1106 public void fillXfaForm(File file) throws IOException { 1107 fillXfaForm(new FileInputStream(file)); 1108 } 1109 1110 public void fillXfaForm(InputStream is) throws IOException { 1111 fillXfaForm(new InputSource(is)); 1112 } 1113 1114 1115 public void fillXfaForm(InputSource is) throws IOException { 1116 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1117 DocumentBuilder db; 1118 try { 1119 db = dbf.newDocumentBuilder(); 1120 Document newdoc = db.parse(is); 1121 fillXfaForm(newdoc.getDocumentElement()); 1122 } catch (ParserConfigurationException e) { 1123 throw new ExceptionConverter(e); 1124 } catch (SAXException e) { 1125 throw new ExceptionConverter(e); 1126 } 1127 } 1128 1129 /** 1130 * Replaces the data under datasets/data. 1131 * @since iText 5.0.0 1132 */ 1133 public void fillXfaForm(Node node) { 1134 NodeList allChilds = datasetsNode.getChildNodes(); 1135 int len = allChilds.getLength(); 1136 Node data = null; 1137 for (int k = 0; k < len; ++k) { 1138 Node n = allChilds.item(k); 1139 if (n.getNodeType() == Node.ELEMENT_NODE && n.getLocalName().equals("data") && XFA_DATA_SCHEMA.equals(n.getNamespaceURI())) { 1140 data = n; 1141 break; 1142 } 1143 } 1144 if (data == null) { 1145 data = datasetsNode.getOwnerDocument().createElementNS(XFA_DATA_SCHEMA, "xfa:data"); 1146 datasetsNode.appendChild(data); 1147 } 1148 NodeList list = data.getChildNodes(); 1149 if (list.getLength() == 0) { 1150 data.appendChild(domDocument.importNode(node, true)); 1151 } 1152 else { 1153 data.replaceChild(domDocument.importNode(node, true), data.getFirstChild()); 1154 } 1155 extractNodes(); 1156 setChanged(true); 1157 } 1158}