pdfbox-trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDField.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.interactive.form;

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

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
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.fdf.FDFField;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;

/**
 * A field in an interactive form.
 */
public abstract class PDField implements COSObjectable
{
    private static final int FLAG_READ_ONLY = 1;
    private static final int FLAG_REQUIRED = 1 << 1;
    private static final int FLAG_NO_EXPORT = 1 << 2;

    private final PDAcroForm acroForm;
    private final PDNonTerminalField parent;
    private final COSDictionary dictionary;
   
    /**
     * Constructor.
     * 
     * @param acroForm The form that this field is part of.
     */
    PDField(PDAcroForm acroForm)
    {
        this(acroForm, new COSDictionary(), null);
    }

    /**
     * Constructor.
     *  @param acroForm The form that this field is part of.
     * @param field the PDF object to represent as a field.
     * @param parent the parent node of the node
     */
    PDField(PDAcroForm acroForm, COSDictionary field, PDNonTerminalField parent)
    {
        this.acroForm = acroForm;
        this.dictionary = field;
        this.parent = parent;
    }
    
    /**
     * Creates a COSField subclass from the given COS field. This is for reading fields from PDFs.
     *
     * @param form the form that the field is part of
     * @param field the dictionary representing a field element
     * @param parent the parent node of the node to be created, or null if root.
     * @return a new PDField instance
     */
    static PDField fromDictionary(PDAcroForm form, COSDictionary field, PDNonTerminalField parent)
    {
        return PDFieldFactory.createField(form, field, parent);
    }

    /**
     * Returns the given attribute, inheriting from parent nodes if necessary.
     *
     * @param key the key to look up
     * @return COS value for the given key
     */
    protected COSBase getInheritableAttribute(COSName key)
    {
        if (dictionary.containsKey(key))
        {
            return dictionary.getDictionaryObject(key);
        }
        else if (parent != null)
        {
            return parent.getInheritableAttribute(key);
        }
        else
        {
            return acroForm.getCOSObject().getDictionaryObject(key);
        }
    }
    
    /**
     * Get the FT entry of the field. This is a read only field and is set depending on the actual type. The field type
     * is an inheritable attribute.
     * 
     * @return The Field type.
     * 
     */
    public abstract String getFieldType();

    /**
     * Returns a string representation of the "V" entry, or an empty string.
     * 
     * @return The list of widget annotations.
     */
    public abstract String getValueAsString();

    /**
     * Sets the value of the field.
     *
     * @param value the new field value.
     * 
     * @throws IOException if the value could not be set
     */
    public abstract void setValue(String value) throws IOException;
    
    
    /**
     * Returns the widget annotations associated with this field.
     * 
     * For {@link PDNonTerminalField} the list will be empty as non terminal fields
     * have no visual representation in the form.
     * 
     * @return a List of {@link PDAnnotationWidget} annotations. Be aware that this list is
     * <i>not</i> backed by the actual widget collection of the field, so adding or deleting has no
     * effect on the PDF document. For {@link PDTerminalField} you'd have to call
     * {@link PDTerminalField#setWidgets(java.util.List) setWidgets()} with the modified list.
     */
    public abstract List<PDAnnotationWidget> getWidgets();
    
    /**
     * sets the field to be read-only.
     * 
     * @param readonly The new flag for readonly.
     */
    public void setReadOnly(boolean readonly)
    {
        dictionary.setFlag(COSName.FF, FLAG_READ_ONLY, readonly);
    }

    /**
     * 
     * @return true if the field is readonly
     */
    public boolean isReadOnly()
    {
        return dictionary.getFlag(COSName.FF, FLAG_READ_ONLY);
    }

    /**
     * sets the flag whether the field is to be required to have a value at the time it is exported
     * by a submit-form action.
     *
     * @param required The new flag for required.
     */
    public void setRequired(boolean required)
    {
        dictionary.setFlag(COSName.FF, FLAG_REQUIRED, required);
    }

