<?PHP
#
#   FILE:  MetadataField.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2012-2013 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

/**
* Object representing a locally-defined type of metadata field.
*/
class MetadataField
{
    # ---- PUBLIC INTERFACE --------------------------------------------------

    # update methods for timestamp fields
    const UPDATEMETHOD_NOAUTOUPDATE   = "NoAutoUpdate";
    const UPDATEMETHOD_ONRECORDCREATE = "OnRecordCreate";
    const UPDATEMETHOD_BUTTON         = "Button";
    const UPDATEMETHOD_ONRECORDEDIT   = "OnRecordEdit";
    const UPDATEMETHOD_ONRECORDCHANGE = "OnRecordChange";

    # values for the *UserIsValue fields
    const USERISVALUE_OR = -1;
    const USERISVALUE_UNSET = 0;
    const USERISVALUE_AND = 1;

    /**
    * Get current error status of object.
    * @return int Error status value drawn from MDFSTAT constants defined
    *       in the MetadataSchema class.
    */
    public function Status()
    {
        return $this->ErrorStatus;
    }

    /**
    * Get/set type of metadata field (enumerated value).  Types are MDFTYPE_
    * constants defined in the MetadataSchema class.
    * @param enum $NewValue New type for field.  (OPTIONAL)
    * @return enum Current type for field.
    */
    public function Type($NewValue = DB_NOVALUE)
    {
        # if new value supplied
        $FTFieldName = $this->DBFields["FieldType"];
        if (($NewValue != DB_NOVALUE)
             && ($NewValue != self::$FieldTypePHPEnums[$FTFieldName]))
        {
            # update database fields and store new type
            $this->ModifyField(NULL, $NewValue);
        }

        # return type to caller
        return self::$FieldTypePHPEnums[$FTFieldName];
    }

    /**
    * Get type of field as string.
    * @return string Field type.
    */
    public function TypeAsName()
    {
        return $this->DBFields["FieldType"];
    }

