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}