    /**
     * @return true if the field is required to have a value at the time it is exported by a
     * submit-form action.
     */
    public boolean isRequired()
    {
        return dictionary.getFlag(COSName.FF, FLAG_REQUIRED);
    }

    /**
     * sets the field to be not exported.
     * 
     * @param noExport The new flag for noExport.
     */
    public void setNoExport(boolean noExport)
    {
        dictionary.setFlag(COSName.FF, FLAG_NO_EXPORT, noExport);
    }

    /**
     * 
     * @return true if the field is not to be exported.
     */
    public boolean isNoExport()
    {
        return dictionary.getFlag(COSName.FF, FLAG_NO_EXPORT);
    }

    /**
     * This will get the flags for this field.
     * 
     * @return flags The set of flags.
     */
    public abstract int getFieldFlags();

    /**
     * This will set the flags for this field.
     * 
     * @param flags The new flags.
     */
    public void setFieldFlags(int flags)
    {
        dictionary.setInt(COSName.FF, flags);
    }

    /**
     * Get the additional actions for this field. This will return null if there
     * are no additional actions for this field.
     *
     * @return The actions of the field.
     */
    public PDFormFieldAdditionalActions getActions()
    {
        COSDictionary aa = dictionary.getCOSDictionary(COSName.AA);
        return aa != null ? new PDFormFieldAdditionalActions(aa) : null;
    }

   /**
     * This will import a fdf field from a fdf document.
     * 
     * @param fdfField The fdf field to import.
     * @throws IOException If there is an error importing the data for this field.
     */
    void importFDF(FDFField fdfField) throws IOException
    {
        COSBase fieldValue = fdfField.getCOSValue();
        
        if (fieldValue != null && this instanceof PDTerminalField)
        {
            PDTerminalField currentField = (PDTerminalField) this;
            
            if (fieldValue instanceof COSName)
            {
                currentField.setValue(((COSName) fieldValue).getName());
            }
            else if (fieldValue instanceof COSString)
            {
                currentField.setValue(((COSString) fieldValue).getString());
            }
            else if (fieldValue instanceof COSStream)
            {
                currentField.setValue(((COSStream) fieldValue).toTextString());
            }
            else if (fieldValue instanceof COSArray && this instanceof PDChoice)
            {
                ((PDChoice) this).setValue(((COSArray) fieldValue).toCOSStringStringList());
            }
            else
            {
                throw new IOException("Error:Unknown type for field import" + fieldValue);
            }
        }
        else if (fieldValue != null)
        {
            dictionary.setItem(COSName.V, fieldValue);
        }
        
        Integer ff = fdfField.getFieldFlags();
        if (ff != null)
        {
            setFieldFlags(ff);
        }
        else
        {
            // these are suppose to be ignored if the Ff is set.
            Integer setFf = fdfField.getSetFieldFlags();
            int fieldFlags = getFieldFlags();
            
            if (setFf != null)
            {
                int setFfInt = setFf;
                fieldFlags = fieldFlags | setFfInt;
                setFieldFlags(fieldFlags);
            }

            Integer clrFf = fdfField.getClearFieldFlags();
            if (clrFf != null)
            {
                // we have to clear the bits of the document fields for every bit that is
                // set in this field.
                //
                // Example:
                // docFf = 1011
                // clrFf = 1101
                // clrFfValue = 0010;
                // newValue = 1011 & 0010 which is 0010
                int clrFfValue = clrFf;
                clrFfValue ^= 0xFFFFFFFF;
                fieldFlags = fieldFlags & clrFfValue;
                setFieldFlags(fieldFlags);
            }
        }
    }

    /**
     * Exports this field and its children as FDF.
     */
    abstract FDFField exportFDF() throws IOException;
    
    /**
     * Get the parent field to this field, or null if none exists.
     * 
     * @return The parent field.
     */
    public PDNonTerminalField getParent()
    {
        return parent;
    }

