<?php
/**
 * The Turba_Source:: class provides a set of methods for dealing with
 * specific contact sources.
 *
 * $Horde: turba/lib/Source.php,v 1.59 2003/02/18 05:47:20 chuck Exp $
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @author  Jon Parise <jon@csh.rit.edu>
 * @version $Revision: 1.59 $
 * @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;

    /**
     * Any admins for this source.
     * @var array $admin
     */
    var $admin = array();

    /**
     * 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. */
        require_once TURBA_BASE . '/lib/Driver.php';

        $object->driver = &Turba_Driver::singleton($source['type'], $source['params']);
        if (!empty($object->driver->errno)) {
            return PEAR::raiseError(sprintf('Failed to load %s: %s %s', $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'];
        }

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

        /* 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 = serialize(array($name, $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();
        $iMax = count($hash);
        for ($i = 0; $i < $iMax; $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, $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 PEAR::raiseError(_("Requested object not found."));
        }

        $remapped_objects = $this->toTurbaKeys($objects);

        if (!empty($remapped_objects[0]['__type'])) {
            $type = ucwords($remapped_objects[0]['__type']);
            $class = 'Turba_' . $type;
            if (!class_exists($class)) {
                @require_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';
        }
        if (isset($this->map['__owner'])) {
            $attributes['__owner'] = Auth::getAuth();
        }

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

        return is_a($result, 'PEAR_Error') ? $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 array         An array containing the criteria.
     */
    function getCriteria()
    {
        $criteria = $this->map;
        unset($criteria['__key']);
        return $criteria;
    }

}
