<?PHP
#
#   FILE:  Resource.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2011 Edward Almasy and Internet Scout Project
#   http://scout.wisc.edu/
#

/**
* Represents a "resource" in CWIS.
*/
class Resource {

    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
    * Object constructor.
    * @param ResourceId ID of resource to load.  New resource is created if
    *       no ID is supplied.
    */
    function Resource($ResourceId = NULL)
    {
        $this->DB = new SPTDatabase();
        $DB = $this->DB;
        $this->Schema = new MetadataSchema();

        # if resource ID supplied
        if ($ResourceId !== NULL)
        {
            # save resource ID
            $this->Id = intval($ResourceId);

            # locate resource in database
            $DB->Query("SELECT * FROM Resources WHERE ResourceId = ".$this->Id);

            # if unable to locate resource
            $Record = $DB->FetchRow();
            if ($Record == FALSE)
            {
                # set status to -1 to indicate that creation failed
                $this->LastStatus = -1;
            }
            else
            {
                # load in attributes from database
                $this->DBFields = $Record;
                $this->CumulativeRating = $Record["CumulativeRating"];

                # set status to 1 to indicate that creation succeeded
                $this->LastStatus = 1;
            }
        }
        else
        {
            # clean out any temp resource records more than three days old
            $RFactory = new ResourceFactory();
            $RFactory->CleanOutStaleTempItems(60 * 24 * 3);

            # lock DB tables to prevent next ID from being grabbed
            $DB->Query(
                "LOCK TABLES Resources write, APUsers read, ".
                "APSessions write, APSessionData write");

            # find next temp resource ID
            $this->Id = $RFactory->GetNextTempItemId();

            # write out new resource record with temp resource ID and user ID
            global $User;
            $DB->Query("INSERT INTO Resources (ResourceId, AddedById,"
                    ." LastModifiedById, DateLastModified, DateOfRecordCreation)"
                       ." VALUES (".$this->Id.", "
                                   .$User->Get("UserId").", "
                                   .$User->Get("UserId").", "
                                   ."NOW(), NOW())");

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

            # load in attributes from database
            $DB->Query("SELECT * FROM Resources WHERE ResourceId = ".$this->Id);
            $this->DBFields = $DB->FetchRow();
            $this->CumulativeRating = $this->DBFields["CumulativeRating"];

            # set any default values
            $Schema = new MetadataSchema();
            $Fields = $Schema->GetFields(MetadataSchema::MDFTYPE_OPTION
                    |MetadataSchema::MDFTYPE_TEXT
                    |MetadataSchema::MDFTYPE_FLAG
                    |MetadataSchema::MDFTYPE_NUMBER
                    |MetadataSchema::MDFTYPE_POINT);
            foreach ($Fields as $Field)
            {
                $DefaultValue = $Field->DefaultValue();

                # flip option default values to get into the form that
                # Resource::Set() expects
                if ($Field->Type() == MetadataSchema::MDFTYPE_OPTION
                    && is_array($DefaultValue))
                {
                    $DefaultValue = array_flip($DefaultValue);
                }

                $this->SetByField($Field, $DefaultValue);
            }

            # Update timestamps as required:
            $TimestampFields = $Schema->GetFields(
                MetadataSchema::MDFTYPE_TIMESTAMP);
            foreach ($TimestampFields as $Field)
            {
                if ($Field->UpdateMethod() ==
                    MetadataField::UPDATEMETHOD_ONRECORDCREATE)
                {
                    $this->SetByField($Field, "now");
                }
            }

            # signal resource creation
            $GLOBALS["AF"]->SignalEvent("EVENT_RESOURCE_CREATE", array(
                    "Resource" => $this,
                    ));

            # set status to 1 to indicate that creation succeeded
            $this->LastStatus = 1;
        }
    }

    /**
    * Remove resource (and accompanying associations) from database and
    * delete any associated files.
    */
    function Delete()
    {
        global $SysConfig;

        # signal that resource deletion is about to occur
        global $AF;
        $AF->SignalEvent("EVENT_RESOURCE_DELETE", array(
                "Resource" => $this,
                ));

        # grab list of classifications
        $Classifications = $this->Classifications();

        # delete resource/classification intersections
        $DB = $this->DB;
        $DB->Query("DELETE FROM ResourceClassInts WHERE ResourceId = ".$this->Id());

        # for each classification type
        foreach ($Classifications as $ClassType => $ClassesOfType)
        {
            # for each classification of that type
            foreach ($ClassesOfType as $ClassId => $ClassName)
            {
                # recalculate resource count for classification
                $Class = new Classification($ClassId);
                $Class->RecalcResourceCount();
            }
        }

        # delete resource/name intersections
        $DB->Query("DELETE FROM ResourceNameInts WHERE ResourceId = ".$this->Id());

        # delete any associated images not in use by other resources
        $Fields = $this->Schema->GetFields(MetadataSchema::MDFTYPE_IMAGE);
        foreach ($Fields as $Field)
        {
            $ImageId = $DB->Query("SELECT `".$Field->DBFieldName()
                                  ."` FROM Resources WHERE ResourceId = ".$this->Id(),
                                  $Field->DBFieldName());
            if ($ImageId > 0)
            {
                $ImageCount = $DB->Query("SELECT COUNT(*) AS ImageCount FROM Resources"
                                         ." WHERE ".$Field->DBFieldName()." = ".$ImageId,
                                         "ImageCount");
                if ($ImageCount < 2)
                {
                    $Image = new SPTImage($ImageId);
                    $Image->Delete();
                }
            }
        }

        # delete any associated files
        $Factory = new FileFactory(NULL);
        $Files = $Factory->GetFilesForResource($this->Id());
        foreach ($Files as $File)
        {
            $File->Delete();
        }

        # delete resource record from database
        $DB->Query("DELETE FROM Resources WHERE ResourceId = ".$this->Id());

        # drop item from search engine and recommender system
        if ($SysConfig->SearchDBEnabled())
        {
            $SearchEngine = new SPTSearchEngine();
            $SearchEngine->DropItem($this->Id());
        }
        if ($SysConfig->RecommenderDBEnabled())
        {
            $Recommender = new SPTRecommender();
            $Recommender->DropItem($this->Id());
        }

        # delete any resource comments
        $DB->Query("DELETE FROM Messages WHERE ParentId = ".$this->Id);
    }

    /**
    * Retrieve result of last operation if available.
    * @return Result of last operation (if available).
    */
    function Status() {  return $this->LastStatus;  }

    /**
    * Retrieve numerical resource ID.
    * @return Resource ID.
    */
    function Id() {  return $this->Id;  }