    /**
     * This will find one of the child elements. The name array are the components of the name to search down the tree
     * of names. The nameIndex is where to start in that array. This method is called recursively until it finds the end
     * point based on the name array.
     * 
     * @param name An array that picks the path to the field.
     * @param nameIndex The index into the array.
     * @return The field at the endpoint or null if none is found.
     */
    PDField findKid(String[] name, int nameIndex)
    {
        PDField retval = null;
        COSArray kids = dictionary.getCOSArray(COSName.KIDS);
        if (kids != null)
        {
            for (int i = 0; retval == null && i < kids.size(); i++)
            {
                COSDictionary kidDictionary = (COSDictionary) kids.getObject(i);
                if (name[nameIndex].equals(kidDictionary.getString(COSName.T)))
                {
                    retval = PDField.fromDictionary(acroForm, kidDictionary,
                                                    (PDNonTerminalField)this);
                    if (retval != null && name.length > nameIndex + 1)
                    {
                        retval = retval.findKid(name, nameIndex + 1);
                    }
                }
            }
        }
        return retval;
    }

    /**
     * This will get the acroform that this field is part of.
     * 
     * @return The form this field is on.
     */
    public PDAcroForm getAcroForm()
    {
        return acroForm;
    }

    /**
     * This will get the dictionary associated with this field.
     * 
     * @return the dictionary that this class wraps.
     */
    @Override
    public COSDictionary getCOSObject()
    {
        return dictionary;
    }

    /**
     * Returns the partial name of the field.
     * 
     * @return the name of the field
     */
    public String getPartialName()
    {
        return dictionary.getString(COSName.T);
    }

    /**
     * This will set the partial name of the field.
     * 
     * @param name The new name for the field.
     * @throws IllegalArgumentException If the name contains a period character.
     */
    public void setPartialName(String name)
    {
        if (name.contains("."))
        {
            throw new IllegalArgumentException(
                    "A field partial name shall not contain a period character: " + name);
        }
        dictionary.setString(COSName.T, name);
    }

    /**
     * Returns the fully qualified name of the field, which is a concatenation of the names of all the parents fields.
     * 
     * @return the name of the field
     */
    public String getFullyQualifiedName()
    {
        String finalName = getPartialName();
        String parentName = parent != null ? parent.getFullyQualifiedName() : null;
        if (parentName != null)
        {
            if (finalName != null)
            {
                finalName = parentName + "." + finalName;
            }
            else
            {
                finalName = parentName;
            }
        }
        return finalName;
    }

    /**
     * Gets the alternate name of the field ("shall be used in place of the actual field name
     * wherever the field shall be identified in the user interface (such as in error or status
     * messages referring to the field)").
     *
     * @return the alternate name of the field
     */
    public String getAlternateFieldName()
    {
        return dictionary.getString(COSName.TU);
    }

    /**
     * This will set the alternate name of the field ("shall be used in place of the actual field
     * name wherever the field shall be identified in the user interface (such as in error or status
     * messages referring to the field)"). The text appears as a tool tip in Adobe Reader. Because
     * of the usage for error or status messages, it should be different for each field.
     *
     * @param alternateFieldName the alternate name of the field.
     */
    public void setAlternateFieldName(String alternateFieldName)
    {
        dictionary.setString(COSName.TU, alternateFieldName);
    }
    
    /**
     * Gets the mapping name of the field.
     * 
     * The mapping name shall be used when exporting interactive form field
     * data from the document.
     * 
     * @return the mapping name of the field
     */
    public String getMappingName()
    {
        return dictionary.getString(COSName.TM);
    }

    /**
     * This will set the mapping name of the field.
     * 
     * @param mappingName the mapping name of the field
     */
    public void setMappingName(String mappingName)
    {
        dictionary.setString(COSName.TM, mappingName);
    }

    @Override
    public String toString()
    {
        return getFullyQualifiedName() + "{type: " + getClass().getSimpleName() + " value: " +
                getInheritableAttribute(COSName.V) + "}";
    }
}