    /**
    * Check whether field is a type that uses controlled vocabularies.
    * @return bool TRUE if field type is based on controlled vocabularies,
    *       otherwise FALSE.
    */
    public function IsControlledVocabularyField()
    {
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
            case MetadataSchema::MDFTYPE_TREE:
                return TRUE;

            default:
                return FALSE;
        }
    }

    /**
    * Get ID of schema for field.
    * @return int Schema ID.
    */
    public function SchemaId()
    {
        return $this->DBFields["SchemaId"];
    }

    /**
    * Get display name for field.  Returns label if available, or field name
    * if label is not set for field.
    * @return string Display name.
    */
    public function GetDisplayName()
    {
        return strlen($this->Label()) ? $this->Label() : $this->Name();
    }

    /**
    * Get/set name of field.  Field names are limited to alphanumerics, spaces,
    * and parentheses.
    * @param string $NewName New field name.  (OPTIONAL)
    * @return string Current field name.
    */
    public function Name($NewName = DB_NOVALUE)
    {
        # if new name specified
        if ($NewName != DB_NOVALUE
            && trim($NewName) != $this->DBFields["FieldName"])
        {
            $NewName = trim($NewName);
            $NormalizedName = $this->NormalizeFieldNameForDB(strtolower($NewName));

            # if field name is invalid
            if (!preg_match("/^[[:alnum:] \(\)]+$/", $NewName))
            {
                # set error status to indicate illegal name
                $this->ErrorStatus = MetadataSchema::MDFSTAT_ILLEGALNAME;
            }

            # if the new name is a reserved word
            else if ($NormalizedName == "resourceid" || $NormalizedName == "schemaid")
            {
                # set error status to indicate illegal name
                $this->ErrorStatus = MetadataSchema::MDFSTAT_ILLEGALNAME;
            }

            # the name is okay but might be a duplicate
            else
            {
                # check for duplicate name
                $DuplicateCount = $this->DB->Query(
                    "SELECT COUNT(*) AS RecordCount FROM MetadataFields"
                            ." WHERE FieldName = '".addslashes($NewName)."'"
                            ." AND SchemaId = ".intval($this->DBFields["SchemaId"]),
                    "RecordCount");

                # if field name is duplicate
                if ($DuplicateCount > 0)
                {
                    # set error status to indicate duplicate name
                    $this->ErrorStatus = MetadataSchema::MDFSTAT_DUPLICATENAME;
                }
                else
                {
                    # modify database declaration to reflect new field name
                    $this->ErrorStatus = MetadataSchema::MDFSTAT_OK;
                    $this->ModifyField($NewName);
                }
            }
        }

        # return value to caller
        return $this->DBFields["FieldName"];
    }

    /**
    * Get/set label for field.
    * @param string $NewLabel New label for field.  (OPTIONAL)
    * @return string Current label for field.
    */
    public function Label($NewLabel = DB_NOVALUE)
    {
        $ValidValueExp = '/^[[:alnum:] ]*$/';
        $Value = $this->DBFields["Label"];

        # if a new label was specified
        if ($NewLabel !== DB_NOVALUE && trim($NewLabel) != $Value)
        {
            $NewLabel = trim($NewLabel);

            # if field label is valid
            if (preg_match($ValidValueExp, $NewLabel))
            {
                $this->UpdateValue("Label", $NewLabel);
                $Value = $NewLabel;
            }
            # the field label is invalid
            else
            {
                $this->ErrorStatus = MetadataSchema::MDFSTAT_ILLEGALLABEL;
            }
        }

        return $Value;
    }

    /**
    * Get metadata field types that this field can be converted to.
    * @return array Array with constants (MDFTYPE_ values) for the index and
    *       field type strings for the values.
    */
    public function GetAllowedConversionTypes()
    {
        # determine type list based on our type
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TEXT:
            case MetadataSchema::MDFTYPE_PARAGRAPH:
            case MetadataSchema::MDFTYPE_NUMBER:
            case MetadataSchema::MDFTYPE_FLAG:
            case MetadataSchema::MDFTYPE_URL:
                $AllowedTypes = array(
                        MetadataSchema::MDFTYPE_TEXT       => "Text",
                        MetadataSchema::MDFTYPE_PARAGRAPH  => "Paragraph",
                        MetadataSchema::MDFTYPE_NUMBER     => "Number",
                        MetadataSchema::MDFTYPE_FLAG       => "Flag",
                        MetadataSchema::MDFTYPE_URL        => "Url"
                    );
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $AllowedTypes = array(
                        MetadataSchema::MDFTYPE_CONTROLLEDNAME => "ControlledName",
                        MetadataSchema::MDFTYPE_OPTION         => "Option",
                    );
                break;

            case MetadataSchema::MDFTYPE_DATE:
                $AllowedTypes = array(
                        MetadataSchema::MDFTYPE_TEXT  => "Text",
                        MetadataSchema::MDFTYPE_DATE  => "Date",
                    );
                break;

            case MetadataSchema::MDFTYPE_IMAGE:
                $AllowedTypes = array(
                        MetadataSchema::MDFTYPE_TEXT  => "Text",
                        MetadataSchema::MDFTYPE_IMAGE => "Still Image",
                    );
                break;

            case MetadataSchema::MDFTYPE_TIMESTAMP:
            case MetadataSchema::MDFTYPE_TREE:
            case MetadataSchema::MDFTYPE_USER:
            case MetadataSchema::MDFTYPE_FILE:
            case MetadataSchema::MDFTYPE_REFERENCE:
            default:
                $AllowedTypes = array();
                break;
        }

        # return type list to caller
        return $AllowedTypes;
    }

    /**
    * Get/set whether field is temporary instance.
    * @param bool $NewSetting If TRUE, field is a temporary instance, or
    *       if FALSE, field is non-temporary.  (OPTIONAL)
    * @return bool If TRUE, field is a temporary instance, or
    *       if FALSE, field is non-temporary.
    */
    public function IsTempItem($NewSetting = NULL)
    {
        $Schema = new MetadataSchema($this->SchemaId());
        $ItemTableName = "MetadataFields";
        $ItemIdFieldName = "FieldId";
        $ItemFactoryObjectName = "MetadataSchema";
        $ItemAssociationTables = array(
                "FieldQualifierInts",
                );
        $ItemAssociationFieldName = "MetadataFieldId";

        # if new temp item setting supplied
        if (!is_null($NewSetting))
        {
            # if caller requested to switch
            if (($this->Id() < 0 && $NewSetting == FALSE)
                || ($this->Id() >= 0 && $NewSetting == TRUE))
            {
                # if field name is invalid
                if (strlen($this->NormalizeFieldNameForDB($this->Name())) < 1)
                {
                    # set error status to indicate illegal name
                    $this->ErrorStatus = MetadataSchema::MDFSTAT_ILLEGALNAME;
                }
                else
                {
                    # lock DB tables to prevent next ID from being grabbed
                    $DB = $this->DB;
                    $DB->Query("
                        LOCK TABLES ".$ItemTableName." WRITE,
                        APSessions WRITE, APSessionData WRITE,
                        MetadataSchemas WRITE");

                    # nuke stale field cache
                    self::$FieldCache = NULL;

                    # get next temp item ID
                    $OldItemId = $this->Id();
                    $Factory = new $ItemFactoryObjectName();
                    if ($NewSetting == TRUE)
                    {
                        $NewId = $Factory->GetNextTempItemId();
                    }
                    else
                    {
                        $NewId = $Factory->GetNextItemId();
                    }

                    # change item ID
                    $DB->Query("UPDATE ".$ItemTableName." SET ".$ItemIdFieldName." = ".
                        $NewId.  " WHERE ".$ItemIdFieldName." = ".$OldItemId);

                    # release DB tables
                    $DB->Query("UNLOCK TABLES");

                    # change associations
                    foreach ($ItemAssociationTables as $TableName)
                    {
                        $DB->Query("UPDATE ".$TableName." ".
                                "SET ".$ItemAssociationFieldName." = ".$NewId." ".
                                "WHERE ".$ItemAssociationFieldName." = ".$OldItemId);
                    }

                    # if changing item from temp to non-temp
                    if ($NewSetting == FALSE)
                    {
                        # add any needed database fields and/or entries
                        $this->AddDatabaseFields();

                        # Signal that a new (real) field was added:
                        global $AF;
                        $AF->SignalEvent(
                            "EVENT_FIELD_ADDED",
                            array("FieldId" => $NewId ) );

                        # set field order values for new field
                        $Schema->GetDisplayOrder()->AppendItem($NewId, "MetadataField");
                        $Schema->GetEditOrder()->AppendItem($NewId, "MetadataField");
                    }

                    # update metadata field id
                    $this->DBFields["FieldId"] = $NewId;
                    $this->Id = $NewId;
                }
            }
        }

        # report to caller whether we are a temp item
        return ($this->Id() < 0) ? TRUE : FALSE;
    }

    /**
    * Get/set privileges that allowing authoring values for this field.
    * @param object $NewValue New PrivilegeSet value.  (OPTIONAL)
    * @return object PrivilegeSet that allows authoring.
    */
    public function AuthoringPrivileges($NewValue = NULL)
    {
        # if new privileges supplied
        if ($NewValue !== NULL)
        {
            # store new privileges in database
            $this->UpdateValue("AuthoringPrivileges", $NewValue->Data());
            $this->AuthoringPrivileges = $NewValue;
        }

        # return current value to caller
        return $this->AuthoringPrivileges;
    }

    /**
    * Get/set privileges that allowing editing values for this field.
    * @param object $NewValue New PrivilegeSet value.  (OPTIONAL)
    * @return object PrivilegeSet that allows editing.
    */
    public function EditingPrivileges($NewValue = NULL)
    {
        # if new privileges supplied
        if ($NewValue !== NULL)
        {
            # store new privileges in database
            $this->UpdateValue("EditingPrivileges", $NewValue->Data());
            $this->EditingPrivileges = $NewValue;
        }

        # return current value to caller
        return $this->EditingPrivileges;
    }

    /**
    * Get/set privileges that allowing viewing values for this field.
    * @param object $NewValue New PrivilegeSet value.  (OPTIONAL)
    * @return object PrivilegeSet that allows viewing.
    */
    public function ViewingPrivileges($NewValue = NULL)
    {
        # if new privileges supplied
        if ($NewValue !== NULL)
        {
            # store new privileges in database
            $this->UpdateValue("ViewingPrivileges", $NewValue->Data());
            $this->ViewingPrivileges = $NewValue;
        }

        # return current value to caller
        return $this->ViewingPrivileges;
    }

    /**
    * Get/set privileges that allowing previewing values for this field.
    * @param object $NewValue New PrivilegeSet value.  (OPTIONAL)
    * @return object PrivilegeSet that allows previewing.
    */
    public function PreviewingPrivileges($NewValue = NULL)
    {
        # if new privileges supplied
        if ($NewValue !== NULL)
        {
            # store new privileges in database
            $this->UpdateValue("PreviewingPrivileges", $NewValue->Data());
            $this->PreviewingPrivileges = $NewValue;
        }

        # return current value to caller
        return $this->PreviewingPrivileges;
    }

    /**
    * Get metadata field ID.
    * @return int Field ID.
    */
    public function Id()
    {
        return $this->Id;
    }

    /**
    * Get base name of database column used to store metadata field
    * value.  (Only valid for some field types.)
    * @return string Column name.
    */
    public function DBFieldName()
    {
        return $this->DBFields["DBFieldName"];
    }

    /**
    * Get/set field description.
    * @param string $NewValue Updated description.  (OPTIONAL)
    * @return string Current field description.
    */
    public function Description($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("Description", $NewValue);
    }

    /**
    * Get/set field instructions.
    * @param string $NewValue Updated instructions.  (OPTIONAL)
    * @return string Current field instructions.
    */
    public function Instructions($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("Instructions", $NewValue);
    }

    /**
    * Get/set field owner.
    * @param string $NewValue Updated owner.  (OPTIONAL)
    * @return string Current owner.
    */
    public function Owner($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("Owner", $NewValue);
    }

    /**
    * Get/set whether field is enabled.
    * @param bool $NewValue TRUE to enable field, or FALSE to disable.
    *       (OPTIONAL)
    * @return bool TRUE if field is enabled, otherwise FALSE.
    */
    public function Enabled($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("Enabled", $NewValue);
    }

    /**
    * Get/set whether a value is required for this field.
    * @param bool $NewValue TRUE to require a value, or FALSE to make
    *       entering a value optional.  (OPTIONAL)
    * @return bool TRUE if a value is required, otherwise FALSE.
    */
    public function Optional($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("Optional", $NewValue);
    }

    /**
    * Get/set whether this field is editable.
    * @param bool $NewValue TRUE to indicate that field is editable,
    *       or FALSE to indicate it non-editable.  (OPTIONAL)
    * @return bool TRUE if field is editable, otherwise FALSE.
    */
    public function Editable($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("Editable", $NewValue);
    }

    /**
    * Get/set whether to allow multiple values for field.
    * @param bool $NewValue TRUE to allow multiple values, or FALSE if
    *       only one value may be set.  (OPTIONAL)
    * @return bool TRUE if field allows multiple values, otherwise FALSE.
    */
    public function AllowMultiple($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("AllowMultiple", $NewValue);
    }

    /**
    * Get/set whether to include field in keyword search.
    * @param bool $NewValue TRUE to include field, or FALSE if field should
    *       not be included.  (OPTIONAL)
    * @return bool TRUE if field should be included, otherwise FALSE.
    */
    public function IncludeInKeywordSearch($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("IncludeInKeywordSearch", $NewValue);
    }

    /**
    * Get/set whether to include field in advanced search.
    * @param bool $NewValue TRUE to include field, or FALSE if field should
    *       not be included.  (OPTIONAL)
    * @return bool TRUE if field should be included, otherwise FALSE.
    */
    public function IncludeInAdvancedSearch($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("IncludeInAdvancedSearch", $NewValue);
    }

    /**
    * Get/set whether to include field in faceted search.
    * @param bool $NewValue TRUE to include field, or FALSE if field should
    *       not be included.  (OPTIONAL)
    * @return bool TRUE if field should be included, otherwise FALSE.
    */
    public function IncludeInFacetedSearch($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("IncludeInFacetedSearch", $NewValue);
    }

    /**
    * Get/set the search group logic, used for both facets and
    * advanced search when more than one value is selected for this
    * field.
    * @param bool $NewValue One of the SearchEngine::LOGIC_* consts
    * @return Current SearchGroupLogic setting
    */
    public function SearchGroupLogic($NewValue = DB_NOVALUE)
    {
        if ($NewValue !== DB_NOVALUE)
        {
            # if a new value was passed, verify that it's a legal value
            if ($NewValue != SearchEngine::LOGIC_AND &&
                $NewValue != SearchEngine::LOGIC_OR)
            {
                throw new Exception(
                    "Invalid NewValue for SearchGroupLogic(). "
                    ."Must be a SearchEngine::LOGIC_* constant.");
            }
        }

        return $this->UpdateIntValue("SearchGroupLogic", $NewValue);
    }

    /**
    * Get/set whether to include field in search result sort options.
    * @param bool $NewValue TRUE to include field, or FALSE if field should
    *       not be included.  (OPTIONAL)
    * @return bool TRUE if field should be included, otherwise FALSE.
    */
    public function IncludeInSortOptions($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("IncludeInSortOptions", $NewValue);
    }

    /**
    * Get/set whether to include field in recommender system comparisons.
    * @param bool $NewValue TRUE to include field, or FALSE if field should
    *       not be included.  (OPTIONAL)
    * @return bool TRUE if field should be included, otherwise FALSE.
    */
    public function IncludeInRecommender($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("IncludeInRecommender", $NewValue);
    }

    /**
    * Get/set whether to duplciate this field when a resource is duplicated.
    * @param bool $NewValue Update setting.
    * @return Current setting.
    */
    public function CopyOnResourceDuplication($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("CopyOnResourceDuplication", $NewValue);
    }

    /**
    * Get/set the width of text fields.
    * @param int $NewValue Updated value.
    * @return int Current setting.
    */
    public function TextFieldSize($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("TextFieldSize", $NewValue);
    }

    /**
    * Get/set maximum length to store in a text field.
    * @param int $NewValue Updated value.
    * @return int Current setting.
    */
    public function MaxLength($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxLength", $NewValue);
    }

    /**
    * Get/set the number of rows to display for a paragraph field.
    * @param int $NewValue Updated value.
    * @return int Current setting.
    */
    public function ParagraphRows($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("ParagraphRows", $NewValue);
    }

    /**
    * Get/set the number of columns to display for a paragraph field.
    * @param int $NewValue Updated value.
    * @return int Current setting.
    */
    public function ParagraphCols($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("ParagraphCols", $NewValue);
    }

    /**
    * Get/set the minimum value allowed for a number field.
    * @param float $NewValue Updated value.
    * @return float Current Setting.
    */
    public function MinValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateFloatValue("MinValue", $NewValue);
    }

    /**
    * Get/set the maximum allowed value for a number field.
    * @param float $NewValue Updated value (OPTIONAL).
    * @return float Current setting.
    */
    public function MaxValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateFloatValue("MaxValue", $NewValue);
    }

    /**
    * Get/set the label displayed when a flag field is 'on'.
    * @param string $NewValue Updated value (OPTIONAL).
    * @return string Current setting.
    */
    public function FlagOnLabel($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("FlagOnLabel", $NewValue);
    }

    /**
    * Get/set the label displayed when a flag field is 'off'.
    * @param string $NewValue Updated value (OPTIONAL).
    * @return string Current setting.
    */
    public function FlagOffLabel($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("FlagOffLabel", $NewValue);
    }

    /**
    * Get/set the date format.
    * @param string $NewValue Updated value (OPTIONAL).
    * @return string Current setting.
    */
    public function DateFormat($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("DateFormat", $NewValue);
    }

    /**
    * Get/set the weight this field has for search results (higher
    * weights have a larger impact).
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function SearchWeight($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("SearchWeight", $NewValue);
    }

    /**
    * Get/set the weight this field has for recommendations (higher
    * weights have a larger impact).
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function RecommenderWeight($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("RecommenderWeight", $NewValue);
    }

    /**
    * Get/set the max width (in pixels) of images.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function MaxHeight($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxHeight", $NewValue);
    }

    /**
    * Get/set the max width (in pixels) of images.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function MaxWidth($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxWidth", $NewValue);
    }

    /**
    * Get/set the max height (in pixels) of thumbnail images.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function MaxPreviewHeight($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxPreviewHeight", $NewValue);
    }

    /**
    * Get/set the max width (in pixels) of thumbnail images.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function MaxPreviewWidth($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxPreviewWidth", $NewValue);
    }

    /**
    * Get/set the max height (in pixels) of thumbnail images.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function MaxThumbnailHeight($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxThumbnailHeight", $NewValue);
    }

    /**
    * Get/set the max width (in pixels) of thumbnail images.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function MaxThumbnailWidth($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxThumbnailWidth", $NewValue);
    }

    /**
    * Get/set the default alt text for this field
    * @param string $NewValue Updated value (OPTIONAL).
    * @return string Current setting.
    */
    public function DefaultAltText($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("DefaultAltText", $NewValue);
    }

    /**
    * Get/set if this field uses qualifiers.
    * @param bool $NewValue Updated value (OPTIONAL).
    * @return bool Current setting.
    */
    public function UsesQualifiers($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("UsesQualifiers", $NewValue);
    }

    /**
    * Get/set if this field should display qualifiers on EditResource.
    * @param bool $NewValue Updated value (OPTIONAL).
    * @return bool Current setting.
    */
    public function ShowQualifiers($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("ShowQualifiers", $NewValue);
    }

    /**
    * Get/set the default qualifier for this field.
    * @param mixed $NewValue Updated value (OPTIONAL).
    * @return mixed Current setting.
    */
    public function DefaultQualifier($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("DefaultQualifier", $NewValue);
    }

    /**
    * Get/set if this field should allow HTML.
    * @param bool $NewValue Updated value (OPTIONAL).
    * @return bool Current setting.
    */
    public function AllowHTML($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("AllowHTML", $NewValue);
    }

    /**
    * Get/set if this field should enable WYSIWYG editing.
    * @param bool $NewValue Updated value (OPTIONAL).
    * @return bool Current setting.
    */
    public function UseWysiwygEditor($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("UseWysiwygEditor", $NewValue);
    }

    /**
    * Get/set if this field should be used to create OAI sets.
    * @param bool $NewValue Updated value (OPTIONAL).
    * @return bool Current setting.
    */
    public function UseForOaiSets($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("UseForOaiSets", $NewValue);
    }

    /**
    * Get/set if this field should be displayed as a list on the
    * advanced search page.
    * @param bool $NewValue Updated value (OPTIONAL).
    * @return bool Current setting.
    */
    public function DisplayAsListForAdvancedSearch($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("DisplayAsListForAdvancedSearch", $NewValue);
    }

    /**
    * Get/set maximum depth of classifications to display in the list
    * view on the AdvancedSearch page.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function MaxDepthForAdvancedSearch($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("MaxDepthForAdvancedSearch", $NewValue);
    }

    /**
    * Get/set the number of results necessary to active option list menus.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function OptionListThreshold($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("OptionListThreshold", $NewValue);
    }

    /**
    * Get/set the number of results necessary to activate the AJAX dropdown.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function AjaxThreshold($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("AjaxThreshold", $NewValue);
    }

    /**
    * Get/set the maximum number of results to display in an AJAX
    * dropdown.
    * @param int $NewValue Updated value (OPTIONAL).
    * @return int Current setting.
    */
    public function NumAjaxResults($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("NumAjaxResults", $NewValue);
    }



    /**
    * Get/set 'RequiredBySPT'.
    * @param bool $NewValue Updated value (OPTIONAL).
    * @return bool Current setting.
    */
    public function RequiredBySPT($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("RequiredBySPT", $NewValue);
    }

    /**
    * Get/set the current number of digits after the decimal point.
    * @param integer $NewValue Updated value (OPTIONAL).
    * @return integer Current setting.
    */
    public function PointPrecision($NewValue = DB_NOVALUE)
    {
        if ($NewValue !== DB_NOVALUE && $this->Id() >= 0
            && $this->Type() == MetadataSchema::MDFTYPE_POINT)
        {
            $OldValue = $this->UpdateValue("PointPrecision", DB_NOVALUE);

            if ($NewValue != $OldValue)
            {
                $Decimals  = $this->UpdateValue("PointDecimalDigits", DB_NOVALUE);
                $TotalDigits = $NewValue + $Decimals;

                $this->DB->Query("ALTER TABLE Resources MODIFY COLUMN "
                           ."`".$this->DBFields["DBFieldName"]."X` "
                           ."DECIMAL(".$TotalDigits.",".$Decimals.")");
                $this->DB->Query("ALTER TABLE Resources MODIFY COLUMN "
                           ."`".$this->DBFields["DBFieldName"]."Y` "
                           ."DECIMAL(".$TotalDigits.",".$Decimals.")");
            }
        }

        return $this->UpdateValue("PointPrecision", $NewValue);
    }

    /**
    * Get/set the total number of digits a point field should store.
    * @param integer $NewValue Updated value (OPTIONAL).
    * @return integer Current setting.
    */
    public function PointDecimalDigits($NewValue = DB_NOVALUE)
    {
        if ($NewValue !== DB_NOVALUE && $this->Id() >= 0
            && $this->Type() == MetadataSchema::MDFTYPE_POINT)
        {
            $OldValue = $this->UpdateValue("PointDecimalDigits", DB_NOVALUE);

            if ($NewValue != $OldValue)
            {
                $Precision = $this->UpdateValue("PointPrecision", DB_NOVALUE);

                $TotalDigits = $NewValue + $Precision;

                $this->DB->Query("ALTER TABLE Resources MODIFY COLUMN "
                           ."`".$this->DBFields["DBFieldName"]."X` "
                           ."DECIMAL(".$TotalDigits.",".$NewValue.")");
                $this->DB->Query("ALTER TABLE Resources MODIFY COLUMN "
                           ."`".$this->DBFields["DBFieldName"]."Y` "
                           ."DECIMAL(".$TotalDigits.",".$NewValue.")");
            }
        }

        return $this->UpdateValue("PointDecimalDigits", $NewValue);
    }

    /**
    * Get/set default value.
    * @param mixed $NewValue Updated value (OPTIONAL).
    * @return mixed Current setting.
    */
    public function DefaultValue($NewValue = DB_NOVALUE)
    {
        if ($this->Type() == MetadataSchema::MDFTYPE_POINT)
        {
            # valid value given
            if ($NewValue !== DB_NOVALUE &&
                isset($NewValue["X"]) && isset($NewValue["Y"]))
            {
                $NewValue = $NewValue["X"].",".$NewValue["Y"];
            }

            # invalid value given
            else
            {
                $NewValue = DB_NOVALUE;
            }

            $Value = $this->UpdateValue("DefaultValue", $NewValue);

            if (is_array($Value))
            {
                $tmp = explode(",", $Value);

                if (count($tmp)==2)
                {
                    return array("X" => $tmp[0], "Y" => $tmp[1]);
                }
            }

            return array("X" => NULL, "Y" => NULL);
        }

        else if ($this->Type() == MetadataSchema::MDFTYPE_OPTION)
        {
            # multiple default values to set
            if (is_array($NewValue))
            {
                # empty array
                if (count($NewValue) == 0)
                {
                    $NewValue = NULL;
                }

                # multiple defaults are allowed
                else if ($this->AllowMultiple())
                {
                    $NewValue = serialize($NewValue);
                }

                # only one default is allowed so get the first one
                else
                {
                    $NewValue = array_shift($NewValue);
                }
            }

            $Result = $this->UpdateValue("DefaultValue", $NewValue);

            return empty($Result) || is_numeric($Result) ?
                $Result : unserialize($Result);
        }

        return $this->UpdateValue("DefaultValue", $NewValue);
    }

    /**
     * Get/set method by which field is updated.
     * @param string $NewValue New update method.
     * @return Existing update method.
     */
    public function UpdateMethod($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("UpdateMethod", $NewValue);
    }

    /**
    * get possible values (only meaningful for Trees, Controlled Names, Options,
    * Flags, and Users)
    * @param integer|NULL $MaxNumberOfValues Maximum number of values to get.
    * @param integer $Offset Offset into the list of values.
    * @return array ItemIds => Values
    */
    public function GetPossibleValues($MaxNumberOfValues = NULL, $Offset=0)
    {
        # retrieve values based on field type
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TREE:
                $QueryString = "SELECT ClassificationId, ClassificationName"
                        ." FROM Classifications WHERE FieldId = ".$this->Id()
                        ." ORDER BY ClassificationName";
                if ($MaxNumberOfValues)
                {
                    $QueryString .= " LIMIT ".intval($MaxNumberOfValues)." OFFSET "
                        .intval($Offset);
                }
                $this->DB->Query($QueryString);
                $PossibleValues = $this->DB->FetchColumn(
                        "ClassificationName", "ClassificationId");
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $QueryString = "SELECT ControlledNameId, ControlledName"
                        ." FROM ControlledNames WHERE FieldId = ".$this->Id()
                        ." ORDER BY ControlledName";
                if ($MaxNumberOfValues)
                {
                    $QueryString .= " LIMIT ".intval($MaxNumberOfValues)." OFFSET "
                        .intval($Offset);
                }
                $this->DB->Query($QueryString);
                $PossibleValues = $this->DB->FetchColumn(
                        "ControlledName", "ControlledNameId");
                break;

            case MetadataSchema::MDFTYPE_FLAG:
                $PossibleValues[0] = $this->FlagOffLabel();
                $PossibleValues[1] = $this->FlagOnLabel();
                break;

            case MetadataSchema::MDFTYPE_USER:
                $UserFactory = new CWUserFactory();
                $Restrictions = $this->UserPrivilegeRestrictions();
                $PossibleValues = array();

                if (count($Restrictions))
                {
                    $PossibleValues = call_user_func_array(
                        array($UserFactory, "GetUsersWithPrivileges"),
                        $Restrictions);
                }

                else
                {
                    $Users = $UserFactory->GetMatchingUsers(".*.");

                    foreach ($Users as $Id => $Data)
                    {
                        $PossibleValues[$Id] = $Data["UserName"];
                    }
                }

                break;

            default:
                # for everything else return an empty array
                $PossibleValues = array();
                break;
        }

        # return array of possible values to caller
        return $PossibleValues;
    }

    /**
    * Get count of possible values (only meaningful for Trees, Controlled Names,
    * Options, and Users)
    * @return integer Number of possible values.
    */
    public function GetCountOfPossibleValues()
    {
        # retrieve values based on field type
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TREE:
                $Count = $this->DB->Query("SELECT count(*) AS ValueCount"
                        ." FROM Classifications WHERE FieldId = ".$this->Id(),
                        "ValueCount");
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $Count = $this->DB->Query("SELECT count(*) AS ValueCount"
                        ." FROM ControlledNames WHERE FieldId = ".$this->Id(),
                        "ValueCount");
                break;

            case MetadataSchema::MDFTYPE_FLAG:
                $Count = 2;
                break;

            case MetadataSchema::MDFTYPE_USER:
                $Count = count($this->GetPossibleValues());
                break;

            default:
                # for everything else return an empty array
                $Count = 0;
                break;
        }

        # return count of possible values to caller
        return $Count;
    }

    /**
    * Get ID for specified value (only meaningful for Trees / Controlled Names / Options)
    * @param string $Value Value to search for.
    * @return integer|NULL ItemId for the specified value.
    */
    public function GetIdForValue($Value)
    {
        # retrieve ID based on field type
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TREE:
                $Id = $this->DB->Query("SELECT ClassificationId FROM Classifications"
                        ." WHERE ClassificationName = '".addslashes($Value)."'"
                        ." AND FieldId = ".$this->Id(),
                        "ClassificationId");
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $Id = $this->DB->Query("SELECT ControlledNameId FROM ControlledNames"
                        ." WHERE ControlledName = '".addslashes($Value)."'"
                        ." AND FieldId = ".$this->Id(),
                        "ControlledNameId");
                break;

            default:
                # for everything else return NULL
                $Id = NULL;
                break;
        }

        # return ID for value to caller
        return $Id;
    }

    /**
    * Get value for specified ID (only meaningful for Trees / Controlled Names / Options)
    * @param int $Id ItemId to search for.
    * @return string|NULL Value for the specified ID.
    */
    public function GetValueForId($Id)
    {
        # retrieve ID based on field type
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TREE:
                $Value = $this->DB->Query("SELECT ClassificationName FROM Classifications"
                        ." WHERE ClassificationId = '".intval($Id)."'"
                        ." AND FieldId = ".$this->Id(),
                        "ClassificationName");
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $Value = $this->DB->Query("SELECT ControlledName FROM ControlledNames"
                        ." WHERE ControlledNameId = '".intval($Id)."'"
                        ." AND FieldId = ".$this->Id(),
                        "ControlledName");
                break;

            default:
                # for everything else return NULL
                $Value = NULL;
                break;
        }

        # return ID for value to caller
        return $Value;
    }

    /**
     * Check how many times a specific value is currently used for this field.
     * This method is not valid for Date fields.
     * @param mixed $Value Value to check.  For Flag, Tree, Option, Image, and
     *       Controlled Name fields this must be an ID or an appropriate object.
     *       For Point fields this must be an associative array with two values
     *       with "X" and "Y" indexes.  Date fields are not supported.  For other
     *       field types, the literal value to check should be passed in.
     * @return Number of times values is currently used.
     */
    public function ValueUseCount($Value)
    {
        # retrieve ID if object passed in
        if (is_object($Value) && method_exists($Value, "Id"))
        {
            $Value = $Value->Id();
        }

        # check value based on field type
        $DBFieldName = $this->DBFields["DBFieldName"];
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TEXT:
            case MetadataSchema::MDFTYPE_PARAGRAPH:
            case MetadataSchema::MDFTYPE_NUMBER:
            case MetadataSchema::MDFTYPE_USER:
            case MetadataSchema::MDFTYPE_IMAGE:
            case MetadataSchema::MDFTYPE_TIMESTAMP:
            case MetadataSchema::MDFTYPE_URL:
            case MetadataSchema::MDFTYPE_FLAG:
            case MetadataSchema::MDFTYPE_FILE:
                $UseCount = $this->DB->Query("SELECT COUNT(*) AS UseCount"
                        ." FROM Resources"
                        ." WHERE `".$DBFieldName."` = '".addslashes($Value)."'"
                        ." AND SchemaId = ".intval($this->DBFields["SchemaId"]),
                        "UseCount");
                break;

            case MetadataSchema::MDFTYPE_TREE:
                $UseCount = $this->DB->Query("SELECT COUNT(*) AS UseCount"
                        ." FROM ResourceClassInts"
                        ." WHERE ClassificationId = ".intval($Value),
                        "UseCount");
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $UseCount = $this->DB->Query("SELECT COUNT(*) AS UseCount"
                        ." FROM ResourceNameInts"
                        ." WHERE ControlledNameId = ".intval($Value),
                        "UseCount");
                break;

            case MetadataSchema::MDFTYPE_POINT:
                $UseCount = $this->DB->Query("SELECT COUNT(*) AS UseCount"
                        ." FROM Resources"
                        ." WHERE `".$DBFieldName."X` = '".$Value["X"]."'"
                        ." AND `".$DBFieldName."Y` = '".$Value["Y"]."'"
                        ." AND SchemaId = ".intval($this->DBFields["SchemaId"]),
                        "UseCount");
                break;

            default:
                throw new Exception(__CLASS__."::".__METHOD__."() called for"
                        ." unsupported field type (".$this->Type().").");
                break;
        }

        # report use count to caller
        return $UseCount;
    }

    /**
    * Get/set whether field uses item-level qualifiers.
    * @param mixed $NewValue Updated value (OPTIONAL).
    * @return bool TRUE if this field users item-lvel qualifiers,
    *     FALSE otherwise.
    */
    public function HasItemLevelQualifiers($NewValue = DB_NOVALUE)
    {
        # if value provided different from present value
        if (($NewValue != DB_NOVALUE)
            && ($NewValue != $this->DBFields["HasItemLevelQualifiers"]))
        {
            # check if qualifier column currently exists
            $QualColName = $this->DBFieldName()."Qualifier";
            $QualColExists = $this->DB->FieldExists("Resources", $QualColName);

            # if new value indicates qualifiers should now be used
            if ($NewValue == TRUE)
            {
                # if qualifier column does not exist in DB for this field
                if ($QualColExists == FALSE)
                {
                    # add qualifier column in DB for this field
                    $this->DB->Query("ALTER TABLE Resources ADD COLUMN `"
                                     .$QualColName."` INT");
                }
            }
            else
            {
                # if qualifier column exists in DB for this field
                if ($QualColExists == TRUE)
                {
                    # remove qualifier column from DB for this field
                    $this->DB->Query("ALTER TABLE Resources DROP COLUMN `"
                                     .$QualColName."`");
                }
            }
        }

        return $this->UpdateValue("HasItemLevelQualifiers", $NewValue);
    }

    /**
    * Get list of qualifiers associated with field.
    * @return array Associated qualifiers.
    */
    public function AssociatedQualifierList()
    {
        # start with empty list
        $List = array();

        # for each associated qualifier
        $this->DB->Query("SELECT QualifierId FROM FieldQualifierInts"
                     ." WHERE MetadataFieldId = ".$this->DBFields["FieldId"]);
        while ($Record = $this->DB->FetchRow())
        {
            # load qualifier object
            $Qual = new Qualifier($Record["QualifierId"]);

            # add qualifier ID and name to list
            $List[$Qual->Id()] = $Qual->Name();
        }

        # return list to caller
        return $List;
    }

    /**
    * Get list of qualifiers not associated with field.
    * @return array Qualifiers not associated.
    */
    public function UnassociatedQualifierList()
    {
        # grab list of associated qualifiers
        $AssociatedQualifiers = $this->AssociatedQualifierList();

        # get list of all qualifiers
        $QFactory = new QualifierFactory();
        $AllQualifiers = $QFactory->GetItemNames();

        # return list of unassociated qualifiers
        return array_diff($AllQualifiers, $AssociatedQualifiers);
    }

    /**
    * Associate qualifier with field.
    * @param mixed $Qualifier Qualifer ID, name, or object.
    * @throws InvalidArgumentException If unknown name supplied.
    */
    public function AddQualifier($Qualifier)
    {
        # if qualifier object passed in
        if (is_object($Qualifier))
        {
            # grab qualifier ID from object
            $Qualifier = $Qualifier->Id();
        }
        # else if string passed in does not look like ID
        elseif (!is_numeric($Qualifier))
        {
            # assume string passed in is name and use it to retrieve ID
            $QFact = new QualifierFactory();
            $Qualifier = $QFact->GetItemIdByName($Qualifier);
            if ($Qualifier === FALSE)
            {
                throw new InvalidArgumentException("Unknown qualifier name (\""
                        .$Qualifier."\").");
            }
        }

        # if not already associated
        $RecordCount = $this->DB->Query(
            "SELECT COUNT(*) AS RecordCount FROM FieldQualifierInts"
            ." WHERE QualifierId = ".$Qualifier
            ." AND MetadataFieldId = ".$this->Id(), "RecordCount");
        if ($RecordCount < 1)
        {
            # associate field with qualifier
            $this->DB->Query("INSERT INTO FieldQualifierInts SET"
                             ." QualifierId = ".$Qualifier.","
                             ." MetadataFieldId = ".$this->Id());
        }
    }



    /**
    * Delete a qualifier association.
    * @param mixed $QualifierIdOrObject Qualifier to remove from this field.
    */
    public function UnassociateWithQualifier($QualifierIdOrObject)
    {
        # if qualifier object passed in
        if (is_object($QualifierIdOrObject))
        {
            # grab qualifier ID from object
            $QualifierIdOrObject = $QualifierIdOrObject->Id();
        }

        # delete intersection record from database
        $this->DB->Query("DELETE FROM FieldQualifierInts WHERE QualifierId = "
                         .$QualifierIdOrObject." AND MetadataFieldId = ".
                         $this->Id());
    }

    /**
    * Retrieve item factory object for this field.
    * @return ItemFactory Corresponding factory for this field.
    */
    public function GetFactory()
    {
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TREE:
                $Factory = new ClassificationFactory($this->Id());
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $Factory = new ControlledNameFactory($this->Id());
                break;

            default:
                $Factory = NULL;
                break;
        }

        return $Factory;
    }

    /**
    * Determine if a user can view a specified field in the absence of
    * a resource.
    * @param CWUser $User User to check.
    * @param bool $AllowHooksToModify Should events should be signaled
    *        (OPTIONAL, default TRUE)
    * @return TRUE when the user can view this field, FALSE otherwise.
    */
    public function UserCanView($User, $AllowHooksToModify=TRUE)
    {
        $CacheKey = "View".$User->Id() ."-".
            ($AllowHooksToModify ? "1" : "0");

        # see if we have a cached permission for this field and user
        if (!isset($this->PermissionCache[$CacheKey]))
        {
            if (!$this->Enabled())
            {
                # the field should not be viewed if it is disabled
                $this->PermissionCache[$CacheKey] = FALSE;
            }
            else
            {

                $Schema = new MetadataSchema($this->SchemaId());

                # otherwise, evaluate the perms
                $CheckResult =
                    $Schema->ViewingPrivileges()->MeetsRequirements($User) &&
                    $this->ViewingPrivileges()->MeetsRequirements($User);

                # and optionally
                if ($AllowHooksToModify)
                {
                    $SignalResult = $GLOBALS["AF"]->SignalEvent(
                        "EVENT_FIELD_VIEW_PERMISSION_CHECK", array(
                            "Field" => $this,
                            "Resource" => NULL,
                            "User" => $User,
                            "CanView" => $CheckResult));
                    $CheckResult =  $SignalResult["CanView"];
                }

                $this->PermissionCache[$CacheKey] = $CheckResult;
            }
        }

        return $this->PermissionCache[$CacheKey];
    }

    /**
    * Get/set the list of SchemaIds that provide allowable values for
    * a reference field.
    * @param mixed $Ids SchemaId or array/ of SchemaIds that are allowed (OPTIONAL).
    * @return array List of allowed SchemaIds.
    */
    public function ReferenceableSchemaIds($Ids = DB_NOVALUE)
    {
        # if a new value was provided, convert it to a string
        if ($Ids != DB_NOVALUE)
        {
            if (is_array($Ids))
            {
                $Ids = implode(",", $Ids);
            }
        }

        # update/retrieve the value
        $Value = $this->UpdateValue(
            "ReferenceableSchemaIds", $Ids);

        # and convert stored string to an array
        return explode(",", $Value);
    }

    # ---- PRIVATE INTERFACE -------------------------------------------------

    private $DB;
    private $Id;
    private $DBFields;
    private $ErrorStatus;
    private $AuthoringPrivileges;
    private $EditingPrivileges;
    private $ViewingPrivileges;
    private $PreviewingPrivileges;
    private $PermissionCache;

    /**
     * A map of metadata field types to human-readable strings.
     * @var array $FieldTypeHumanEnums
     */
    public static $FieldTypeHumanEnums = array(
            MetadataSchema::MDFTYPE_TEXT             => "Text",
            MetadataSchema::MDFTYPE_PARAGRAPH        => "Paragraph",
            MetadataSchema::MDFTYPE_NUMBER           => "Number",
            MetadataSchema::MDFTYPE_DATE             => "Date",
            MetadataSchema::MDFTYPE_TIMESTAMP        => "Timestamp",
            MetadataSchema::MDFTYPE_FLAG             => "Flag",
            MetadataSchema::MDFTYPE_TREE             => "Tree",
            MetadataSchema::MDFTYPE_CONTROLLEDNAME   => "Controlled Name",
            MetadataSchema::MDFTYPE_OPTION           => "Option",
            MetadataSchema::MDFTYPE_USER             => "User",
            MetadataSchema::MDFTYPE_IMAGE            => "Image",
            MetadataSchema::MDFTYPE_FILE             => "File",
            MetadataSchema::MDFTYPE_URL              => "URL",
            MetadataSchema::MDFTYPE_POINT            => "Point",
            MetadataSchema::MDFTYPE_REFERENCE        => "Reference");

    # field type DB/PHP enum translations
    public static $FieldTypeDBEnums = array(
            MetadataSchema::MDFTYPE_TEXT             => "Text",
            MetadataSchema::MDFTYPE_PARAGRAPH        => "Paragraph",
            MetadataSchema::MDFTYPE_NUMBER           => "Number",
            MetadataSchema::MDFTYPE_DATE             => "Date",
            MetadataSchema::MDFTYPE_TIMESTAMP        => "TimeStamp",
            MetadataSchema::MDFTYPE_FLAG             => "Flag",
            MetadataSchema::MDFTYPE_TREE             => "Tree",
            MetadataSchema::MDFTYPE_CONTROLLEDNAME   => "ControlledName",
            MetadataSchema::MDFTYPE_OPTION           => "Option",
            MetadataSchema::MDFTYPE_USER             => "User",
            MetadataSchema::MDFTYPE_IMAGE            => "Still Image",
            MetadataSchema::MDFTYPE_FILE             => "File",
            MetadataSchema::MDFTYPE_URL              => "Url",
            MetadataSchema::MDFTYPE_POINT            => "Point",
            MetadataSchema::MDFTYPE_REFERENCE        => "Reference"
            );
    public static $FieldTypeDBAllowedEnums = array(
            MetadataSchema::MDFTYPE_TEXT             => "Text",
            MetadataSchema::MDFTYPE_PARAGRAPH        => "Paragraph",
            MetadataSchema::MDFTYPE_NUMBER           => "Number",
            MetadataSchema::MDFTYPE_DATE             => "Date",
            MetadataSchema::MDFTYPE_TIMESTAMP        => "TimeStamp",
            MetadataSchema::MDFTYPE_FLAG             => "Flag",
            MetadataSchema::MDFTYPE_TREE             => "Tree",
            MetadataSchema::MDFTYPE_CONTROLLEDNAME   => "ControlledName",
            MetadataSchema::MDFTYPE_OPTION           => "Option",
            MetadataSchema::MDFTYPE_USER             => "User",
            MetadataSchema::MDFTYPE_IMAGE            => "Still Image",
            MetadataSchema::MDFTYPE_FILE             => "File",
            MetadataSchema::MDFTYPE_URL              => "Url",
            MetadataSchema::MDFTYPE_POINT            => "Point",
            MetadataSchema::MDFTYPE_REFERENCE        => "Reference"
            );
    public static $FieldTypePHPEnums = array(
            "Text"                   => MetadataSchema::MDFTYPE_TEXT,
            "Paragraph"              => MetadataSchema::MDFTYPE_PARAGRAPH,
            "Number"                 => MetadataSchema::MDFTYPE_NUMBER,
            "Date"                   => MetadataSchema::MDFTYPE_DATE,
            "TimeStamp"              => MetadataSchema::MDFTYPE_TIMESTAMP,
            "Flag"                   => MetadataSchema::MDFTYPE_FLAG,
            "Tree"                   => MetadataSchema::MDFTYPE_TREE,
            "ControlledName"         => MetadataSchema::MDFTYPE_CONTROLLEDNAME,
            "Option"                 => MetadataSchema::MDFTYPE_OPTION,
            "User"                   => MetadataSchema::MDFTYPE_USER,
            "Still Image"            => MetadataSchema::MDFTYPE_IMAGE,
            "File"                   => MetadataSchema::MDFTYPE_FILE,
            "Url"                    => MetadataSchema::MDFTYPE_URL,
            "Point"                  => MetadataSchema::MDFTYPE_POINT,
            "Reference"              => MetadataSchema::MDFTYPE_REFERENCE
            );

    public static $UpdateTypes = array(
        self::UPDATEMETHOD_NOAUTOUPDATE   => "Do not update automatically",
        self::UPDATEMETHOD_ONRECORDCREATE => "Update on record creation",
        self::UPDATEMETHOD_BUTTON         => "Provide an update button",
        self::UPDATEMETHOD_ONRECORDEDIT   => "Update when record is edited",
        self::UPDATEMETHOD_ONRECORDCHANGE => "Update when record is changed"
        );

    /**
    * Create a new metadata field.
    * @param int $SchemaId ID of schema in which to place field.
    * @param enum $FieldType Metadata field type.
    * @param string $FieldName Name of metadata field.
    * @param bool $Optional If FALSE, field must always have a value.
    *       (OPTIONAL, defaults to TRUE)
    * @param mixed $DefaultValue Default value for field.
    * @return object New MetadataField object.
    * @throws InvalidArgumentException When field type is invalid.
    * @throws InvalidArgumentException When field name is duplicates name of
    *       another existing field.
    */
    public static function Create($SchemaId, $FieldType, $FieldName,
            $Optional = NULL, $DefaultValue = NULL)
    {
        # error out if field type is bad
        if (empty(self::$FieldTypeDBEnums[$FieldType]))
        {
            throw new InvalidArgumentException("Bad field type (".$FieldType.").");
        }

        # error out if field name is duplicate
        $DB = new Database();
        $FieldName = trim($FieldName);
        $DuplicateCount = $DB->Query(
                "SELECT COUNT(*) AS RecordCount FROM MetadataFields"
                        ." WHERE FieldName = '".addslashes($FieldName)."'"
                        ." AND SchemaId = ".intval($SchemaId),
                "RecordCount");
        if ($DuplicateCount > 0)
        {
            throw new InvalidArgumentException("Duplicate field name (".$FieldName.").");
        }

        # grab current user ID
        $UserId = $GLOBALS["G_User"]->Get("UserId");

        # normalize schema ID
        $Schema = new MetadataSchema($SchemaId);
        $SchemaId = $Schema->Id();

        # use schema privileges as starting privilege values
        $AuthorPrivs = $Schema->AuthoringPrivileges();
        $EditPrivs = $Schema->EditingPrivileges();
        $ViewPrivs = $Schema->ViewingPrivileges();
        $PreviewPrivs = $Schema->ViewingPrivileges();

        # lock DB tables and get next temporary field ID
        $DB->Query("LOCK TABLES MetadataFields WRITE");
        $FieldId = $Schema->GetNextTempItemId();

        # add field to MDF table in database
        $DB->Query("INSERT INTO MetadataFields"
                ." (FieldId, SchemaId, FieldName, FieldType, LastModifiedById,"
                        ." Optional, AuthoringPrivileges, EditingPrivileges,"
                        ." ViewingPrivileges, PreviewingPrivileges)"
                ." VALUES ("
                .intval($FieldId).", "
                .intval($SchemaId).","
                ." '".addslashes($FieldName)."',"
                ." '".self::$FieldTypeDBEnums[$FieldType]."', "
                .intval($UserId).", "
                .($Optional ? "1" : "0").","
                ."'".$DB->EscapeString($AuthorPrivs->Data())."',"
                ."'".$DB->EscapeString($EditPrivs->Data())."',"
                ."'".$DB->EscapeString($ViewPrivs->Data())."',"
                ."'".$DB->EscapeString($PreviewPrivs->Data())."')");

        # release DB tables
        $DB->Query("UNLOCK TABLES");

        # nuke potentially stale cache information
        self::$FieldCache = NULL;

        # load field object
        $Field = new MetadataField($FieldId);

        # set field defaults
        $Field->SetDefaults();

        # set the default value if specified
        if ($DefaultValue !== NULL)
        {
            $Field->DefaultValue($DefaultValue);
        }

        # return newly-constructed field to caller
        return $Field;
    }

    /**
    * Create duplicate of field.  The new field will be a temporary instance,
    * so if it is to persist, IsTempItem() must be called on it with FALSE.
    * The only difference between the original and the duplicate (other than
    * their IDs and possibly the temporary status) is that the duplicate will
    * have "(duplicate YYMMDD-HHMMSS)" appended to the field name.
    * @return object New duplicate field.
    */
    public function Duplicate()
    {
        # create new field
        $NewName = $this->Name()." (duplicate ".date("ymd-His").")";
        $NewField = self::Create($this->SchemaId(), $this->Type(), $NewName);

        # copy all attributes to database record for new field
        $TableName = "MetadataFields";
        $IdColumn = "FieldId";
        $SrcId = $this->Id();
        $DstId = $NewField->Id();
        $ColumnsToExclude = array("FieldName");
        $this->DB->CopyValues(
                $TableName, $IdColumn, $SrcId, $DstId, $ColumnsToExclude);

        # reload new field and return to caller
        return new MetadataField($NewField->Id());
    }

    /**
    * Object contstructor, used to load an existing metadata field.  To create
    * new fields, use
    * @param int $FieldId ID of metadata field to load.
    * @return object New MetadataField object.
    */
    public function __construct($FieldId)
    {
        # assume everything will be okay
        $this->ErrorStatus = MetadataSchema::MDFSTAT_OK;

        # check if we have cached field info
        $this->DB = new Database();
        if (self::$FieldCache === NULL)
        {
            # if not, retrieve field info from database
            $this->DB->Query("SELECT * FROM MetadataFields");
            while ($Row = $this->DB->FetchRow())
            {
                self::$FieldCache[$Row["FieldId"]] = $Row;
            }
        }

        # error if requested field did not exist
        if (!array_key_exists($FieldId, self::$FieldCache) )
        {
            throw new InvalidArgumentException("Invalid metadata field ID ("
                    .$FieldId.") from "
                    .StdLib::GetMyCaller().".");
        }
        $Row = self::$FieldCache[$FieldId];
        $this->DBFields = $Row;
        $this->Id = $FieldId;

        # if privileges have not yet been initialized
        if (!strlen($this->DBFields["AuthoringPrivileges"]))
        {
            # set default values for privileges from metadata schema
            $Schema = new MetadataSchema($Row["SchemaId"]);
            $this->AuthoringPrivileges($Schema->AuthoringPrivileges());
            $this->EditingPrivileges($Schema->EditingPrivileges());
            $this->ViewingPrivileges($Schema->ViewingPrivileges());
            $this->PreviewingPrivileges($Schema->ViewingPrivileges());
        }
        else
        {
            # set privileges from stored values
            $this->AuthoringPrivileges = new PrivilegeSet(
                    $Row["AuthoringPrivileges"]);
            $this->EditingPrivileges = new PrivilegeSet(
                    $Row["EditingPrivileges"]);
            $this->ViewingPrivileges = new PrivilegeSet(
                    $Row["ViewingPrivileges"]);
            $this->PreviewingPrivileges = new PrivilegeSet(
                    $Row["PreviewingPrivileges"]);
        }

        # set database column name
        $this->DBFields["DBFieldName"] =
                $this->NormalizeFieldNameForDB($this->DBFields["FieldName"]);
    }

    /**
    * The metadata field defaults that are the same for all field types.
    * @var array $FixedDefaults
    */
    public static $FixedDefaults = array(
        "Label" => NULL,
        "Description" => NULL,
        "Instructions" => NULL,
        "Enabled" => TRUE,
        "Optional" => TRUE,
        "Editable" => TRUE,
        "CopyOnResourceDuplication" => TRUE,
        "AllowMultiple" => FALSE,
        "IncludeInKeywordSearch" => FALSE,
        "IncludeInAdvancedSearch" => FALSE,
        "IncludeInFacetedSearch" => FALSE,
        "SearchGroupLogic" => SearchEngine::LOGIC_OR,
        "IncludeInSortOptions" => TRUE,
        "IncludeInRecommender" => FALSE,
        "ParagraphRows" => 4,
        "ParagraphCols" => 50,
        "MinValue" => 1,
        "FlagOnLabel" => "On",
        "FlagOffLabel" => "Off",
        "DateFormat" => NULL,
        "RecommenderWeight" => 1,
        "MaxHeight" => 500,
        "MaxWidth" => 500,
        "MaxPreviewHeight" => 100,
        "MaxPreviewWidth" => 100,
        "MaxThumbnailHeight" => 50,
        "MaxThumbnailWidth" => 50,
        "DefaultAltText" => NULL,
        "UsesQualifiers" => FALSE,
        "HasItemLevelQualifiers" => FALSE,
        "ShowQualifiers" => FALSE,
        "DefaultQualifier" => NULL,
        "AllowHTML" => FALSE,
        "UseWysiwygEditor" => FALSE,
        "UseForOaiSets" => FALSE,
        "DisplayAsListForAdvancedSearch" => FALSE,
        "MaxDepthForAdvancedSearch" => 1,
        "OptionListThreshold" => 25,
        "AjaxThreshold" => 50,
        "NumAjaxResults" => 50,
        "PointPrecision" => 8,
        "PointDecimalDigits" => 5,
        "UserPrivilegeRestrictions" => array(),
        "UpdateMethod" => "NoAutoUpdate",
        # 9999 is the default max value because default number field length is 4
        "MaxValue" => 9999);

    /**
     * The metadata field defaults that vary depending on the field type.
     * @var array $TypeBasedDefaults
     */
    public static $TypeBasedDefaults = array(
        MetadataSchema::MDFTYPE_TEXT  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_PARAGRAPH  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_NUMBER  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 4,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_DATE  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 10,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_TIMESTAMP  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_FLAG  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_TREE  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "AllowMultiple" => TRUE,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_CONTROLLEDNAME  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 3,
            "AllowMultiple" => TRUE,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_OPTION  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 3,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_USER  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_IMAGE  => array(
            "DefaultValue" => NULL,
            "CopyOnResourceDuplication" => FALSE,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_FILE  => array(
            "DefaultValue" => NULL,
            "CopyOnResourceDuplication" => FALSE,
            "AllowMultiple" => TRUE,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_URL  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 255),
        MetadataSchema::MDFTYPE_POINT  => array(
            "DefaultValue" => array("X" => NULL, "Y" => NULL),
            "SearchWeight" => 1,
            "TextFieldSize" => 10,
            "MaxLength" => 100),
        MetadataSchema::MDFTYPE_REFERENCE  => array(
            "DefaultValue" => NULL,
            "SearchWeight" => 1,
            "TextFieldSize" => 50,
            "MaxLength" => 100,
            "ReferenceableSchemaIds" => array(
                MetadataSchema::SCHEMAID_DEFAULT) ));

    /**
    * Set defaults values for the field.
    */
    public function SetDefaults()
    {
        # set defaults that are the same for every field
        foreach (self::$FixedDefaults as $Key => $Value)
        {
            $this->$Key($Value);
        }

        # set defaults that depend on the type of the field
        foreach (self::$TypeBasedDefaults[$this->Type()] as $Key => $Value)
        {
            $this->$Key($Value);
        }

        # tweak the update method if dealing with the date of record creation
        if ($this->Name() == "Date Of Record Creation")
        {
            $this->UpdateMethod("OnRecordCreate");
        }
    }


    /**
    *  Remove field from database (only for use by MetadataSchema object).
    */
    public function Drop()
    {
        StdLib::CheckMyCaller(
            "MetadataSchema",
            "Attempt to update drop Metadata Field at %FILE%:%LINE%."
            ." (Fields may only be dropped by MetadataSchema.)");

        # clear other database entries as appropriate for field type
        $DB = $this->DB;
        $DBFieldName = $this->DBFields["DBFieldName"];
        $Schema = new MetadataSchema($this->SchemaId());
        switch (self::$FieldTypePHPEnums[$this->DBFields["FieldType"]])
        {
            case MetadataSchema::MDFTYPE_TEXT:
            case MetadataSchema::MDFTYPE_PARAGRAPH:
            case MetadataSchema::MDFTYPE_NUMBER:
            case MetadataSchema::MDFTYPE_USER:
            case MetadataSchema::MDFTYPE_IMAGE:
            case MetadataSchema::MDFTYPE_TIMESTAMP:
            case MetadataSchema::MDFTYPE_URL:
            case MetadataSchema::MDFTYPE_FLAG:
                # remove field from resources table
                if ($DB->FieldExists("Resources", $DBFieldName))
                {
                    $DB->Query("ALTER TABLE Resources DROP COLUMN `".$DBFieldName."`");
                }
                break;

            case MetadataSchema::MDFTYPE_POINT:
                if ($DB->FieldExists("Resources", $DBFieldName."X"))
                {
                    $DB->Query("ALTER TABLE Resources DROP COLUMN `".$DBFieldName."X`");
                    $DB->Query("ALTER TABLE Resources DROP COLUMN `".$DBFieldName."Y`");
                }
                break;

            case MetadataSchema::MDFTYPE_DATE:
                # remove fields from resources table
                if ($DB->FieldExists("Resources", $DBFieldName."Begin"))
                {
                    $DB->Query("ALTER TABLE Resources "
                            ."DROP COLUMN `".$DBFieldName."Begin`");
                    $DB->Query("ALTER TABLE Resources "
                            ."DROP COLUMN `".$DBFieldName."End`");
                    $DB->Query("ALTER TABLE Resources "
                            ."DROP COLUMN `".$DBFieldName."Precision`");
                }
                break;

            case MetadataSchema::MDFTYPE_TREE:
                $DB->Query("SELECT ClassificationId FROM Classifications "
                           ."WHERE FieldId = ".$this->Id());
                $TempDB = new Database();
                while ($ClassificationId = $DB->FetchField("ClassificationId"))
                {
                    # remove any resource / name intersections
                    $TempDB->Query("DELETE FROM ResourceClassInts WHERE "
                                   ."ClassificationId = ".$ClassificationId);

                    # remove controlled name
                    $TempDB->Query("DELETE FROM Classifications WHERE "
                                   ."ClassificationId = ".$ClassificationId);
                }
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $DB->Query("SELECT ControlledNameId FROM ControlledNames "
                           ."WHERE FieldId = ".$this->Id());
                $TempDB = new Database();
                while ($ControlledNameId = $DB->FetchField("ControlledNameId"))
                {
                    # remove any resource / name intersections
                    $TempDB->Query("DELETE FROM ResourceNameInts WHERE "
                                   ."ControlledNameId = ".$ControlledNameId);

                    # remove any variant names
                    $TempDB->Query("DELETE FROM VariantNames WHERE "
                                   ."ControlledNameId = ".$ControlledNameId);

                    # remove controlled name
                    $TempDB->Query("DELETE FROM ControlledNames WHERE "
                                   ."ControlledNameId = ".$ControlledNameId);
                }
                break;

            case MetadataSchema::MDFTYPE_FILE:
                # for each file associated with this field
                $DB->Query("SELECT FileId FROM Files WHERE FieldId = '".$this->Id()."'");
                while ($FileId = $DB->FetchRow())
                {
                    # delete file
                    $File = new File(intval($FileId));
                    $File->Delete();
                }
                break;

            case MetadataSchema::MDFTYPE_REFERENCE:
                # remove any resource references for the field
                $DB->Query("
                    DELETE FROM ReferenceInts
                    WHERE FieldId = '".addslashes($this->Id())."'");
                break;
        }

        # remove field from database
        $DB->Query("DELETE FROM MetadataFields "
                   ."WHERE FieldId = '".$this->DBFields["FieldId"]."'");

        # remove any qualifier associations
        $DB->Query("DELETE FROM FieldQualifierInts WHERE MetadataFieldId = '"
                   .$this->DBFields["FieldId"]."'");

        # get the order objects the field is part of
        foreach (MetadataFieldOrder::GetOrdersForSchema($Schema) as $Order)
        {
            # remove it if it's a direct descendant
            $Order->RemoveItem($this->Id(), "MetadataField");

            # also make sure to remove it if it's part of a group
            foreach ($Order->GetItemIds() as $Item)
            {
                if ($Item["Type"] == "MetadataFieldGroup")
                {
                    $Group = new MetadataFieldGroup($Item["ID"]);
                    $Group->RemoveItem($this->Id(), "MetadataField");
                }
            }
        }

        # nuke stale field cache
        self::$FieldCache = NULL;
    }

    /**
    * Edit the name and/or type of this Metadata Field.
    * @param mixed $NewName Name to use when renaming or NULL to leave
    *     the current name alone.
    * @param mixed $NewType New field type that this field should be
    *     convereted into or NULL when no type conversion is desired.
    */
    private function ModifyField($NewName = NULL, $NewType = NULL)
    {
        # grab old DB field name
        $OldDBFieldName = $this->DBFields["DBFieldName"];
        $OldFieldType = NULL;

        # if new field name supplied
        if ($NewName != NULL)
        {
            # cache the old name for options and controllednames below
            $OldName = $this->DBFields["FieldName"];

            # store new name
            $this->UpdateValue("FieldName", $NewName);

            # determine new DB field name
            $NewDBFieldName = $this->NormalizeFieldNameForDB($NewName);

            # store new database field name
            $this->DBFields["DBFieldName"] = $NewDBFieldName;
        }
        else
        {
            # set new field name equal to old field name
            $NewDBFieldName = $OldDBFieldName;
        }

        # if new type supplied
        if ($NewType != NULL)
        {
            # grab old field type
            $OldFieldType = self::$FieldTypePHPEnums[$this->DBFields["FieldType"]];

            # store new field type
            $this->UpdateValue("FieldType", self::$FieldTypeDBEnums[$NewType]);
        }

        # if this is not a temporary field
        if ($this->Id() >= 0)
        {
            # modify field in DB as appropriate for field type
            $DB = $this->DB;
            $FieldType = self::$FieldTypePHPEnums[$this->DBFields["FieldType"]];
            switch ($FieldType)
            {
                case MetadataSchema::MDFTYPE_TEXT:
                case MetadataSchema::MDFTYPE_PARAGRAPH:
                case MetadataSchema::MDFTYPE_URL:
                    # alter field declaration in Resources table
                    $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                            .$OldDBFieldName."` `"
                            .$NewDBFieldName."` TEXT DEFAULT NULL");
                    break;

                case MetadataSchema::MDFTYPE_NUMBER:
                    # alter field declaration in Resources table
                    $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                            .$OldDBFieldName."` `"
                            .$NewDBFieldName."` INT DEFAULT NULL");

                    break;

                case MetadataSchema::MDFTYPE_POINT:
                    $Precision = $this->UpdateValue("PointPrecision",
                                                    DB_NOVALUE);
                    $Digits    = $this->UpdateValue("PointDecimalDigits",
                                                    DB_NOVALUE);
                    $DB->Query("ALTER TABLE Resources CHANGE COLUMN "
                               ."`".$OldDBFieldName."X` "
                               ."`".$NewDBFieldName."X`".
                               " DECIMAL(".$Precision.",".$Digits.")");
                    $DB->Query("ALTER TABLE Resources CHANGE COLUMN "
                               ."`".$OldDBFieldName."Y` "
                               ."`".$NewDBFieldName."Y`".
                               " DECIMAL(".$Precision.",".$Digits.")");
                    break;

                case MetadataSchema::MDFTYPE_FLAG:
                    # alter field declaration in Resources table
                    $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                               .$OldDBFieldName."` `"
                               .$NewDBFieldName."` INT"
                               ." DEFAULT ".intval($this->DefaultValue()));

                    # set any unset values to default
                    $DB->Query("UPDATE Resources SET `".$NewDBFieldName
                            ."` = ".intval($this->DefaultValue())
                            ." WHERE `".$NewDBFieldName."` IS NULL");
                    break;

                case MetadataSchema::MDFTYPE_DATE:
                    # if new type supplied and new type is different from old
                    if (($NewType != NULL) && ($NewType != $OldFieldType))
                    {
                        # if old type was time stamp
                        if ($OldFieldType == MetadataSchema::MDFTYPE_TIMESTAMP)
                        {
                            # change time stamp field in resources table to begin date
                            $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                                       .$OldDBFieldName."` `"
                                       .$NewDBFieldName."Begin` DATE "
                                       ."DEFAULT NULL");

                            # add end date and precision fields
                            $DB->Query("ALTER TABLE Resources "
                                    ."ADD COLUMN `".$NewDBFieldName."End` DATE");
                            $DB->Query("ALTER TABLE Resources "
                                    ."ADD COLUMN `".$NewDBFieldName."Precision`"
                                    ."INT DEFAULT NULL");


                            # set precision to reflect time stamp content
                            $DB->Query("UPDATE Resources "
                                    ."SET `".$NewDBFieldName."Precision` = "
                                    .(DATEPRE_BEGINYEAR|DATEPRE_BEGINMONTH
                                    |DATEPRE_BEGINDAY));
                        }
                        else
                        {
                            exit("<br>ERROR:  Attempt to convert metadata field "
                                    ."to date from type other than timestamp<br>\n");
                        }
                    }
                    else
                    {
                        # change name of fields
                        $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                                   .$OldDBFieldName."Begin` `"
                                   .$NewDBFieldName."Begin` DATE "
                                   ."DEFAULT NULL");
                        $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                                   .$OldDBFieldName."End` `"
                                   .$NewDBFieldName."End` DATE "
                                   ."DEFAULT NULL");
                        $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                                   .$OldDBFieldName."Precision` `"
                                   .$NewDBFieldName."Precision` INT "
                                   ."DEFAULT NULL");
                    }
                    break;

                case MetadataSchema::MDFTYPE_TIMESTAMP:
                    # if new type supplied and new type is different from old
                    if (($NewType != NULL) && ($NewType != $OldFieldType))
                    {
                        # if old type was date
                        if ($OldFieldType == MetadataSchema::MDFTYPE_DATE)
                        {
                            # change begin date field in resource table to time stamp
                            $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                                       .$OldDBFieldName."Begin` `"
                                       .$NewDBFieldName."` DATETIME "
                                       ."DEFAULT NULL");

                            # drop end date and precision fields
                            $DB->Query("ALTER TABLE Resources DROP COLUMN `"
                                       .$OldDBFieldName."End`");
                            $DB->Query("ALTER TABLE Resources DROP COLUMN `"
                                       .$OldDBFieldName."Precision`");
                        }
                        else
                        {
                            exit("<br>ERROR:  Attempt to convert metadata field to "
                                    ."time stamp from type other than date<br>\n");
                        }
                    }
                    else
                    {
                        # change name of field
                        $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                                   .$OldDBFieldName."` `"
                                   .$NewDBFieldName."` DATETIME "
                                   ."DEFAULT NULL");
                    }
                    break;

                case MetadataSchema::MDFTYPE_TREE:
                case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
                case MetadataSchema::MDFTYPE_OPTION:
                case MetadataSchema::MDFTYPE_REFERENCE:
                case MetadataSchema::MDFTYPE_IMAGE:
                case MetadataSchema::MDFTYPE_FILE:
                    break;
            }

            # if qualifier DB field exists
            if ($DB->FieldExists("Resources", $OldDBFieldName."Qualifier"))
            {
                # rename qualifier DB field
                $DB->Query("ALTER TABLE Resources CHANGE COLUMN `"
                           .$OldDBFieldName."Qualifier` `"
                           .$NewDBFieldName."Qualifier` INT ");
            }
        }
    }

    /**
    * Update a value stored in the database.
    * @param string $FieldName Name of the database column to update.
    * @param string $NewValue New value to set.
    * @return string Updated value.
    */
    private function UpdateValue($FieldName, $NewValue)
    {
        # nuke stale field cache
        self::$FieldCache = NULL;

        return $this->DB->UpdateValue("MetadataFields", $FieldName, $NewValue,
                               "FieldId = ".intval($this->DBFields["FieldId"]),
                               $this->DBFields);
    }

    /**
    * Update an integer point value stored in the database.
    * @param string $FieldName Name of the database column to update.
    * @param string $NewValue New value to set.
    * @return string Updated value.
    */
    private function UpdateIntValue($FieldName, $NewValue)
    {
        # nuke stale field cache
        self::$FieldCache = NULL;

        return $this->DB->UpdateIntValue("MetadataFields", $FieldName, $NewValue,
                               "FieldId = ".intval($this->DBFields["FieldId"]),
                               $this->DBFields);
    }

    /**
    * Update a floating point value stored in the database.
    * @param string $FieldName Name of the database column to update.
    * @param string $NewValue New value to set.
    * @return string Updated value.
    */
    private function UpdateFloatValue($FieldName, $NewValue)
    {
        # nuke stale field cache
        self::$FieldCache = NULL;

        return $this->DB->UpdateFloatValue("MetadataFields", $FieldName, $NewValue,
                               "FieldId = ".intval($this->DBFields["FieldId"]),
                               $this->DBFields);
    }

    /**
    * Update a boolean value stored in the database.
    * @param string $FieldName Name of the database column to update.
    * @param string $NewValue New value to set.
    * @return string Updated value.
    */
    private function UpdateBoolValue($FieldName, $NewValue)
    {
        # nuke stale field cache
        self::$FieldCache = NULL;

        $NewValue = $this->TranslateStringToConstants($NewValue);
        return $this->DB->UpdateIntValue("MetadataFields", $FieldName, $NewValue,
                               "FieldId = ".intval($this->DBFields["FieldId"]),
                               $this->DBFields);
    }

    /**
    * Update database records for CONST values.
    * @param string $FieldName Name of the database column to update.
    * @param string $NewValue New value to set.
    * @param mixed $ClassName Class name to pull constants from.
    * @return int Updated value.
    */
    private function UpdateConstValue($FieldName, $NewValue, $ClassName=NULL)
    {
        # nuke stale field cache
        self::$FieldCache = NULL;

        $NewValue = $this->TranslateStringToConstants($NewValue, $ClassName);
        return $this->DB->UpdateIntValue("MetadataFields", $FieldName, $NewValue,
                               "FieldId = ".intval($this->DBFields["FieldId"]),
                               $this->DBFields);
    }

    /**
    * Normalize field name for use as database field name.
    * @param string $Name Metadata field name to normalize.
    * @return string DB-safe name.
    */
    private function NormalizeFieldNameForDB($Name)
    {
        return preg_replace("/[^a-z0-9]/i", "", $Name)
                .(($this->SchemaId() != MetadataSchema::SCHEMAID_DEFAULT)
                        ? $this->SchemaId() : "");
    }

    /**
    * Add any necessary database fields and/or entries.
    */
    private function AddDatabaseFields()
    {
        # grab values for common use
        $DB = $this->DB;
        $DBFieldName = $this->DBFieldName();

        # set up field(s) based on field type
        switch ($this->Type())
        {
            case MetadataSchema::MDFTYPE_TEXT:
            case MetadataSchema::MDFTYPE_PARAGRAPH:
            case MetadataSchema::MDFTYPE_URL:
                # add field to resources table (if not already present)
                if (!$DB->FieldExists("Resources", $DBFieldName))
                {
                    $DB->Query("ALTER TABLE Resources ADD COLUMN `".$DBFieldName
                               ."` TEXT DEFAULT NULL");
                }

                break;

            case MetadataSchema::MDFTYPE_NUMBER:
                # add field to resources table (if not already present)
                if (!$DB->FieldExists("Resources", $DBFieldName))
                {
                    $DB->Query("ALTER TABLE Resources ADD COLUMN `".$DBFieldName
                               ."` INT DEFAULT NULL");
                }
                break;

            case MetadataSchema::MDFTYPE_POINT:
                if (!$DB->FieldExists("Resources", $DBFieldName."X"))
                {
                    $Precision = $this->UpdateValue("PointPrecision",
                                                    DB_NOVALUE);
                    $Digits    = $this->UpdateValue("PointDecimalDigits",
                                                    DB_NOVALUE);

                    $DB->Query("ALTER TABLE Resources ADD COLUMN `"
                               .$DBFieldName."X`".
                               " DECIMAL(".$Precision.",".$Digits.") DEFAULT NULL");
                    $DB->Query("ALTER TABLE Resources ADD COLUMN `"
                               .$DBFieldName."Y`".
                               " DECIMAL(".$Precision.",".$Digits.") DEFAULT NULL");
                }

                break;
            case MetadataSchema::MDFTYPE_FLAG:
                # if field is not already present in database
                if (!$DB->FieldExists("Resources", $DBFieldName))
                {
                    # add field to resources table
                    $DB->Query("ALTER TABLE Resources ADD COLUMN `".$DBFieldName
                               ."` INT DEFAULT NULL");
                }
                break;

            case MetadataSchema::MDFTYPE_DATE:
                # add fields to resources table (if not already present)
                if (!$DB->FieldExists("Resources", $DBFieldName."Begin"))
                {
                    $DB->Query("ALTER TABLE Resources ADD COLUMN `".$DBFieldName."Begin`"
                               ." DATE DEFAULT NULL");
                }
                if (!$DB->FieldExists("Resources", $DBFieldName."End"))
                {
                    $DB->Query("ALTER TABLE Resources ADD COLUMN `".$DBFieldName."End`"
                               ." DATE DEFAULT NULL");
                }
                if (!$DB->FieldExists("Resources", $DBFieldName."Precision"))
                {
                    $DB->Query("ALTER TABLE Resources "
                            ."ADD COLUMN `".$DBFieldName."Precision`"
                            ." INT DEFAULT NULL");
                }
                break;

            case MetadataSchema::MDFTYPE_TIMESTAMP:
                # add fields to resources table (if not already present)
                if (!$DB->FieldExists("Resources", $DBFieldName))
                {
                    $DB->Query("ALTER TABLE Resources ADD COLUMN `".$DBFieldName
                               ."` DATETIME DEFAULT NULL");
                }
                break;

            case MetadataSchema::MDFTYPE_FILE:
            case MetadataSchema::MDFTYPE_IMAGE:
            case MetadataSchema::MDFTYPE_TREE:
            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
            case MetadataSchema::MDFTYPE_REFERENCE:
            case MetadataSchema::MDFTYPE_USER:
                break;

            default:
                exit("<br>ERROR:  Attempt to add database fields "
                        ."for illegal metadata field type<br>\n");
                break;
        }
    }

    /**
    * Translate string with boolean or one or more PHP constant names into
    * corresponding values.
    * @param string $CString String containing boolean or constant(s).
    * @param string $ClassName Name of class in which to search for constant. (OPTIONAL)
    * @return Constant value(s) found.  (Multiple values are ORed together.)
    */
    private function TranslateStringToConstants($CString, $ClassName = NULL)
    {
        # if not a string return value unchanged to caller
        if (!is_string($CString) || ($CString === DB_NOVALUE))
        {
            $ReturnValue = $CString;
        }
        # handle booleans as a special case
        elseif (strtoupper(trim($CString)) == "TRUE")
        {
            $ReturnValue = TRUE;
        }
        elseif (strtoupper(trim($CString)) == "FALSE")
        {
            $ReturnValue = FALSE;
        }
        else
        {
            # assume no values will be found
            $ReturnValue = NULL;

            # split apart any ORed-together values
            $Values = explode("|", $CString);

            # for each value found
            foreach ($Values as $Value)
            {
                # trim off any extraneous whitespace
                $Value = trim($Value);

                # add class name prefix to constant name if requested
                if ($ClassName) {  $Value = $ClassName."::".$Value;  }

                # if value corresponds to a constant
                if (defined($Value))
                {
                    # add constant to return value
                    $ReturnValue = ($ReturnValue === NULL)
                            ? constant($Value)
                            : ($ReturnValue | constant($Value));
                }
            }

            # if no corresponding constants were found
            if ($ReturnValue === NULL)
            {
                # return original value to caller
                $ReturnValue = $CString;
            }
        }

        # return result to caller
        return $ReturnValue;
    }

    /**
    * Storage for metadata field information to reduce repeated DB queries.
    */
    private static $FieldCache = NULL;


    # ---- DEPRECATED METHODS ------------------------------------------------
    #
    # These are maintained only for backward compatibility with older
    # code.  Newer code should not use them.
    #
    // @codingStandardsIgnoreStart

    public function Viewable()
    {
        return $this->UserCanView( $GLOBALS["G_User"] );
    }

    /**
    * @see AddQualifier()
    */
    public function AssociateWithQualifier($Qualifier)
    {
        $this->AddQualifier($Qualifier);
    }

    public function UserPrivilegeRestrictions($NewValue = DB_NOVALUE)
    {
        # new value
        if ($NewValue != DB_NOVALUE)
        {
            $NewValue = serialize((array) $NewValue);
        }

        $Value = $this->UpdateValue("UserPrivilegeRestrictions", $NewValue);

        # value set
        if (strlen($Value))
        {
            $Value = (array) unserialize($Value);
        }

        # no value set, set it to an empty array
        else
        {
            $Value = $this->UserPrivilegeRestrictions(array());
        }

        return $Value;
    }

    public function AuthoringUserIsValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateConstValue(
                "AuthoringUserIsValue", $NewValue, "MetadataField");
    }

    public function EditingUserIsValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateConstValue("EditingUserIsValue", $NewValue, "MetadataField");
    }

    public function ViewingUserValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("ViewingUserValue", $NewValue, "MetadataField");
    }

    public function AuthoringUserValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("AuthoringUserValue", $NewValue, "MetadataField");
    }

    public function EditingUserValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateIntValue("EditingUserValue", $NewValue, "MetadataField");
    }

    public function ViewingUserIsValue($NewValue = DB_NOVALUE)
    {
        return $this->UpdateConstValue("ViewingUserIsValue", $NewValue, "MetadataField");
    }

    public function EnableOnOwnerReturn($NewValue = DB_NOVALUE)
    {
        return $this->UpdateBoolValue("EnableOnOwnerReturn", $NewValue);
    }

    public function ViewingPrivilege($NewValue = DB_NOVALUE)
    {
        if ($NewValue === DB_NOVALUE)
        {
            return $this->ViewingPrivileges();
        }
        else
        {
            throw new Exception("Deprecated ".__METHOD__."() called -- ".
              __METHOD__."s() should be used instead.");
        }
    }

    public function AuthoringPrivilege($NewValue = DB_NOVALUE)
    {
        if ($NewValue === DB_NOVALUE)
        {
            return $this->AuthoringPrivileges();
        }
        else
        {
            throw new Exception("Deprecated ".__METHOD__."() called -- ".
                    __METHOD__."s() should be used instead.");
        }
    }

    public function EditingPrivilege($NewValue = DB_NOVALUE)
    {
        if ($NewValue === DB_NOVALUE)
        {
            return $this->EditingPrivileges();
        }
        else
        {
            throw new Exception("Deprecated ".__METHOD__."() called -- ".
                    __METHOD__."s() should be used instead.");
        }
    }

    public function ImagePreviewPrivilege($NewValue = DB_NOVALUE)
    {
        if ($NewValue === DB_NOVALUE)
        {
            return $this->ViewingPrivileges();
        }
        else
        {
            throw new Exception("Deprecated ".__METHOD__."() called -- ".
                                "ViewingPrivileges() should be used instead.");
        }
    }

    public function TreeBrowsingPrivilege($NewValue = DB_NOVALUE)
    {
        if ($NewValue === DB_NOVALUE)
        {
            return $this->ViewingPrivileges();
        }
        else
        {
            throw new Exception("Deprecated ".__METHOD__."() called -- ".
                    "this should probably be using ViewingPrivileges() instead.");
        }
    }


    // @codingStandardsIgnoreEnd
}
