<?php
/*
 * $RCSfile: GalleryEntity.class,v $
 *
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2005 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */
/**
 * @package GalleryCore
 * @version $Revision: 1.45 $ $Date: 2005/08/23 03:49:02 $
 * @author Bharat Mediratta <bharat@menalto.com>
 */

/**
 * Load the parent class
 */
GalleryCoreApi::relativeRequireOnce('modules/core/classes/GalleryPersistent.class');

/**
 * A GalleryEntity is a stateful representation of an entity in the persistent
 * store.  You can load, modify, save and delete it.  Any modifications made to
 * this entity will live in memory until you commit the changes to the store
 * (hence, it's stateful).
 *
 * @g2 <class-name>GalleryEntity</class-name>
 * @g2 <parent-class-name>GalleryPersistent</parent-class-name>
 * @g2 <schema>
 * @g2   <schema-major>1</schema-major>
 * @g2   <schema-minor>1</schema-minor>
 * @g2 </schema>
 *
 * @package GalleryCore
 * @subpackage Classes
 */
class GalleryEntity_core extends GalleryPersistent {

    /**
     * The id of this item
     *
     * @g2 <member>
     * @g2   <member-name>id</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2   <id/>
     * @g2   <primary/>
     * @g2 </member>
     *
     * @var int $_id
     * @access private
     */
    var $_id;

    /**
     * Date and time this item was created, in seconds since the epoch.
     *
     * @g2 <member>
     * @g2   <member-name>creationTimestamp</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2   <indexed/>
     * @g2   <required/>
     * @g2 </member>
     *
     * @var int $_creationTimestamp
     * @access private
     */
    var $_creationTimestamp;

    /**
     * Does this entity type support linking?  Subclasses should override this method and
     * return true if they support linking
     *
     * @g2 <member>
     * @g2   <member-name>isLinkable</member-name>
     * @g2   <member-type>BOOLEAN</member-type>
     * @g2   <indexed/>
     * @g2   <required/>
     * @g2 </member>
     */
    var $_isLinkable;

    /**
     * The id of the target entity this entity is linked to
     *
     * @g2 <member>
     * @g2   <member-name>linkId</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2 </member>
     *
     * @var int $_linkId
     * @access private
     */
    var $_linkId;

    /**
     * The target entity this entity is linked to
     *
     * @var int $_linkedEntity
     * @access private
     */
    var $_linkedEntity = null;

    /**
     * Date and time this item was last modified, in seconds since the epoch.
     *
     * @g2 <member>
     * @g2   <member-name>modificationTimestamp</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2   <indexed/>
     * @g2   <required/>
     * @g2 </member>
     *
     * @var int $_modificationTimestamp;
     * @access private
     */
    var $_modificationTimestamp;

    /**
     * The serial number of this object in the persistent store.  This value
     * will help us to avoid collisions.
     *
     * @g2 <member>
     * @g2   <member-name>serialNumber</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2   <indexed/>
     * @g2   <required/>
     * @g2 </member>
     *
     * @var int $_serialNumber
     * @access private
     */
    var $_serialNumber;

    /**
     * The actual type of this object.  This will allow the storage class to
     * correctly load this object from the database.
     *
     * @g2 <member>
     * @g2   <member-name>entityType</member-name>
     * @g2   <member-type>STRING</member-type>
     * @g2   <member-size>SMALL</member-size>
     * @g2   <required/>
     * @g2 </member>
     *
     * @var string $_entityType
     * @access private
     */
    var $_entityType;

    /**
     * Handlers to run when this entity is loaded.
     *
     * @g2 <member>
     * @g2   <member-name>onLoadHandlers</member-name>
     * @g2   <member-type>STRING</member-type>
     * @g2   <member-size>MEDIUM</member-size>
     * @g2 </member>
     *
     * @var string $_onLoadHandlers
     * @access private
     */
    var $_onLoadHandlers;

    /*
     * ****************************************
     *                 Methods
     * ****************************************
     */

    /**
     * Create a new instance of this GalleryEntity in the persistent store
     *
     * @return object GalleryStatus a status code
     */
    function create() {
	global $gallery;

	/* Create a new instance of this entity in the persistent store */
	$storage =& $gallery->getStorage();
	$ret = $storage->newEntity($this);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Set the creation time on this entity */
	$this->setCreationTimestamp(time());

	/* Set the object type */
	$this->setEntityType($this->getClassName());

	/* By default, entities are not linkable */
	$this->setIsLinkable(false);

	return GalleryStatus::success();
    }

    /**
     * Create a new linked version of this item into a new album
     *
     * @param object GalleryEntity the entity we're linking to
     * @param int the id of the new parent
     * @return array object GalleryStatus a status code
     *               object GalleryItem the linked item
     */
    function createLink($entity) {
	global $gallery;

	if (get_class($this) != get_class($entity)) {
	    return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__);
	}

	/* Follow our usual creation process (same as create() above) */
	$storage =& $gallery->getStorage();
	$ret = $storage->newEntity($this);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Set the creation time on this entity */
	$this->setCreationTimestamp(time());

