<?php
// $Horde: turba/lib/Source.php,v 1.36.2.8 2003/01/27 22:08:44 jan Exp $

require_once TURBA_BASE . '/lib/Turba.php';

/**
 * The Turba_Source:: class provides a set of methods for dealing with
 * specific contact sources.
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @author  Jon Parise <jon@csh.rit.edu>
 * @version $Revision: 1.36.2.8 $
 * @since   Turba 0.0.1
 * @package turba
 */
class Turba_Source {

    /** String containing the internal name of this source. */
    var $name;

    /** String containing the symbolic title of this source. */
    var $title;

    /** Instance of the underlying Turba_Driver class. */
    var $driver;

    /** Hash describing the mapping between Turba attributes and
        driver-specific fields. */
    var $map = array();

    /** Array of fields that must match exactly. */
    var $strict = array();

    /** Boolean indicating whether this source is publicly searchable. */
    var $public = false;

    /** Boolean indicating whether this source is read-only (not editable). */
    var $readonly = true;

    /**
     * Static method to contruct Turba_Source objects. Use this so
     * that we can return PEAR_Error objects if anything goes
     * wrong.
     *
     * @param $name         String containing the internal name of this source.
     * @param $source       Array containing the configuration information for this source.
     */
    function &factory($name, $source)
    {
        $object = new Turba_Source();

        $object->name = $name;
        $object->title = $source['title'];

        /* Obtain a handle to a driver of the requested type. If an
         * instance of that driver doesn't already exist, a new one
         * will be created and returned. */
        include_once TURBA_BASE . '/lib/Driver.php';

        $object->driver = &Turba_Driver::singleton($source['type'], $source['params']);
        if (!empty($object->driver->errno)) {
            return new PEAR_Error("Failed to load $name: [{$object->driver->errno}] {$object->driver->errstr}");
        }

        /* Store and translate the map at the Source level. */
        $object->map = $source['map'];

        /* Store strict fields. */
        if (isset($source['strict'])) {
            $object->strict = $source['strict'];
        }

        /* Set flags. */
        if (isset($source['public'])) {
            $object->public = $source['public'];
        }
        if (isset($source['readonly'])) {
            $object->readonly = $source['readonly']
                                && (!isset($source['admin']) || !in_array(Auth::getAuth(), $source['admin']));
        }

        return $object;
    }

    /**
     * Attempts to return a reference to a concrete Turba_Source instance
     * based on $driver. It will only create a new instance if no
     * Turba_Source instance with the same parameters currently exists.
     *
     * This method must be invoked as: $source = &Turba_Source::singleton()
     *
     * @param $name         String containing the internal name of this source.
     * @param $source       Array containing the configuration information for this source.
     *
     * @return          The concrete Turba_Source reference, or false on an
     *                  error.
     */
    function &singleton($name, $source)
    {
        static $instances;

        if (!isset($instances)) $instances = array();

        $signature = md5(strtolower($name) . '][' . @implode('][', $source));
        if (!isset($instances[$signature])) {
            $instances[$signature] = &Turba_Source::factory($name, $source);
        }

        return $instances[$signature];
    }

    /**
     * Constructs a new Turba_Source object.
     */
    function Turba_Source()
    {
    }

    /**
     * Translates the keys of the first hash from the generalized Turba
     * attributes to the driver-specific fields.  The translation is based
     * on the contents of $this->map.
     *
     * @param $hash         Hash using Turba keys.
     *
     * @return              Translated version of $hash.
     */
    function toDriverKeys($hash)
    {
        $fields = array();
        foreach ($hash as $turba_key => $val) {
            if (isset($this->map[$turba_key])) {
                $driver_key = $this->map[$turba_key];
                $fields[$driver_key] = $val;
            }
        }
        return $fields;
    }

    /**
     * Translates the keys of the first hash from the driver-specific
     * fields to the generalized Turba attributes.  The translation is based
     * on the contents of $this->map.
     *
     * @param $hash         Hash using driver-specific keys.
     *
     * @return              Translated version of $hash.
     */
    function toTurbaKeys($hash)
    {
        $attributes = array();
        for ($i = 0; $i < count($hash); $i++) {
            $entry = $hash[$i];
            $new_entry = array();

            reset($this->map);
            foreach ($this->map as $turba_key => $val) {
                $new_entry[$turba_key] = isset($entry[$val]) ? $entry[$val] : '';
            }

            $attributes[] = $new_entry;
        }
        return $attributes;
    }