    /**
    * Get/set whether resource is a temporary record.
    * @param NewSetting TRUE/FALSE setting for whether resource is temporary. (OPTIONAL)
    * @return TRUE if resource is temporary record, or FALSE otherwise.
    */
    function IsTempResource($NewSetting = NULL)
    {
        # if new temp resource setting supplied
        if (!is_null($NewSetting))
        {
            # if caller requested to switch
            $DB = $this->DB;
            if ((($this->Id() < 0) && ($NewSetting == FALSE))
                    || (($this->Id() >= 0) && ($NewSetting == TRUE)))
            {
                # lock DB tables to prevent next ID from being grabbed
                $DB->Query("LOCK TABLES Resources write");

                # get next resource ID as appropriate
                $OldResourceId = $this->Id;
                $Factory = new ResourceFactory();
                if ($NewSetting == TRUE)
                {
                    $this->Id = $Factory->GetNextTempItemId();
                }
                else
                {
                    $this->Id = $Factory->GetNextItemId();
                }

                # change resource ID
                $DB->Query("UPDATE Resources SET ResourceId = ".
                    $this->Id.  " WHERE ResourceId = ".$OldResourceId);

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

                # change associations
                unset($this->ClassificationCache);
                $DB->Query("UPDATE ResourceClassInts SET ResourceId = ".
                    $this->Id.  " WHERE ResourceId = ".$OldResourceId);
                unset($this->ControlledNameCache);
                unset($this->ControlledNameVariantCache);
                $DB->Query("UPDATE ResourceNameInts SET ResourceId = ".
                    $this->Id.  " WHERE ResourceId = ".$OldResourceId);
                $DB->Query("UPDATE Files SET ResourceId = ".
                    $this->Id.  " WHERE ResourceId = ".$OldResourceId);

                # signal event as appropriate
                if ($NewSetting === FALSE)
                {
                    $GLOBALS["AF"]->SignalEvent("EVENT_RESOURCE_ADD", array(
                            "Resource" => $this,
                            ));
                }
            }
        }

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


    # --- Generic Attribute Retrieval Methods -------------------------------

    /**
    * Retrieve value using field name or field object.
    * @param FieldNameOrObject Full name of field or a Field object.
    * @param ReturnObject For field types that can return multiple values, if
    *       TRUE, returns array of objects, else returns array of values.
    *       Defaults to FALSE.
    * @param IncludeVariants If TRUE, includes variants in return value.
    *       Only applicable for ControlledName fields.
    * @return Requested object(s) or value(s).  Returns empty array (for field
    *       types that allow multiple values) or NULL (for field types that do
    *       not allow multiple values) if no values found.  Returns NULL if
    *       field does not exist or was otherwise invalid.
    * @see GetByFieldId
    */
    function Get($FieldNameOrObject, $ReturnObject = FALSE, $IncludeVariants = FALSE)
    {
        # load field object if needed
        $Field = is_object($FieldNameOrObject) ? $FieldNameOrObject
                : $this->Schema->GetFieldByName($FieldNameOrObject);

        # return no value found if we don't have a valid field
        if ((get_class($Field) != "MetadataField")
                || ($Field->Status() != MetadataSchema::MDFSTAT_OK))
                {  return NULL;  }

        # grab database field name
        $DBFieldName = $Field->DBFieldName();

        # format return value based on field type
        switch ($Field->Type())
        {
            case MetadataSchema::MDFTYPE_TEXT:
            case MetadataSchema::MDFTYPE_PARAGRAPH:
            case MetadataSchema::MDFTYPE_NUMBER:
            case MetadataSchema::MDFTYPE_FLAG:
            case MetadataSchema::MDFTYPE_URL:
                if (isset($this->DBFields[$DBFieldName]))
                {
                    $ReturnValue = $this->DBFields[$DBFieldName];
                }
                else
                {
                    $ReturnValue = NULL;
                }
                break;

            case MetadataSchema::MDFTYPE_POINT:
                $ReturnValue = array("X" => $this->DBFields[$DBFieldName."X"],
                                     "Y" => $this->DBFields[$DBFieldName."Y"]);
                break;

            case MetadataSchema::MDFTYPE_DATE:
                $Date = new Date($this->DBFields[$DBFieldName."Begin"],
                                    $this->DBFields[$DBFieldName."End"],
                                    $this->DBFields[$DBFieldName."Precision"]);
                if ($ReturnObject)
                {
                    $ReturnValue = $Date;
                }
                else
                {
                    $ReturnValue = $Date->Formatted();
                }
                break;

            case MetadataSchema::MDFTYPE_TIMESTAMP:
                $ReturnValue = $this->DBFields[$DBFieldName];
                break;

            case MetadataSchema::MDFTYPE_TREE:
                # start with empty array
                $ReturnValue = array();

                # if classification cache has not been loaded
                if (!isset($this->ClassificationCache))
                {
                    # load all classifications associated with this resource into cache
                    $this->ClassificationCache = array();
                    $this->DB->Query("SELECT Classifications.ClassificationId,Classifications.FieldId,ClassificationName "
                            ."FROM ResourceClassInts, Classifications  "
                            ."WHERE ResourceClassInts.ResourceId = ".$this->Id." "
                            ."AND ResourceClassInts.ClassificationId = Classifications.ClassificationId ");
                    while ($Record = $this->DB->FetchRow())
                    {
                        $this->ClassificationCache[$Record["ClassificationId"]]["Name"] =
                                $Record["ClassificationName"];
                        $this->ClassificationCache[$Record["ClassificationId"]]["FieldId"] =
                                $Record["FieldId"];
                    }
                }

                # for each entry in classification cache
                foreach ($this->ClassificationCache as $ClassificationId => $ClassificationInfo)
                {
                    # if classification ID matches field we are looking for
                    if ($ClassificationInfo["FieldId"] == $Field->Id())
                    {
                        # add field to result
                        if ($ReturnObject)
                        {
                            $ReturnValue[$ClassificationId] = new Classification($ClassificationId);
                        }
                        else
                        {
                            $ReturnValue[$ClassificationId] = $ClassificationInfo["Name"];
                        }
                    }
                }
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                # start with empty array
                $ReturnValue = array();

                # if controlled name cache has not been loaded
                if (!isset($this->ControlledNameCache))
                {
                    # load all controlled names associated with this resource into cache
                    $this->ControlledNameCache = array();
                    $this->DB->Query("SELECT ControlledNames.ControlledNameId,ControlledNames.FieldId,ControlledName "
                            ."FROM ResourceNameInts, ControlledNames  "
                            ."WHERE ResourceNameInts.ResourceId = ".$this->Id." "
                            ."AND ResourceNameInts.ControlledNameId = ControlledNames.ControlledNameId ");
                    while ($Record = $this->DB->FetchRow())
                    {
                        $this->ControlledNameCache[$Record["ControlledNameId"]]["Name"] = $Record["ControlledName"];
                        $this->ControlledNameCache[$Record["ControlledNameId"]]["FieldId"] = $Record["FieldId"];
                    }
                }

                # if variant names requested and variant name cache has not been loaded
                if ($IncludeVariants && !isset($this->ControlledNameVariantCache))
                {
                    # load all controlled names associated with this resource into cache
                    $this->ControlledNameVariantCache = array();
                    $this->DB->Query("SELECT ControlledNames.ControlledNameId,ControlledNames.FieldId,ControlledName,VariantName "
                            ."FROM ResourceNameInts, ControlledNames, VariantNames  "
                            ."WHERE ResourceNameInts.ResourceId = ".$this->Id." "
                            ."AND ResourceNameInts.ControlledNameId = ControlledNames.ControlledNameId "
                            ."AND VariantNames.ControlledNameId = ControlledNames.ControlledNameId");
                    while ($Record = $this->DB->FetchRow())
                    {
                        $this->ControlledNameVariantCache[$Record["ControlledNameId"]][] = $Record["VariantName"];
                    }
                }

                # for each entry in controlled name cache
                foreach ($this->ControlledNameCache as $ControlledNameId => $ControlledNameInfo)
                {
                    # if controlled name type matches field we are looking for
                    if ($ControlledNameInfo["FieldId"] == $Field->Id())
                    {
                        # if objects requested
                        if ($ReturnObject)
                        {
                            $ReturnValue[$ControlledNameId] =
                                    new ControlledName($ControlledNameId);
                        }
                        else
                        {
                            # if variant names requested
                            if ($IncludeVariants)
                            {
                                # add field to result
                                $ReturnValue[] = $ControlledNameInfo["Name"];

                                # add any variant names to result
                                if (isset($this->ControlledNameVariantCache[$ControlledNameId]))
                                {
                                    $ReturnValue = array_merge($ReturnValue, $this->ControlledNameVariantCache[$ControlledNameId]);
                                }
                            }
                            else
                            {
                                # add field with index to result
                                $ReturnValue[$ControlledNameId] = $ControlledNameInfo["Name"];
                            }
                        }
                    }
                }
                break;

            case MetadataSchema::MDFTYPE_USER:
                $User = new User($this->DB, (int)$this->DBFields[$DBFieldName]);
                if ($ReturnObject)
                {
                    $ReturnValue = $User;
                }
                else
                {
                    $ReturnValue = $User->Get("UserName");
                }
                break;

            case MetadataSchema::MDFTYPE_IMAGE:
                if ($this->DBFields[$DBFieldName] > 0)
                {
                    $ImageObject = new SPTImage($this->DBFields[$DBFieldName]);
                    if ($ReturnObject)
                    {
                        $ReturnValue = $ImageObject;
                    }
                    else
                    {
                        $ReturnValue = $ImageObject->AltText();
                    }
                }
                else
                {
                    $ReturnValue = NULL;
                }
                break;

            case MetadataSchema::MDFTYPE_FILE:
                # retrieve files using factory
                $Factory = new FileFactory($Field->Id());
                $ReturnValue = $Factory->GetFilesForResource(
                        $this->Id, $ReturnObject);
                break;

            default:
                # ERROR OUT
                exit("<br>SPT - ERROR: attempt to retrieve unknown resource field type (".$Field->Type().")<br>\n");
                break;
        }

        # return formatted value to caller
        return $ReturnValue;
    }
    /**
    * Old method for retrieving values, deprecated in favor of Get().
    * @see Get
    */
    function GetByField($FieldNameOrObject,
            $ReturnObject = FALSE, $IncludeVariants = FALSE)
    {  return $this->Get($FieldNameOrObject, $ReturnObject, $IncludeVariants);  }

    /**
    * Retrieve value using field ID.
    * @param FieldId ID of field.
    * @param ReturnObject For field types that can return multiple values,
    *   if TRUE, returns array of objects, else returns array of values.
    *   Defaults to FALSE.
    * @param IncludeVariants If TRUE, includes variants in return value.
    *   Only applicable for ControlledName fields.
    * @return Requested object(s) or value(s).  Returns empty array (for field
    *   types that allow multiple values) or NULL (for field types that do not
    *   allow multiple values) if no values found.
    * @see Get
    */
    function GetByFieldId($FieldId, $ReturnObject = FALSE, $IncludeVariants = FALSE)
    {
        $Field = $this->Schema->GetField($FieldId);
        return ($Field) ? $this->Get($Field, $ReturnObject, $IncludeVariants) : NULL;
    }

    /**
    * Retrieve all resource values as an array.
    * @param IncludeDisabledFields Include values for disabled fields.
    *       (OPTIONAL, defaults to FALSE)
    * @param ReturnObjects If TRUE, an object is returned for field types
    *       where appropriate, in the same fashion as Resource::Get()
    *       (OPTIONAL, defaults to TRUE)
    * @return Array of values with field names for array indices.  Qualifiers
    *       (where available) are returned with an index of the field name
    *       with " Qualifier" appended.
    * @see Get
    */
    function GetAsArray($IncludeDisabledFields = FALSE, $ReturnObjects = TRUE)
    {
        # retrieve field info
        $Fields = $this->Schema->GetFields();

        # for each field
        foreach ($Fields as $Field)
        {
            # if field is enabled or caller requested disabled fields
            if ($Field->Enabled() || $IncludeDisabledFields)
            {
                # retrieve info and add it to the array
                $FieldStrings[$Field->Name()] = $this->Get($Field, $ReturnObjects);

                # if field uses qualifiers
                if ($Field->UsesQualifiers())
                {
                    # get qualifier attributes and add to the array
                    $FieldStrings[$Field->Name()." Qualifier"] =
                            $this->GetQualifierByField($Field, $ReturnObjects);
                }
            }
        }

        # add in internal values
        $FieldStrings["ResourceId"] = $this->Id();
        $FieldStrings["CumulativeRating"] = $this->CumulativeRating();

        # return array to caller
        return $FieldStrings;
    }

    /**
    * Retrieve value using standard (mapped) field name.
    * @param MappedName Standard field name.
    * @param ReturnObject For field types that can return multiple values, if
    *       TRUE, returns array of objects, else returns array of values.
    *       Defaults to FALSE.
    * @param IncludeVariants If TRUE, includes variants in return value.  Only
    *       applicable for ControlledName fields.  Defaults to FALSE.
    * @return Requested object(s) or value(s), or NULL if no mapping found.
    *       Returns empty array (for field types that allow multiple values) or
    *       NULL (for field types that do not allow multiple values) if no
    *       values found.
    * @see Get
    */
    function GetMapped($MappedName, $ReturnObject = FALSE, $IncludeVariants = FALSE)
    {
        return $this->Schema->StdNameToFieldMapping($MappedName)
                ? $this->GetByFieldId($this->Schema->StdNameToFieldMapping($MappedName),
                        $ReturnObject, $IncludeVariants)
                : NULL;
    }

    /**
    * Retrieve qualifier by field name.
    * @param FieldName Full name of field.
    * @param ReturnObject If TRUE, return Qualifier objects, else return
    *       qualifier IDs.  Defaults to TRUE.
    * @return Array of qualifiers if field supports qualifiers, or NULL if
    *       field does not support qualifiers.
    */
    function GetQualifier($FieldName, $ReturnObject = TRUE)
    {
        $Field = $this->Schema->GetFieldByName($FieldName);
        return $this->GetQualifierByField($Field, $ReturnObject);
    }

    /**
    * Retrieve qualifier by field ID.
    * @param FieldId ID of field.
    * @param ReturnObject If TRUE, return Qualifier objects, else return
    *       qualifier IDs.  Defaults to TRUE.
    * @return Array of qualifiers if field supports qualifiers, or NULL
    *       if field does not support qualifiers.
    */
    function GetQualifierByFieldId($FieldId, $ReturnObject = TRUE)
    {
        $Field = $this->Schema->GetField($FieldId);
        return $this->GetQualifierByField($Field, $ReturnObject);
    }

    /**
    * Retrieve qualifier by Field object.
    * @param Field Field object.
    * @param ReturnObject If TRUE, return Qualifier objects, else return
    *       qualifier IDs.  Defaults to TRUE.
    * @return Array of qualifiers if field supports qualifiers, or NULL if
    *       field does not support qualifiers or field is invalid.
    */
    function GetQualifierByField($Field, $ReturnObject = TRUE)
    {
        # return NULL if field is invalid
        if ((get_class($Field) != "MetadataField")
                || ($Field->Status() != MetadataSchema::MDFSTAT_OK))
                {  return NULL;  }

        # assume no qualifiers if not otherwise determined
        $ReturnValue = NULL;

        # if field uses qualifiers
        if ($Field->UsesQualifiers())
        {
            # retrieve qualifiers based on field type
            switch ($Field->Type())
            {
                case MetadataSchema::MDFTYPE_TREE:
                case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
                case MetadataSchema::MDFTYPE_OPTION:
                    # retrieve list of items
                    $Items = $this->Get($Field);

                    # if field uses item-level qualifiers
                    if ($Field->HasItemLevelQualifiers())
                    {
                        # determine general item name in DB
                        $TableName = ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
                                ? "Classification" : "ControlledName";

                        # for each item
                        foreach ($Items as $ItemId => $ItemName)
                        {
                            # look up qualifier for item
                            $QualId = $this->DB->Query(
                                    "SELECT * FROM ".$TableName."s"
                                    ." WHERE ".$TableName."Id = ".$ItemId
                                    , "QualifierId");


                            if ($QualId > 0)
                            {
                                # if object was requested by caller
                                if ($ReturnObject)
                                {
                                    # load qualifier and add to return value array
                                    $ReturnValue[$ItemId] = new Qualifier($QualId);
                                }
                                else
                                {
                                    # add qualifier ID to return value array
                                    $ReturnValue[$ItemId] = $QualId;
                                }
                            }
                            else
                            {
                                # add NULL to return value array for this item
                                $ReturnValue[$ItemId] = NULL;
                            }
                        }
                    }
                    else
                    {
                        # for each item
                        foreach ($Items as $ItemId => $ItemName)
                        {
                            # if object was requested by caller
                            if ($ReturnObject)
                            {
                                # load default qualifier and add to return value array
                                $ReturnValue[$ItemId] = new Qualifier($Field->DefaultQualifier());
                            }
                            else
                            {
                                # add default qualifier ID to return value array
                                $ReturnValue[$ItemId] = $Field->DefaultQualifier();
                            }
                        }
                    }
                    break;

                default:
                    # if field uses item-level qualifiers
                    if ($Field->HasItemLevelQualifiers())
                    {
                        # if qualifier available
                        if ($this->DBFields[$Field->DBFieldName()."Qualifier"] > 0)
                        {
                            # if object was requested by caller
                            if ($ReturnObject)
                            {
                                # return qualifier for field
                                $ReturnValue = new Qualifier($this->DBFields[$Field->DBFieldName()."Qualifier"]);
                            }
                            else
                            {
                                # return qualifier ID for field
                                $ReturnValue = $this->DBFields[$Field->DBFieldName()."Qualifier"];
                            }
                        }
                    }
                    else
                    {
                        # if default qualifier available
                        if ($Field->DefaultQualifier() > 0)
                        {
                            # if object was requested by caller
                            if ($ReturnObject)
                            {
                                # return default qualifier
                                $ReturnValue = new Qualifier($Field->DefaultQualifier());
                            }
                            else
                            {
                                # return default qualifier ID
                                $ReturnValue = $Field->DefaultQualifier();
                            }
                        }
                    }
                    break;
            }
        }

        # return qualifier object or ID (or array of same) to caller
        return $ReturnValue;
    }


    # --- Generic Attribute Setting Methods ---------------------------------

    # set value using field name or field object
    function Set($FieldNameOrObject, $NewValue)
    {
        # load field object if needed
        $Field = is_object($FieldNameOrObject) ? $FieldNameOrObject
                : $this->Schema->GetFieldByName($FieldNameOrObject);

        # grab commonly-used values for local use
        $DB = $this->DB;
        $ResourceId = $this->Id;

        # grab database field name
        $DBFieldName = $Field->DBFieldName();

        # Flag to deterimine if we've actually changed anything.
        $UpdateModTime = FALSE;

        # store value in DB based on field type
        switch ($Field->Type())
        {
            case MetadataSchema::MDFTYPE_TEXT:
            case MetadataSchema::MDFTYPE_PARAGRAPH:
            case MetadataSchema::MDFTYPE_URL:
                if ($this->DBFields[$DBFieldName] != $NewValue)
                {
                    # save value directly to DB
                    $DB->Query("UPDATE Resources SET `"
                               .$DBFieldName."` = '".addslashes($NewValue)."' "
                               ."WHERE ResourceId = ".$ResourceId);

                    # save value locally
                    $this->DBFields[$DBFieldName] = $NewValue;
                    $UpdateModTime=TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_NUMBER:
                if ( $this->DBFields[$DBFieldName] != $NewValue )
                {
                    # save value directly to DB
                    if (is_null($NewValue))
                    {
                        $DB->Query("UPDATE Resources SET `"
                                   .$DBFieldName."` = NULL"
                                   ." WHERE ResourceId = ".$ResourceId);
                    }
                    else
                    {
                        $DB->Query("UPDATE Resources SET `"
                                   .$DBFieldName."` = ".intval($NewValue)
                                   ." WHERE ResourceId = ".$ResourceId);
                    }

                    # save value locally
                    $this->DBFields[$DBFieldName] = $NewValue;
                    $UpdateModTime = TRUE;
                }
                break;


            case MetadataSchema::MDFTYPE_POINT:
                if ($this->DBFields[$DBFieldName."X"] != $NewValue["X"] ||
                    $this->DBFields[$DBFieldName."Y"] != $NewValue["Y"] )
                {
                    if (is_null($NewValue))
                    {
                        $DB->Query("UPDATE Resources SET "
                                   ."`".$DBFieldName."X` = NULL, "
                                   ."`".$DBFieldName."Y` = NULL "
                                   ."WHERE ResourceId = ".$ResourceId);
                        $this->DBFields[$DBFieldName."X"] = NULL;
                        $this->DBFields[$DBFieldName."Y"] = NULL;
                    }
                    else
                    {
                        $DB->Query("UPDATE Resources SET "
                                   ."`".$DBFieldName."X` = ".(strlen($NewValue["X"])
                                                              ? "'".$NewValue["X"]."'" : "NULL").", "
                                   ."`".$DBFieldName."Y` = ".(strlen($NewValue["Y"])
                                                              ? "'".$NewValue["Y"]."'" : "NULL")
                                   ." WHERE ResourceId = ".$ResourceId);

                        $Digits = $Field->PointDecimalDigits();

                        $this->DBFields[$DBFieldName."X"] =
                            strlen($NewValue["X"]) ? round($NewValue["X"], $Digits) : NULL;
                        $this->DBFields[$DBFieldName."Y"] =
                            strlen($NewValue["Y"]) ? round($NewValue["Y"], $Digits) : NULL;
                    }
                    $UpdateModTime = TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_FLAG:
                if ($this->DBFields[$DBFieldName] != $NewValue)
                {
                    # save value directly to DB
                    if (is_null($NewValue))
                    {
                        $DB->Query("UPDATE Resources SET `"
                                   .$DBFieldName."` = NULL"
                                   ." WHERE ResourceId = ".$ResourceId);
                    }
                    else
                    {
                        $DB->Query("UPDATE Resources SET `"
                                   .$DBFieldName."` = ".$NewValue
                                   ." WHERE ResourceId = ".$ResourceId);
                    }

                    $this->DBFields[$DBFieldName] = $NewValue;

                    # recalculate counts for any associated classifications if necessary
                    if ($DBFieldName == "ReleaseFlag")
                    {
                        $DB->Query("SELECT ClassificationId FROM ResourceClassInts"
                                ." WHERE ResourceId = ".$ResourceId);
                        while ($ClassId = $DB->FetchField("ClassificationId"))
                        {
                            $Class = new Classification($ClassId);
                            $Class->RecalcResourceCount();
                        }
                    }
                    $UpdateModTime = TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_USER:
                # if value passed in was object
                if (is_object($NewValue))
                {
                    # retrieve user ID from object
                    $UserId = $NewValue->Get("UserId");
                }
                # else if value passed in was user name
                elseif (is_string($NewValue))
                {
                    # create user object and retrieve user ID from there
                    $User = new User($this->DB, $NewValue);
                    $UserId = $User->Get("UserId");
                }
                else
                {
                    # assume value is user ID and use value directly
                    $UserId = $NewValue;
                }

                if ($this->DBFields[$DBFieldName] != $UserId)
                {
                    # save value directly to DB
                    $DB->Query("UPDATE Resources SET `"
                               .$DBFieldName."` = '".$UserId."' "
                               ."WHERE ResourceId = ".$ResourceId);

                    # save value locally
                    $this->DBFields[$DBFieldName] = $UserId;
                    $UpdateModTime = TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_DATE:
                # if we were given a date object
                if (is_object($NewValue))
                {
                    # use supplied date object
                    $Date = $NewValue;
                }
                else
                {
                    # create date object
                    $Date = new Date($NewValue);
                }

                $OldDate = new Date(
                    $this->DBFields[$DBFieldName."Begin"],
                    $this->DBFields[$DBFieldName."End"]);

                if ($OldDate->BeginDate() != $Date->BeginDate() ||
                    $OldDate->EndDate()   != $Date->EndDate()   ||
                    $OldDate->Precision() != $Date->Precision() )
                {
                    # extract values from date object and store in DB
                    $BeginDate = "'".$Date->BeginDate()."'";
                    if (strlen($BeginDate) < 3) {  $BeginDate = "NULL";  }
                    $EndDate = "'".$Date->EndDate()."'";
                    if (strlen($EndDate) < 3) {  $EndDate = "NULL";  }

                    $DB->Query("UPDATE Resources SET "
                               .$DBFieldName."Begin = ".$BeginDate.", "
                               .$DBFieldName."End = ".$EndDate.", "
                               .$DBFieldName."Precision = '".$Date->Precision()."' "
                               ."WHERE ResourceId = ".$ResourceId);

                    # save values locally
                    $this->DBFields[$DBFieldName."Begin"] = $Date->BeginDate();
                    $this->DBFields[$DBFieldName."End"] = $Date->EndDate();
                    $this->DBFields[$DBFieldName."Precision"] = $Date->Precision();
                    $UpdateModTime=TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_TIMESTAMP:
                if (is_null($NewValue))
                {
                    $DateValue = $NewValue;

                    if (!is_null($this->DBFields[$DBFieldName]))
                    {
                        # save value directly to DB
                        $DB->Query("UPDATE Resources SET "
                                   ."`".$DBFieldName."` = NULL "
                                   ."WHERE ResourceId = ".$ResourceId);
                        $UpdateModTime = TRUE;
                    }
                }
                else
                {
                    # assume value is date and use directly
                    $DateValue = date("Y-m-d H:i:s", strtotime($NewValue));

                    if ($this->DBFields[$DBFieldName] != $DateValue)
                    {
                        # save value directly to DB
                        $DB->Query("UPDATE Resources SET "
                                   ."`".$DBFieldName."` = '".addslashes($DateValue)."' "
                                   ."WHERE ResourceId = ".$ResourceId);
                        $UpdateModTime=TRUE;
                    }
                }

                # save value locally
                $this->DBFields[$DBFieldName] = $DateValue;
                break;

            case MetadataSchema::MDFTYPE_TREE:
                $OldValue = $this->Get($Field);

                # if incoming value is array
                if (is_array($NewValue))
                {
                    if ($OldValue != $NewValue)
                    {
                        # for each element of array
                        foreach ($NewValue as
                                 $ClassificationId => $ClassificationName)
                        {
                            $Class = new Classification($ClassificationId);
                            if ($Class->Status() == Classification::CLASSSTAT_OK)
                            {
                                # associate with resource if not already associated
                                $this->AddAssociation("ResourceClassInts",
                                                      "ClassificationId",
                                                      $ClassificationId);
                                $Class->RecalcResourceCount();
                            }
                        }
                        $UpdateModTime=TRUE;
                    }
                }
                else
                {
                    # associate with resource if not already associated
                    if (is_object($NewValue))
                    {
                        $Class = $NewValue;
                        $NewValue = $Class->Id();
                    }
                    else
                    {
                        $Class = new Classification($NewValue);
                    }

                    if (!array_key_exists($Class->Id(), $OldValue))
                    {

                        $this->AddAssociation("ResourceClassInts",
                                              "ClassificationId",
                                              $NewValue);
                        $Class->RecalcResourceCount();
                        $UpdateModTime=TRUE;
                    }
                }

                # clear our classification cache
                if ($UpdateModTime)
                {
                    unset($this->ClassificationCache);
                }
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $OldValue = $this->Get($Field);

                # Clear other values if this field expects unique options
                if ($Field->AllowMultiple() === FALSE)
                {
                    # If we're fed an array for a unique option,
                    # just use the last element of the array
                    if (is_array($NewValue)){
                        $NewValue = array_pop($NewValue);
                    }

                    if (!array_key_exists($NewValue, $OldValue))
                    {

                        $this->RemoveAllAssociations("ResourceNameInts",
                                                     "ControlledNameId",
                                                     $Field );
                        $UpdateModTime=TRUE;
                    }
                }

                # if incoming value is array
                if (is_array($NewValue) && ($Field->AllowMultiple() !== FALSE) )
                {
                    if ($OldValue != $NewValue)
                    {
                        # for each element of array
                        foreach ($NewValue as $ControlledNameId => $ControlledName)
                        {
                            # associate with resource if not already associated
                            $this->AddAssociation("ResourceNameInts",
                                                  "ControlledNameId",
                                                  $ControlledNameId);
                        }
                        $UpdateModTime=TRUE;
                    }
                }
                else
                {
                    # associate with resource if not already associated
                    if (is_object($NewValue)) {  $NewValue = $NewValue->Id();  }
                    if (!array_key_exists($NewValue, $OldValue))
                    {
                        $this->AddAssociation("ResourceNameInts",
                                              "ControlledNameId",
                                              $NewValue);
                        $UpdateModTime=TRUE;
                    }
                }

                if ($UpdateModTime)
                {
                    # clear our controlled name cache
                    unset($this->ControlledNameCache);
                    unset($this->ControlledNameVariantCache);
                }

                break;

            case MetadataSchema::MDFTYPE_IMAGE:
                # if we were given an image object
                if (is_object($NewValue))
                {
                    # grab ID from object
                    $ImageId = $NewValue->Id();
                }
                else
                {
                    # assume incoming value is ID
                    $ImageId = $NewValue;
                }

                if ($this->DBFields[$DBFieldName] != $ImageId)
                {
                    # store new image object ID in database
                    $DB->Query("UPDATE Resources SET `"
                               .$DBFieldName."` = '".$ImageId."'"
                               ." WHERE ResourceId = ".$ResourceId);
                    $UpdateModTime = TRUE;
                    # save value locally
                    $this->DBFields[$DBFieldName] = $ImageId;
                }
                break;

            case MetadataSchema::MDFTYPE_FILE:
                # convert incoming value to array if necessary
                if (!is_array($NewValue)) {  $NewValue = array($NewValue);  }

                # for each incoming file
                $Factory = new FileFactory($Field->Id());
                foreach ($NewValue as $File)
                {
                    # make copy of file
                    $NewFile = $Factory->Copy($File);

                    # associate copy with this resource and field
                    $NewFile->ResourceId($this->Id);
                    $NewFile->FieldId($Field->Id());
                }
                # Since we make a fresh copy of the File whenever Set is called,
                # we'll always update the modification time for this field.
                $UpdateModTime = TRUE;
                break;

            default:
                # ERROR OUT
                exit("<br>SPT - ERROR: attempt to set unknown resource field type<br>\n");
                break;
        }

        if ($UpdateModTime && !$this->IsTempResource())
        {
            # update modification timestamps
            global $G_User;
            $UserId = $G_User->IsLoggedIn() ? $G_User->Get("UserId") : -1;
            $DB->Query("DELETE FROM ResourceFieldTimestamps "
                       ."WHERE ResourceId=".$this->Id." AND "
                       ."FieldId=".$Field->Id() );
            $DB->Query("INSERT INTO ResourceFieldTimestamps "
                       ."(ResourceId,FieldId,ModifiedBy,Timestamp) VALUES ("
                       .$this->Id.",".$Field->Id().","
                       .$UserId.",NOW())");
        }
    }
    # (for backward compatibility)
    function SetByField($Field, $NewValue) {  return $this->Set($Field, $NewValue);  }

    # set value by field ID
    function SetByFieldId($FieldId, $NewValue)
    {
        $Field = $this->Schema->GetField($FieldId);
        return $this->Set($Field, $NewValue);
    }

    # set qualifier by field name
    function SetQualifier($FieldName, $NewValue)
    {
        $Field = $this->Schema->GetFieldByName($FieldName);
        return $this->SetQualifierByField($Field, $NewValue);
    }

    # set qualifier by field ID
    function SetQualifierByFieldId($FieldId, $NewValue)
    {
        $Field = $this->Schema->GetField($FieldId);
        return $this->SetQualifierByField($Field, $NewValue);
    }

    # set qualifier using field object
    function SetQualifierByField($Field, $NewValue)
    {
        # if field uses qualifiers and uses item-level qualifiers
        if ($Field->UsesQualifiers() && $Field->HasItemLevelQualifiers())
        {
            # if qualifier object passed in
            if (is_object($NewValue))
            {
                # grab qualifier ID from object
                $QualifierId = $NewValue->Id();
            }
            else
            {
                # assume value passed in is qualifier ID
                $QualifierId = $NewValue;
            }

            # update qualifier value in database
            $DBFieldName = $Field->DBFieldName();
            $this->DB->Query("UPDATE Resources SET "
                     .$DBFieldName."Qualifier = '".$QualifierId."' "
                     ."WHERE ResourceId = ".$this->Id);

            # update local qualifier value
            $this->DBFields[$DBFieldName."Qualifier"] = $QualifierId;
        }
    }

    # clear value by field name
    function Clear($FieldName, $ValueToClear = NULL)
    {
        $Field = $this->Schema->GetFieldByName($FieldName);
        return $this->ClearByField($Field, $ValueToClear);
    }

    # clear value by field ID
    function ClearByFieldId($FieldId, $ValueToClear = NULL)
    {
        $Field = $this->Schema->GetField($FieldId);
        return $this->ClearByField($Field, $ValueToClear);
    }

    # clear value using field object
    function ClearByField($Field, $ValueToClear = NULL)
    {
        # grab commonly-used values for local use
        $DB = $this->DB;
        $ResourceId = $this->Id;

        # grab database field name
        $DBFieldName = $Field->DBFieldName();

        $UpdateModTime=FALSE;

        # store value in DB based on field type
        switch ($Field->Type())
        {
            case MetadataSchema::MDFTYPE_TEXT:
            case MetadataSchema::MDFTYPE_PARAGRAPH:
            case MetadataSchema::MDFTYPE_NUMBER:
            case MetadataSchema::MDFTYPE_FLAG:
            case MetadataSchema::MDFTYPE_USER:
            case MetadataSchema::MDFTYPE_TIMESTAMP:
            case MetadataSchema::MDFTYPE_URL:
                if (strlen($this->DBFields[$DBFieldName])>0)
                {
                    # clear value in DB
                    $DB->Query("UPDATE Resources SET `"
                               .$DBFieldName."` = '' "
                               ."WHERE ResourceId = ".$ResourceId);

                    # clear value locally
                    $this->DBFields[$DBFieldName] = NULL;
                    $UpdateModTime=TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_POINT:
                if (!is_null($this->DBFields[$DBFieldName."X"]) ||
                    !is_null($this->DBFields[$DBFieldName."Y"]) )
                {
                    # Clear DB Values
                    $DB->Query("UPDATE Resources SET "
                               ."`".$DBFieldName."X` = NULL ,"
                               ."`".$DBFieldName."Y` = NULL "
                               ."WHERE ResourceId = ".$ResourceId);

                    # Clear local values
                    $this->DBFields[$DBFieldName."X"] = NULL;
                    $this->DBFields[$DBFieldName."Y"] = NULL;
                    $UpdateModTime=TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_DATE:
                if (!is_null($this->DBFields[$DBFieldName."Begin"]) ||
                    !is_null($this->DBFields[$DBFieldName."End"])   ||
                    !is_null($this->DBFields[$DBFieldName."Precision"]))
                {
                    # clear date object values in DB
                    $DB->Query("UPDATE Resources SET "
                               .$DBFieldName."Begin = '', "
                               .$DBFieldName."End = '', "
                               .$DBFieldName."Precision = '' "
                               ."WHERE ResourceId = ".$ResourceId);

                    # clear value locally
                    $this->DBFields[$DBFieldName."Begin"] = NULL;
                    $this->DBFields[$DBFieldName."End"] = NULL;
                    $this->DBFields[$DBFieldName."Precision"] = NULL;
                    $UpdateModTime=TRUE;
                }
                break;

            case MetadataSchema::MDFTYPE_TREE:
                $OldValue = $this->Get($Field);

                # if value to clear supplied
                if ($ValueToClear !== NULL)
                {
                    # if supplied value is array
                    if (is_array($ValueToClear))
                    {
                        # for each element of array
                        foreach ($ValueToClear as $ClassificationId => $Dummy)
                        {
                            if (array_key_exists($ClassificationId, $OldValue))
                            {
                                # remove association with resource (if any)
                                $this->RemoveAssociation("ResourceClassInts",
                                                         "ClassificationId",
                                                         $ClassificationId);
                                $Class = new Classification($ClassificationId);
                                $Class->RecalcResourceCount();
                                $UpdateModTime=TRUE;
                            }
                        }
                    }
                    else
                    {
                        if (array_key_exists($ValueToClear, $OldValue))
                        {
                            # remove association with resource (if any)
                            $this->RemoveAssociation("ResourceClassInts",
                                                     "ClassificationId",
                                                     $ValueToClear);
                            $Class = new Classification($ValueToClear);
                            $Class->RecalcResourceCount();
                            $UpdateModTime=TRUE;
                        }
                    }
                }
                else
                {
                    if (count($OldValue)>0)
                    {
                        # remove all associations for resource and field
                        $this->RemoveAllAssociations("ResourceClassInts", "ClassificationId", $Field);

                        # recompute resource count
                        $Values = $this->Get($Field);
                        foreach ($Values as $ClassificationId => $Dummy)
                        {
                            $Class = new Classification($ClassificationId);
                            $Class->RecalcResourceCount();
                        }
                        $UpdateModTime=TRUE;
                    }
                }

                # clear our classification cache
                if ($UpdateModTime)
                {
                    unset($this->ClassificationCache);
                }
                break;

            case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
            case MetadataSchema::MDFTYPE_OPTION:
                $OldValue = $this->Get($Field);
                # if value to clear supplied
                if ($ValueToClear !== NULL)
                {
                    # if incoming value is array
                    if (is_array($ValueToClear))
                    {
                        # for each element of array
                        foreach ($ValueToClear as $ControlledNameId =>
                            $ControlledName)
                        {
                            if (array_key_exists($ControlledNameId, $OldValue))
                            {
                                # remove association with resource (if any)
                                $this->RemoveAssociation("ResourceNameInts",
                                                         "ControlledNameId",
                                                         $ControlledNameId);
                                $UpdateModTime=TRUE;
                            }
                        }
                    }
                    else
                    {
                        if (array_key_exists($ValueToClear, $OldValue))
                        {
                            # remove association with resource (if any)
                            $this->RemoveAssociation("ResourceNameInts",
                                                     "ControlledNameId",
                                                     $ValueToClear);
                            $UpdateModTime=TRUE;
                        }
                    }
                }
                else
                {
                    if (count($OldValue)>0)
                    {
                        # remove all associations for resource and field
                        $this->RemoveAllAssociations("ResourceNameInts", "ControlledNameId", $Field);
                        $UpdateModTime=TRUE;
                    }
                }

                if ($UpdateModTime)
                {
                    # clear our controlled name cache
                    unset($this->ControlledNameCache);
                    unset($this->ControlledNameVariantCache);
                }
                break;

            case MetadataSchema::MDFTYPE_IMAGE:
                # delete image if no other resources are using it
                $ImageId = $DB->Query("SELECT `".$DBFieldName
                                      ."` FROM Resources WHERE ResourceId = ".$ResourceId,
                                      $DBFieldName);
                if ($ImageId > 0)
                {
                    $ImageCount = $DB->Query("SELECT COUNT(*) AS ImageCount FROM Resources"
                                             ." WHERE `".$DBFieldName."` = ".$ImageId,
                                             "ImageCount");
                    if ($ImageCount < 2)
                    {
                        $Image = new SPTImage($ImageId);
                        $Image->Delete();
                    }
                    $UpdateModTime=TRUE;
                }

                # clear stored ID
                $DB->Query("UPDATE Resources SET `"
                           .$DBFieldName."` = '' "
                           ."WHERE ResourceId = ".$ResourceId);

                # clear value locally
                $this->DBFields[$DBFieldName] = NULL;
                break;

            case MetadataSchema::MDFTYPE_FILE:
                # get array of Files associated with this resource
                $Files = $this->Get($Field, TRUE);

                if (count($Files)>0){
                    # for each File
                    foreach ($Files as $File)
                    {
                        # delete file
                        $File->Delete();
                    }
                    $UpdateModTime=TRUE;
                }
                break;

            default:
                # ERROR OUT
                exit("<br>SPT - ERROR: attempt to clear unknown resource field type<br>\n");
                break;
        }

        if ($UpdateModTime && !$this->IsTempResource())
        {
            # update modification timestamps
            global $G_User;
            $UserId = $G_User->IsLoggedIn() ? $G_User->Get("UserId") : -1;
            $DB->Query("DELETE FROM ResourceFieldTimestamps "
                       ."WHERE ResourceId=".$this->Id." AND "
                       ."FieldId=".$Field->Id() );
            $DB->Query("INSERT INTO ResourceFieldTimestamps "
                       ."(ResourceId,FieldId,ModifiedBy,Timestamp) VALUES ("
                       .$this->Id.",".$Field->Id().","
                       .$UserId.",NOW())");
        }
    }


    # --- Field-Specific or Type-Specific Attribute Retrieval Methods -------

    # return 2D array of classifications associated with resource
    # (first index is classification (field) name, second index is classification ID)
    function Classifications()
    {
        $DB = $this->DB;

        # start with empty array
        $Names = array();

        # for each controlled name
        $DB->Query("SELECT ClassificationName, MetadataFields.FieldName, "
                ."ResourceClassInts.ClassificationId FROM ResourceClassInts, "
                ."Classifications, MetadataFields "
                ."WHERE ResourceClassInts.ResourceId = ".$this->Id." "
                ."AND ResourceClassInts.ClassificationId = Classifications.ClassificationId "
                ."AND Classifications.FieldId = MetadataFields.FieldId ");
        while ($Record = $DB->FetchRow())
        {
            # add name to array
            $Names[$Record["FieldName"]][$Record["ClassificationId"]] =
                    $Record["ClassificationName"];
        }

        # return array to caller
        return $Names;
    }


    # --- Ratings Methods ---------------------------------------------------

    # return cumulative rating  (range is usually 0-100)
    function CumulativeRating() {  return $this->CumulativeRating;  }

    # return cumulative rating scaled to 1/10th  (range is usually 0-10)
    function ScaledCumulativeRating()
    {
        if ($this->CumulativeRating == NULL)
        {
            return NULL;
        }
        else
        {
            return intval(($this->CumulativeRating + 5) / 10);
        }
    }

    # return current number of ratings for resource
    function NumberOfRatings()
    {
        # if number of ratings not already set
        if (!isset($this->NumberOfRatings))
        {
            # obtain number of ratings
            $this->NumberOfRatings =
                    $this->DB->Query("SELECT Count(*) AS NumberOfRatings "
                            ."FROM ResourceRatings "
                            ."WHERE ResourceId = ".$this->Id,
                    "NumberOfRatings"
                    );

            # recalculate cumulative rating if it looks erroneous
            if (($this->NumberOfRatings > 0) && !$this->CumulativeRating())
            {
                $this->UpdateCumulativeRating();
            }
        }

        # return number of ratings to caller
        return $this->NumberOfRatings;
    }

    # update individual rating for resource
    function Rating($NewRating = NULL, $UserId = NULL)
    {
        $DB = $this->DB;

        # if user ID not supplied
        if ($UserId == NULL)
        {
            # if user is logged in
            global $User;
            if ($User->IsLoggedIn())
            {
                # use ID of current user
                $UserId = $User->Get("UserId");
            }
            else
            {
                # return NULL to caller
                return NULL;
            }
        }

        # sanitize $NewRating
        if (!is_null($NewRating))
        {
            $NewRating = intval($NewRating);
        }

        # if there is a rating for resource and user
        $DB->Query("SELECT Rating FROM ResourceRatings "
                ."WHERE UserId = ${UserId} AND ResourceId = ".$this->Id);
        if ($Record = $DB->FetchRow())
        {
            # if new rating was supplied
            if ($NewRating != NULL)
            {
                # update existing rating
                $DB->Query("UPDATE ResourceRatings "
                        ."SET Rating = ${NewRating}, DateRated = NOW() "
                        ."WHERE UserId = ${UserId} AND ResourceId = ".$this->Id);

                # update cumulative rating value
                $this->UpdateCumulativeRating();

                # return value is new rating
                $Rating = $NewRating;
            }
            else
            {
                # get rating value to return to caller
                $Rating = $Record["Rating"];
            }
        }
        else
        {
            # if new rating was supplied
            if ($NewRating != NULL)
            {
                # add new rating
                $DB->Query("INSERT INTO ResourceRatings "
                        ."(ResourceId, UserId, DateRated, Rating) "
                        ."VALUES ("
                                .$this->Id.", "
                                ."${UserId}, "
                                ."NOW(), "
                                ."${NewRating})");

                # update cumulative rating value
                $this->UpdateCumulativeRating();

                # return value is new rating
                $Rating = $NewRating;
            }
            else
            {
                # return value is NULL
                $Rating = NULL;
            }
        }

        # return rating value to caller
        return $Rating;
    }


    # --- Resource Comment Methods ------------------------------------------

    # return comments as array of Message objects
    function Comments()
    {
        # read in comments if not already loaded
        if (!isset($this->Comments))
        {
            $this->DB->Query("SELECT MessageId FROM Messages "
                    ."WHERE ParentId = ".$this->Id
                    ." AND ParentType = 2 "
                    ."ORDER BY DatePosted DESC");
            while ($MessageId = $this->DB->FetchField("MessageId"))
            {
                $this->Comments[] = new Message($MessageId);
            }
        }

        # return array of comments to caller
        return $this->Comments;
    }

    # return current number of comments
    function NumberOfComments()
    {
        # obtain number of comments if not already set
        if (!isset($this->NumberOfComments))
        {
            $this->NumberOfComments =
                    $this->DB->Query("SELECT Count(*) AS NumberOfComments "
                            ."FROM Messages "
                            ."WHERE ParentId = ".$this->Id
                            ." AND ParentType = 2",
                    "NumberOfComments"
                    );
        }

        # return number of comments to caller
        return $this->NumberOfComments;
    }


    # --- Permission Methods -------------------------------------------------

    # return whether user can edit this resource
    function UserCanEdit($User)
    {
        return ($User->HasPriv(PRIV_RESOURCEADMIN)
            || $User->HasPriv(PRIV_RELEASEADMIN)
            || ($User->HasPriv(PRIV_MYRESOURCEADMIN)
                    && ($User->Id() == $this->DBFields["AddedById"]))
        );
    }

    # report whether user can view or edit the specified field
    function UserCanViewField($User, $FieldOrFieldName)
    {
        $Schema = new MetadataSchema();

        # get field (if not supplied)
        if (is_object($FieldOrFieldName)
                && ($FieldOrFieldName instanceof MetadataField))
        {
            $Field = $FieldOrFieldName;
        }
        elseif (strlen(trim($FieldOrFieldName)))
        {
            if ($Schema->FieldExists($FieldOrFieldName))
            {
                $Field = $Schema->GetFieldByName($FieldOrFieldName);
            }
        }

        # the field must be valid and enabled to be viewable
        if (!isset($Field) || !$Field->Enabled())
        {
            return FALSE;
        }

        # if the user can edit the field, they can also view it implicitly
        if ($this->UserCanEditField($User, $Field))
        {
            return TRUE;
        }

        # an "user is value of field" constraint isn't set, so only worry about
        # the viewing privilege
        if ($Field->ViewingUserIsValue() == MetadataField::USERISVALUE_UNSET)
        {
            # no viewing privilege is set, so anyone can view the field
            if ($Field->ViewingPrivilege() == 0)
            {
                return TRUE;
            }

            # a viewing privilege is set, so check if the user has it
            else
            {
                return $User->HasPriv($Field->ViewingPrivilege());
            }
        }

        # an "user is value of field" constraint is set
        else
        {
            $ViewingUserIsValue = $Field->ViewingUserIsValue();
            $ViewingUserValue = $Field->ViewingUserValue();
            $ViewingUserField = $Schema->GetField($ViewingUserValue);
            $ViewingUserFieldValue = $this->Get($ViewingUserField);

            # no viewing privilege is set, so only check the constraint
            if ($Field->ViewingPrivilege() == 0)
            {
                # just check if the user is the value of the field
                return $ViewingUserFieldValue == $User->Name();
            }

            # a viewing privilege is set, so check the privilege and constraint
            else
            {
                # OR user is value of field
                if ($ViewingUserIsValue == MetadataField::USERISVALUE_OR)
                {
                    # user is value of the field, so short circuit
                    if ($ViewingUserFieldValue == $User->Name())
                    {
                        return TRUE;
                    }
                }

                # AND user is value of field constraint, but user isn't the
                # value, so short circuit
                else if ($ViewingUserFieldValue != $User->Name())
                {
                    return FALSE;
                }

                # finally, if the logic hasn't been short circuited, check if
                # the user has the viewing privilege
                return $User->HasPriv($Field->ViewingPrivilege());
            }
        }
    }

    # report whether user can author the specified field
    function UserCanAuthorField($User, $FieldOrFieldName)
    {
        $Schema = new MetadataSchema();

        # get field (if not supplied)
        if (is_object($FieldOrFieldName)
                && ($FieldOrFieldName instanceof MetadataField))
        {
            $Field = $FieldOrFieldName;
        }
        elseif (strlen(trim($FieldOrFieldName)))
        {
            if ($Schema->FieldExists($FieldOrFieldName))
            {
                $Field = $Schema->GetFieldByName($FieldOrFieldName);
            }
        }

        # the field must be valid and enabled to be viewable
        if (!isset($Field) || !$Field->Enabled())
        {
            return FALSE;
        }

        # the user would have had to add the resource to be its author
        if ($User->Name() != $this->Get("Added By Id"))
        {
            return FALSE;
        }

        # an "user is value of field" constraint isn't set, so only worry about
        # the authoring privilege
        if ($Field->AuthoringUserIsValue() == MetadataField::USERISVALUE_UNSET)
        {
            # no authoring privilege is set, so anyone can author the field
            if ($Field->AuthoringPrivilege() == 0)
            {
                return TRUE;
            }

            # an authoring privilege is set, so check if the user has it
            else
            {
                return $User->HasPriv($Field->AuthoringPrivilege());
            }
        }

        # an "user is value of field" constraint is set
        else
        {
            $AuthoringUserIsValue = $Field->AuthoringUserIsValue();
            $AuthoringUserValue = $Field->AuthoringUserValue();
            $AuthoringUserField = $Schema->GetField($AuthoringUserValue);
            $AuthoringUserFieldValue = $this->Get($AuthoringUserField);

            # no authoring privilege is set, so only check the constraint
            if ($Field->AuthoringPrivilege() == 0)
            {
                # just check if the user is the value of the field
                return $AuthoringUserFieldValue == $User->Name();
            }

            # an authoring privilege is set, so check the privilege and
            # constraint
            else
            {
                # OR user is value of field
                if ($AuthoringUserIsValue == MetadataField::USERISVALUE_OR)
                {
                    # user is value of the field, so short circuit
                    if ($AuthoringUserFieldValue == $User->Name())
                    {
                        return TRUE;
                    }
                }

                # AND user is value of field constraint, but user isn't the
                # value, so short circuit
                else if ($AuthoringUserFieldValue != $User->Name())
                {
                    return FALSE;
                }

                # finally, if the logic hasn't been short circuited, check if
                # the user has the authoring privilege
                return $User->HasPriv($Field->AuthoringPrivilege());
            }
        }
    }

    # report whether user can author or edit the specified field
    function UserCanEditField($User, $FieldOrFieldName)
    {
        $Schema = new MetadataSchema();

        # fields hard-coded to be not allow edits
        $UneditableFields = array(
            "Cumulative Rating",
            "Date Of Record Creation",
            "Date Of Record Release",
            "Date Last Modified",
            "Last Modified By Id");

        # get field (if not supplied)
        if (is_object($FieldOrFieldName)
                && ($FieldOrFieldName instanceof MetadataField))
        {
            $Field = $FieldOrFieldName;
        }
        elseif (strlen(trim($FieldOrFieldName)))
        {
            if ($Schema->FieldExists($FieldOrFieldName))
            {
                $Field = $Schema->GetFieldByName($FieldOrFieldName);
            }
        }

        # the field must be valid and enabled to be editable
        if (!isset($Field) || !$Field->Enabled())
        {
            return FALSE;
        }

        # if field name does not appear on "no edit" list
        if (in_array($Field->Name(), $UneditableFields))
        {
            return FALSE;
        }

        # if the user can author the field, they can also edit it implicitly
        if ($this->UserCanAuthorField($User, $Field))
        {
            return TRUE;
        }

        # an "user is value of field" constraint isn't set, so only worry about
        # the editing privilege
        if ($Field->EditingUserIsValue() == MetadataField::USERISVALUE_UNSET)
        {
            # no editing privilege is set, so anyone can author the field
            if ($Field->EditingPrivilege() == 0)
            {
                return TRUE;
            }

            # an editing privilege is set, so check if the user has it
            else
            {
                return $User->HasPriv($Field->EditingPrivilege());
            }
        }

        # an "user is value of field" constraint is set
        else
        {
            $EditingUserIsValue = $Field->EditingUserIsValue();
            $EditingUserValue = $Field->EditingUserValue();
            $EditingUserField = $Schema->GetField($EditingUserValue);
            $EditingUserFieldValue = $this->Get($EditingUserField);

            # no editing privilege is set, so only check the constraint
            if ($Field->EditingPrivilege() == 0)
            {
                # just check if the user is the value of the field
                return $EditingUserFieldValue == $User->Name();
            }

            # an editing privilege is set, so check the privilege and
            # constraint
            else
            {
                # OR user is value of field
                if ($EditingUserIsValue == MetadataField::USERISVALUE_OR)
                {
                    # user is value of the field, so short circuit
                    if ($EditingUserFieldValue == $User->Name())
                    {
                        return TRUE;
                    }
                }

                # AND user is value of field constraint, but user isn't the
                # value, so short circuit
                else if ($EditingUserFieldValue != $User->Name())
                {
                    return FALSE;
                }

                # finally, if the logic hasn't been short circuited, check if
                # the user has the editing privilege
                return $User->HasPriv($Field->EditingPrivilege());
            }
        }
    }

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

    private $DB;
    private $Schema;
    private $DBFields;
    private $Id;
    private $NumberOfRatings;
    private $CumulativeRating;
    private $NumberOfComments;
    private $Comments;
    private $LastStatus;
    private $ControlledNameCache;
    private $ControlledNameVariantCache;
    private $ClassificationCache;

    # recalculate and save cumulative rating value for resource
    private function UpdateCumulativeRating()
    {
        # grab totals from DB
        $this->DB->Query("SELECT COUNT(Rating) AS Count, "
                ."SUM(Rating) AS Total FROM ResourceRatings "
                ."WHERE ResourceId = ".$this->Id);
        $Record = $this->DB->FetchRow();

        # calculate new cumulative rating
        $this->CumulativeRating = round($Record["Total"] / $Record["Count"]);

        # save new cumulative rating in DB
        $this->DB->Query("UPDATE Resources "
                ."SET CumulativeRating = ".$this->CumulativeRating." "
                ."WHERE ResourceId = ".$this->Id);
    }

    # add intersection if not already present
    private function AddAssociation($TableName, $TargetFieldName, $TargetValue)
    {
        # if target not already associated with resource
        if ($this->DB->Query("SELECT COUNT(*) AS RecordCount FROM ".$TableName
                   ." WHERE ResourceId = ".$this->Id
                   ." AND ".$TargetFieldName." = '".$TargetValue."'",
                   "RecordCount") == 0)
        {
            # associate target with resource
            $this->DB->Query("INSERT INTO ".$TableName." SET"
                       ." ResourceId = ".$this->Id
                       .", ".$TargetFieldName." = '".$TargetValue."'");
        }
    }

    # remove intersections (if any)
    private function RemoveAssociation($TableName, $TargetFieldName, $TargetValue)
    {
        # remove any intersections with target ID from DB
        $this->DB->Query("DELETE FROM ".$TableName
                         ." WHERE ResourceId = ".$this->Id
                         ." AND ".$TargetFieldName." = '".$TargetValue."'");
    }

    # remove all intersections for resource and field (if any)
    private function RemoveAllAssociations($TableName, $TargetFieldName, $Field)
    {
        # retrieve list of entries for this field and resource
        $Entries = $this->Get($Field);

        # for each entry
        foreach ($Entries as $EntryId => $EntryName)
        {
            # remove intersection
            $this->RemoveAssociation($TableName, $TargetFieldName, $EntryId);
        }
    }
}