	/* Set the object type */
	$this->setEntityType($this->getClassName());

	/* But attach to the linked entity */
	$this->setIsLinkable(true);
	$this->setLinkId($entity->getId());
	$this->setLinkedEntity($entity);

	return GalleryStatus::success();
    }

    /**
     * Save the changes to this GalleryEntity
     *
     * Save the changes to this GalleryEntity.
     *
     * @param bool post a save event; only disable this during core upgrades
     * @return object GalleryStatus a status code
     * @access public
     */
    function save($postEvent=true) {
	global $gallery;

	/*
	 * Newly created objects don't need to be locked 'cause they're not in
	 * the database yet.  Everything else does.
	 */
	if (!GalleryCoreApi::isWriteLocked($this->getId())) {
	    if (!$this->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
		return GalleryStatus::error(ERROR_LOCK_REQUIRED,
					    __FILE__, __LINE__);
	    }
	}

	if ($this->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
	    GalleryCoreApi::relativeRequireOnce(
		'modules/core/classes/GalleryAccessSubscriberMap.class');
	    $ret = GalleryAccessSubscriberMap::addMapEntry(
		array('itemId' => $this->getId(), 'accessListId' => 0));
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	if ($this->isModified()) {
	    /* Change the modification date */
	    $this->setModificationTimestamp(time());

	    if ($postEvent) {
		$event = GalleryCoreApi::newEvent('GalleryEntity::save');
		$event->setEntity($this);
		list ($ret) = GalleryCoreApi::postEvent($event);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }

	    $storage =& $gallery->getStorage();
	    $changedEntityType = ($this->getModifiedFlag('entityType') != MEMBER_UNMODIFIED);

	    $ret = $storage->saveEntity($this);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    /* Update our caches */
	    if ($changedEntityType) {
		/*
		 * We changed the entity type which probably means that the entity class doesn't
		 * line up with its embedded entityType.  Clear this from our cache so that the
		 * next load actually gets the clean, correct version.
		 */
		GalleryDataCache::remove(
		    sprintf('GalleryEntityHelper::loadEntitiesById(%s)', $this->getId()));
		GalleryDataCache::removeFromDisk(
		    array('type' => 'entity', 'itemId' => $this->getId()));
	    } else {
		GalleryDataCache::put(
		    sprintf('GalleryEntityHelper::loadEntitiesById(%s)', $this->getId()), $this);
		GalleryDataCache::putToDisk(array('type' => 'entity', 'itemId' => $this->getId()),
					    $this, array($this->getClassFile()));
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Refresh this item from the persistent store
     *
     * @access public
     * @return array object GalleryStatus a status code,
     *               object the refreshed GalleryEntity
     */
    function refresh() {
	global $gallery;

	/* Are we trying to refresh an object that isn't in the database? */
	if ($this->testPersistentFlag(STORAGE_FLAG_DELETED) ||
	    $this->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
	    return array(GalleryStatus::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__),
			 null);
	}

	$storage =& $gallery->getStorage();

	list ($ret, $freshEntity) = $storage->refreshEntity($this);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	return array(GalleryStatus::success(), $freshEntity);
    }

    /**
     * Delete this GalleryEntity
     *
     * @access public
     * @return object GalleryStatus a status code
     */
    function delete() {
	global $gallery;

	if (!GalleryCoreApi::isWriteLocked($this->getId())) {
	    return GalleryStatus::error(ERROR_LOCK_REQUIRED, __FILE__, __LINE__);
	}

	$event = GalleryCoreApi::newEvent('GalleryEntity::delete');
	$event->setEntity($this);
	list ($ret) = GalleryCoreApi::postEvent($event);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$ret = GalleryCoreApi::removePluginParametersForItemId($this->getId());
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	GalleryCoreApi::relativeRequireOnce(
	    'modules/core/classes/GalleryAccessSubscriberMap.class');
	$ret = GalleryAccessSubscriberMap::removeMapEntry(array('itemId' => $this->getId()));
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}
	
	$storage =& $gallery->getStorage();
	$ret = $storage->deleteEntity($this);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	GalleryDataCache::remove(
	    sprintf('GalleryEntityHelper::loadEntitiesById(%s)', $this->getId()));
	GalleryDataCache::removeFromDisk(array('type' => 'entity', 'itemId' => $this->getId()));

	return GalleryStatus::success();
    }

    /**
     * This is called after an entity is loaded by the GalleryStorage subsystem.
     * Perform any actions that are required after loading the entity.
     *
     * @return object GalleryStatus a status code
     */
    function onLoad() {
	global $gallery;
	
	$linkId = $this->getLinkId();
	if (!empty($linkId)) {
	    list ($ret, $this->_linkedEntity) = GalleryCoreApi::loadEntitiesById($linkId);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	/* 
	 * Run any onLoad handlers for this entity
	 * Modules that wish to implement an onLoad handler for an entity must provide
	 * a class that implements the GalleryOnLoadHandler interface.
	 */
	$onLoadHandlers = $this->getOnLoadHandlers();
	if (!empty($onLoadHandlers)) {
	    /* Determine whether we're in an upgrade process */
	    $session =& $gallery->getSession();
	    $isDuringInstallOrUpgrade = !isset($session);
	    
	    foreach (explode('|', $onLoadHandlers) as $handlerId) {
		if (empty($handlerId)) {
		    continue;
		}
		list ($ret, $handler) =
		    GalleryCoreApi::newFactoryInstanceById('GalleryOnLoadHandler', $handlerId);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
		if (isset($handler)) {
		    /* GalleryOnLoadHandlerInterface_1_0::onLoad(&$entity, $duringUpgrade)
		     *
		     * A GalleryOnLoadHandler has to implement this method
		     * It is called for each entity when it is loaded from the disk cache or the
		     * persistent store (the database), but not when the entity is already in the
		     * memory cache.
		     *
		     * During the core module install or upgrade, there's no active gallery session
		     * and $duringUpgrade is set to false. If $duringUpgrade is set to false, be
		     * aware that everything that is related to the current active user will
		     * error-out. The recommended usage is to do nothing and just return a success
		     * status in onLoad if $duringUpgrade is set to false. If this would affect
		     * data integrity and / or you are sure that your onLoad method does not
		     * require an activeSession, you can ignore $duringUpgrade.
		     *
		     * @param reference to an entity
		     * @param boolean whether the onLoad handler is being called during an upgrade
		     * @return object GalleryStatus a status code
		     */
		    $ret = $handler->onLoad($this, $isDuringInstallOrUpgrade);
		    if ($ret->isError()) {
			return $ret->wrap(__FILE__, __LINE__);
		    }
		}
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Check for an onLoad handler.
     *
     * @param string handler id
     * @return boolean
     */
    function hasOnLoadHandler($handlerId) {
	return (strpos("|$handlerId|", $this->getOnLoadHandlers()) !== false);
    }

    /**
     * Add onLoad handler.
     *
     * @param string handler id
     */
    function addOnLoadHandler($handlerId) {
	$onLoadHandlers = $this->getOnLoadHandlers();
	if (empty($onLoadHandlers)) {
	    $onLoadHandlers = '|';
	}
	$this->setOnLoadHandlers($onLoadHandlers . $handlerId . '|');
    }

    /**
     * Remove onLoad handler.
     *
     * @param string handler id
     */
    function removeOnLoadHandler($handlerId) {
	$onLoadHandlers = preg_replace('{\|' . preg_quote($handlerId) . '\|}', '|',
				       $this->getOnLoadHandlers());
	if ($onLoadHandlers == '|') {
	    $onLoadHandlers = null;
	}
	$this->setOnLoadHandlers($onLoadHandlers);
    }

    /**
     * Return the linked entity
     *
     * @return object GalleryEntity the linked entity (or null if there's no link)
     */
    function getLinkedEntity() {
	return $this->_linkedEntity;
    }

    /**
     * Set the linked entity
     *
     * @access private
     */
    function setLinkedEntity($entity) {
	$this->_linkedEntity = $entity;
    }

    /**
     * This is called after an entity is saved by the GalleryStorage subsystem.
     * Perform any actions that are required after saving the entity.
     *
     * @return object GalleryStatus a status code
     */
    function onSave() {
	return GalleryStatus::success();
    }

    /**
     * Return the name of this type of item.
     *
     * Subclasses must override this to provide their own type names.
     *
     * @param boolean if the name is to be translated using the current language
     * @return array string to be used by itself ("Photo")
     *               string to be used in context ("photo")
     */
    function itemTypeName($localized = true) {
	assert(false);
    }

    /**
     * Return true if this entity is linked to another
     *
     * @return true if yes
     */
    function isLinked() {
	$linkedEntity = $this->getLinkedEntity();
	return isset($linkedEntity);
    }

    /**
     * Detach this entity from the entity it is linked to by the simple expedient
     * of overwriting over all non-null members with the equivalent from the link target.
     *
     * @return object GalleryStatus a status code
     */
    function detachLink() {
	if (!$this->isLinked()) {
	    return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__, 'Not a link!');
	}

	/* Save off the linked entity */
	$linkedEntity = $this->getLinkedEntity();

	/* Get rid of the link */
	$this->setLinkId(null);
	$this->setLinkedEntity(null);

	/*
	 * Then get the link member data and our member data.  If we do this *before*
	 * getting rid of the link, then my member data will be drawn from the link.
	 */
	$sourceMemberData = $linkedEntity->getMemberData();
	$myMemberData = $this->getMemberData();

	foreach (array_keys($sourceMemberData) as $key) {
	    if (!isset($myMemberData[$key])) {
		$func = "set$key";
		$this->$func($sourceMemberData[$key]);
	    }
	}

	return GalleryStatus::success();
    }
}

include(dirname(__FILE__) . '/interfaces/GalleryEntity.inc');
?>
