# FDFDictionary.java

    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package org.apache.pdfbox.pdmodel.fdf;

    import java.io.IOException;
    import java.io.Writer;
    import java.util.ArrayList;
    import java.util.List;

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.pdfbox.cos.COSArray;
    import org.apache.pdfbox.cos.COSDictionary;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.cos.COSStream;
    import org.apache.pdfbox.cos.COSString;
    import org.apache.pdfbox.pdmodel.common.COSObjectable;
    import org.apache.pdfbox.pdmodel.common.COSArrayList;
    import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification;
    import org.apache.pdfbox.pdmodel.common.filespecification.PDSimpleFileSpecification;
    import org.w3c.dom.Element;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;

    /**
     * This represents an FDF dictionary that is part of the FDF document.
     *
     * @author Ben Litchfield
     */
    public class FDFDictionary implements COSObjectable
    {

        private static final Log LOG = LogFactory.getLog(FDFDictionary.class);

        private final COSDictionary fdf;

        /**
         * Default constructor.
         */
        public FDFDictionary()
        {
            fdf = new COSDictionary();
        }

        /**
         * Constructor.
         *
         * @param fdfDictionary The FDF documents catalog.
         */
        public FDFDictionary(COSDictionary fdfDictionary)
        {
            fdf = fdfDictionary;
        }

        /**
         * This will create an FDF dictionary from an XFDF XML document.
         *
         * @param fdfXML The XML document that contains the XFDF data.
         */
        public FDFDictionary(Element fdfXML)
        {
            this();
            NodeList nodeList = fdfXML.getChildNodes();
            for (int i = 0; i < nodeList.getLength(); i++)
            {
                Node node = nodeList.item(i);
                if (node instanceof Element)
                {
                    Element child = (Element) node;
                    switch (child.getTagName())
                    {
                        case "f":
                            PDSimpleFileSpecification fs = new PDSimpleFileSpecification();
                            fs.setFile(child.getAttribute("href"));
                            setFile(fs);
                            break;
                        case "ids":
                            COSArray ids = new COSArray();
                            String original = child.getAttribute("original");
                            String modified = child.getAttribute("modified");
                            try
                            {
                                ids.add(COSString.parseHex(original));
                            }
                            catch (IOException e)
                            {
                                LOG.warn("Error parsing ID entry for attribute 'original' [" + original +
                                        "]. ID entry ignored.", e);
                            }
                            try
                            {
                                ids.add(COSString.parseHex(modified));
                            }
                            catch (IOException e)
                            {
                                LOG.warn("Error parsing ID entry for attribute 'modified' [" + modified +
                                        "]. ID entry ignored.", e);
                            }
                            setID(ids);
                            break;
                        case "fields":
                            NodeList fields = child.getChildNodes();
                            List<FDFField> fieldList = new ArrayList<>();
                            for (int f = 0; f < fields.getLength(); f++)
                            {
                                Node currentNode = fields.item(f);
                                if (currentNode instanceof Element
                                        && ((Element) currentNode).getTagName().equals("field"))
                                {
                                    try
                                    {
                                        fieldList.add(new FDFField((Element) fields.item(f)));
                                    }
                                    catch (IOException e)
                                    {
                                        LOG.warn("Error parsing field entry [" + currentNode.getNodeValue() +
                                                "]. Field ignored.", e);
                                    }
                                }
                            }
                            setFields(fieldList);
                            break;
                        case "annots":
                            NodeList annots = child.getChildNodes();
                            List<FDFAnnotation> annotList = new ArrayList<>();
                            for (int j = 0; j < annots.getLength(); j++)
                            {
                                Node annotNode = annots.item(j);
                                if (annotNode instanceof Element)
                                {
                                    // the node name defines the annotation type
                                    Element annot = (Element) annotNode;
                                    String annotationName = annot.getNodeName();
                                    try
                                    {
                                        switch (annotationName)
                                        {
                                            case "text":
                                                annotList.add(new FDFAnnotationText(annot));
                                                break;
                                            case "caret":
                                                annotList.add(new FDFAnnotationCaret(annot));
                                                break;
                                            case "freetext":
                                                annotList.add(new FDFAnnotationFreeText(annot));
                                                break;
                                            case "fileattachment":
                                                annotList.add(new FDFAnnotationFileAttachment(annot));
                                                break;
                                            case "highlight":
                                                annotList.add(new FDFAnnotationHighlight(annot));
                                                break;
                                            case "ink":
                                                annotList.add(new FDFAnnotationInk(annot));
                                                break;
                                            case "line":
                                                annotList.add(new FDFAnnotationLine(annot));
                                                break;
                                            case "link":
                                                annotList.add(new FDFAnnotationLink(annot));
                                                break;
                                            case "circle":
                                                annotList.add(new FDFAnnotationCircle(annot));
                                                break;
                                            case "square":
                                                annotList.add(new FDFAnnotationSquare(annot));
                                                break;
                                            case "polygon":
                                                annotList.add(new FDFAnnotationPolygon(annot));
                                                break;
                                            case "polyline":
                                                annotList.add(new FDFAnnotationPolyline(annot));
                                                break;
                                            case "sound":
                                                annotList.add(new FDFAnnotationSound(annot));
                                                break;
                                            case "squiggly":
                                                annotList.add(new FDFAnnotationSquiggly(annot));
                                                break;
                                            case "stamp":
                                                annotList.add(new FDFAnnotationStamp(annot));
                                                break;
                                            case "strikeout":
                                                annotList.add(new FDFAnnotationStrikeOut(annot));
                                                break;
                                            case "underline":
                                                annotList.add(new FDFAnnotationUnderline(annot));
                                                break;
                                            default:
                                                LOG.warn("Unknown or unsupported annotation type '" +
                                                        annotationName + "'");
                                                break;
                                        }
                                    }
                                    catch (IOException e)
                                    {
                                        LOG.warn("Error parsing annotation information [" +
                                                annot.getNodeValue() + "]. Annotation ignored", e);
                                    }
                                }
                            }
                            setAnnotations(annotList);
                            break;
                        default:
                            break;
                    }
                }
            }
        }

        /**
         * This will write this element as an XML document.
         *
         * @param output The stream to write the xml to.
         *
         * @throws IOException If there is an error writing the XML.
         */
        public void writeXML(Writer output) throws IOException
        {
            PDFileSpecification fs = this.getFile();
            if (fs != null)
            {
                output.write("<f href=\"" + fs.getFile() + "\" />\n");
            }
            COSArray ids = this.getID();
            if (ids != null)
            {
                COSString original = (COSString) ids.getObject(0);
                COSString modified = (COSString) ids.getObject(1);
                output.write("<ids original=\"" + original.toHexString() + "\" ");
                output.write("modified=\"" + modified.toHexString() + "\" />\n");
            }
            List<FDFField> fields = getFields();
            if (fields != null && fields.size() > 0)
            {
                output.write("<fields>\n");
                for (FDFField field : fields)
                {
                    field.writeXML(output);
                }
                output.write("</fields>\n");
            }
        }

        /**
         * Convert this standard java object to a COS object.
         *
         * @return The cos object that matches this Java object.
         */
        @Override
        public COSDictionary getCOSObject()
        {
            return fdf;
        }

        /**
         * The source file or target file: the PDF document file that this FDF file was exported from or is intended to be
         * imported into.
         *
         * @return The F entry of the FDF dictionary.
         *
         * @throws IOException If there is an error creating the file spec.
         */
        public PDFileSpecification getFile() throws IOException
        {
            return PDFileSpecification.createFS(fdf.getDictionaryObject(COSName.F));
        }

        /**
         * This will set the file specification.
         *
         * @param fs The file specification.
         */
        public final void setFile(PDFileSpecification fs)
        {
            fdf.setItem(COSName.F, fs);
        }

        /**
         * This is the FDF id.
         *
         * @return The FDF ID.
         */
        public COSArray getID()
        {
            return fdf.getCOSArray(COSName.ID);
        }

        /**
         * This will set the FDF id.
         *
         * @param id The new id for the FDF.
         */
        public final void setID(COSArray id)
        {
            fdf.setItem(COSName.ID, id);
        }

        /**
         * This will get the list of FDF Fields. This will return a list of FDFField objects.
         *
         * @return A list of FDF fields.
         */
        public List<FDFField> getFields()
        {
            List<FDFField> retval = null;
            COSArray fieldArray = fdf.getCOSArray(COSName.FIELDS);
            if (fieldArray != null)
            {
                List<FDFField> fields = new ArrayList<>();
                for (int i = 0; i < fieldArray.size(); i++)
                {
                    fields.add(new FDFField((COSDictionary) fieldArray.getObject(i)));
                }
                retval = new COSArrayList<>(fields, fieldArray);
            }
            return retval;
        }

        /**
         * This will set the list of fields. This should be a list of FDFField objects.
         *
         * @param fields The list of fields.
         */
        public final void setFields(List<FDFField> fields)
        {
            fdf.setItem(COSName.FIELDS, new COSArray(fields));
        }

        /**
         * This will get the status string to be displayed as the result of an action.
         *
         * @return The status.
         */
        public String getStatus()
        {
            return fdf.getString(COSName.STATUS);
        }

        /**
         * This will set the status string.
         *
         * @param status The new status string.
         */
        public void setStatus(String status)
        {
            fdf.setString(COSName.STATUS, status);
        }

        /**
         * This will get the list of FDF Pages. This will return a list of FDFPage objects.
         *
         * @return A list of FDF pages.
         */
        public List<FDFPage> getPages()
        {
            List<FDFPage> retval = null;
            COSArray pageArray = fdf.getCOSArray(COSName.PAGES);
            if (pageArray != null)
            {
                List<FDFPage> pages = new ArrayList<>();
                for (int i = 0; i < pageArray.size(); i++)
                {
                    pages.add(new FDFPage((COSDictionary) pageArray.get(i)));
                }
                retval = new COSArrayList<>(pages, pageArray);
            }
            return retval;
        }

        /**
         * This will set the list of pages. This should be a list of FDFPage objects.
         *
         *
         * @param pages The list of pages.
         */
        public void setPages(List<FDFPage> pages)
        {
            fdf.setItem(COSName.PAGES, new COSArray(pages));
        }

        /**
         * The encoding to be used for a FDF field. The default is PDFDocEncoding and this method will never return null.
         *
         * @return The encoding value.
         */
        public String getEncoding()
        {
            String encoding = fdf.getNameAsString(COSName.ENCODING);
            if (encoding == null)
            {
                encoding = "PDFDocEncoding";
            }
            return encoding;

        }

        /**
         * This will set the encoding.
         *
         * @param encoding The new encoding.
         */
        public void setEncoding(String encoding)
        {
            fdf.setName(COSName.ENCODING, encoding);
        }

        /**
         * This will get the list of FDF Annotations. This will return a list of FDFAnnotation objects or null if the entry
         * is not set.
         *
         * @return A list of FDF annotations.
         *
         * @throws IOException If there is an error creating the annotation list.
         */
        public List<FDFAnnotation> getAnnotations() throws IOException
        {
            List<FDFAnnotation> retval = null;
            COSArray annotArray = fdf.getCOSArray(COSName.ANNOTS);
            if (annotArray != null)
            {
                List<FDFAnnotation> annots = new ArrayList<>();
                for (int i = 0; i < annotArray.size(); i++)
                {
                    annots.add(FDFAnnotation.create((COSDictionary) annotArray.getObject(i)));
                }
                retval = new COSArrayList<>(annots, annotArray);
            }
            return retval;
        }

        /**
         * This will set the list of annotations. This should be a list of FDFAnnotation objects.
         *
         *
         * @param annots The list of annotations.
         */
        public final void setAnnotations(List<FDFAnnotation> annots)
        {
            fdf.setItem(COSName.ANNOTS, new COSArray(annots));
        }

        /**
         * This will get the incremental updates since the PDF was last opened.
         *
         * @return The differences entry of the FDF dictionary.
         */
        public COSStream getDifferences()
        {
            return fdf.getCOSStream(COSName.DIFFERENCES);
        }

        /**
         * This will set the differences stream.
         *
         * @param diff The new differences stream.
         */
        public void setDifferences(COSStream diff)
        {
            fdf.setItem(COSName.DIFFERENCES, diff);
        }

        /**
         * This will get the target frame in the browser to open this document.
         *
         * @return The target frame.
         */
        public String getTarget()
        {
            return fdf.getString(COSName.TARGET);
        }

        /**
         * This will set the target frame in the browser to open this document.
         *
         * @param target The new target frame.
         */
        public void setTarget(String target)
        {
            fdf.setString(COSName.TARGET, target);
        }

        /**
         * This will get the list of embedded FDF entries, or null if the entry is null. This will return a list of
         * PDFileSpecification objects.
         *
         * @return A list of embedded FDF files.
         *
         * @throws IOException If there is an error creating the file spec.
         */
        public List<PDFileSpecification> getEmbeddedFDFs() throws IOException
        {
            List<PDFileSpecification> retval = null;
            COSArray embeddedArray = fdf.getCOSArray(COSName.EMBEDDED_FDFS);
            if (embeddedArray != null)
            {
                List<PDFileSpecification> embedded = new ArrayList<>();
                for (int i = 0; i < embeddedArray.size(); i++)
                {
                    embedded.add(PDFileSpecification.createFS(embeddedArray.get(i)));
                }
                retval = new COSArrayList<>(embedded, embeddedArray);
            }
            return retval;
        }

        /**
         * This will set the list of embedded FDFs. This should be a list of PDFileSpecification objects.
         *
         *
         * @param embedded The list of embedded FDFs.
         */
        public void setEmbeddedFDFs(List<PDFileSpecification> embedded)
        {
            fdf.setItem(COSName.EMBEDDED_FDFS, new COSArray(embedded));
        }

        /**
         * This will get the java script entry.
         *
         * @return The java script entry describing javascript commands.
         */
        public FDFJavaScript getJavaScript()
        {
            COSDictionary dic = fdf.getCOSDictionary(COSName.JAVA_SCRIPT);
            return dic != null ? new FDFJavaScript(dic) : null;
        }

        /**
         * This will set the JavaScript entry.
         *
         * @param js The javascript entries.
         */
        public void setJavaScript(FDFJavaScript js)
        {
            fdf.setItem(COSName.JAVA_SCRIPT, js);
        }

    }