    /**
     * Searches the source based on the provided criteria.
     *
     * TODO: Allow $criteria to contain the comparison operator (<, =, >,
     *       'like') and modify the drivers accordingly.
     *
     * @param $search_criteria   Hash containing the search criteria.
     * @param $sort_criteria     The requested sort order which is passed to
     *                           Turba_List::sort().
     * @param const $match       (optional) Do an 'and' or an 'or' search (defaults to TURBA_SEARCH_AND).
     *
     * @return                   The sorted, filtered list of search results.
     */
    function search($search_criteria, $sort_criteria = 'lastname', $match = null, $sort_direction = 0)
    {
        require_once TURBA_BASE . '/lib/List.php';
        require_once TURBA_BASE . '/lib/Object.php';

        /* If this is not a public source, enforce the requirement
         * that the source's owner must be equal to the current
         * user. */
        $strict_fields = array();
        if (!$this->public) {
            $search_criteria['__owner'] = Auth::getAuth();
            $strict_fields = array_keys($this->toDriverKeys(array('__owner' => '')));
        }

        /* Add any fields that much match exactly for this source to
           the $strict_fields array. */
        foreach ($this->strict as $strict_field) {
            if (!in_array($strict_field, $strict_fields)) {
                $strict_fields[] = $strict_field;
            }
        }

        /* Translate the Turba attributes to the driver-specific fields. */
        $fields = $this->toDriverKeys($search_criteria);

        /* Retrieve the search results from the driver. */
        $results = $this->driver->search($fields, array_values($this->map), $strict_fields, $match);

        /*
         * Translate the driver-specific fields in the result back to the
         * more generalized common Turba attributes using the map.
         */
        $remapped_results = $this->toTurbaKeys($results);

        $list = new Turba_List();
        foreach ($remapped_results as $attributes) {
            $list->insert($this->getObject($attributes['__key']));
        }
        $list->sort($sort_criteria, null, null, $sort_direction);

        /* Return the filtered (sorted) results. */
        return $list;
    }

    /**
     * Retrieve one object from the source.
     *
     * @param string $objectID         The unique id of the object to retrieve.
     *
     * @return Turba_AbstractObject   The retrieved object.
     */
    function getObject($objectID)
    {
        $criteria = $this->map['__key'];

        $objects = $this->driver->read($criteria, $objectID, array_values($this->map));
        if (!is_array($objects) || count($objects) != 1) {
            return false;
        } else {
            $remapped_objects = $this->toTurbaKeys($objects);

            if (!empty($remapped_objects[0]['__type'])) {
                $type = ucwords($remapped_objects[0]['__type']);
                $class = 'Turba_' . $type;
                if (!class_exists($class)) {
                    @include_once TURBA_BASE . '/lib/' . $type . '.php';
                }

                if (class_exists($class)) {
                    return new $class($this, $remapped_objects[0]);
                }
            }

            require_once TURBA_BASE . '/lib/Object.php';
            return new Turba_Object($this, $remapped_objects[0]);
        }
    }

    /**
     * Adds a new entry to the contact source.
     *
     * @param array $attributes     The attributes of the new object to add.
     * @return mixed  The new __key value on success, or a PEAR_Error object on failure.
     */
    function addObject($attributes)
    {
        if ($this->readonly) {
            return false;
        }

        if (!isset($attributes['__key'])) {
            $attributes['__key'] = $this->driver->makeKey($this->toDriverKeys($attributes));
        }
        if (!isset($attributes['__type'])) {
            $attributes['__type'] = 'Object';
        }

        $key = $attributes['__key'];
        $attributes = $this->toDriverKeys($attributes);
        $result = $this->driver->addObject($attributes);

        return PEAR::isError($result) ? $result : $key;
    }

    /**
     * Deletes the specified entry from the contact source.
     *
     * @param string $object_id     The ID of the object to delete.
     */
    function removeObject($object_id)
    {
        if ($this->readonly) return false;

        list($object_key,) = each($this->toDriverKeys(array('__key' => '')));
        return $this->driver->removeObject($object_key, $object_id);
    }

    /**
     * Modifies an existing entry in the contact source.
     *
     * @param Turba_AbstractObject $object     The object to update.
     */
    function setObject($object)
    {
        if ($this->readonly) return false;

        $attributes = $this->toDriverKeys($object->getAttributes());
        list($object_key, $object_id) = each($this->toDriverKeys(array('__key' => $object->getValue('__key'))));

        return $this->driver->setObject($object_key, $object_id, $attributes);
    }

    /**
     * Returns the criteria available for this source except '__key'.
     *
     * @return              An array containing the criteria.
     */
    function getCriteria()
    {
        $criteria = array();
        foreach ($this->map as $key => $value) {
            if ($key != '__key') {
                $criteria[$key] = $value;
            }
        }
        return $criteria;
    }

}
?>
