<?php
// +----------------------------------------------------------------------+
// | popoon                                                               |
// +----------------------------------------------------------------------+
// | Copyright (c) 2001-2006 Bitflux GmbH                                 |
// +----------------------------------------------------------------------+
// | Licensed under the Apache License, Version 2.0 (the "License");      |
// | you may not use this file except in compliance with the License.     |
// | You may obtain a copy of the License at                              |
// | http://www.apache.org/licenses/LICENSE-2.0                           |
// | Unless required by applicable law or agreed to in writing, software  |
// | distributed under the License is distributed on an "AS IS" BASIS,    |
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or      |
// | implied. See the License for the specific language governing         |
// | permissions and limitations under the License.                       |
// +----------------------------------------------------------------------+
// | Author: Hannes Gassert <hannes.gassert@unifr.ch>                     |
// +----------------------------------------------------------------------+
//
// $Id: cache.php 6188 2006-01-03 10:09:08Z chregu $
 

require_once('Cache.php'); 

/**
 * Component Caching for Popoon
 *
 * This classy class should mimic the caching mechanims Cocoon uses.
 * Have a look at http://bugzilla.bitflux.ch/show_bug.cgi?id=23 and the Cocoon documentation for more on this topic.
 *
 * @package  popoon
 * @author Hannes Gassert <hannes.gassert@unifr.ch>
 * @version $Id
 * @TODO Take care of sending HTTP Cache headers, implement and test other containers than the filesystem, DO COMPONENTS
 */
class componentCache{

    /**
     * "Definition" of the 'cachable' interface (methods only)
     *
     * @var array    $cachableInterface    Methods required by the 'cachable' interface
     * @access private
     */
    var $cachableInterface = array('generateValidity', 'checkValidity', 'generateKey');

    /**
     * Array of PEAR::Cache container objects, one entry per container.
     *
     * @var  array   $cacheHandlers
     * @access private
     */
    var $cacheHandlers      = array();

    /**
     * Name of default PEAR::Cache container
     *
     * @var   string
     * @access private
     */
    var $defaultContainer   = 'file';
    
    /**
     * Name of PEAR::Cache container currently in use (currently a container is chosen per pipeline)
     *
     * @var   string
     * @access private
     */    
    var $currentContainer   = 'file';
    
    /**
     * Filename suffix for cache-metafiles (for validityObjects)
     *
     * @var   string
     * @access private
     */    
    var $metafileSuffix     = '.meta';

    /**
     * Filename suffix for cache-globalfiles (for globals)
     *
     * @var   string
     * @access private
     */    
    var $globalSuffix     = '.global';
	
    /**
     * Cache-key generated by the last component in the pipeline
     *
     * @var   string   MD5 hash, probably
     * @access private
     */    
    var $lastCacheKey       = null;



  /**
   * Indicates a cache miss.
   *
   * All components must recalculate their results if this is set to true.
   *
   * @var  $miss             
   * @access        private    
   * @_load()            
   */ 
    var $miss  = false;
    
    /**
     * Type of the last component in the pipeline
     *
     * Used by loadLast(), updated by init().
     *
     * @see init(), loadLast()
     * @access private
     */
    var $componentTypes  = array();
    
    /**
     * Indicates wether the current component is cachable at all.
     * True by default, is set to false if the component doesn't implement the
     * 'cachable' interface, for example.
     *
     * @var bool
     */
    var $iscachable        = true;
    
    /**
     * Cache-key generated by the current component
     *
     * @var    string
     * @see    $currentComponent
     * @access private
     */    
    var $currentCacheKey    = null;
    
    var $currentMetaObject = array();


    /**
     * Constructor
     *
     * If necessary creates a new PEAR::Cache object of the desired type and adds it to $cacheHandlers
     * Also sets $currentContainer to the name of the currenct container. Currenty you should only use the default
     * container (Container/file.php), I haven't tested anything else yet.
     *
     * @param  array  map:pipeline attributes
     * @see $cacheHandlers
     * @access public
     */
    function componentCache($pipelineAttribs, &$sitemap){ 
        $this->sitemap = &$sitemap;
        //determine cache container       
        if(isset($pipelineAttribs['container'])) {
            $containerType = $pipelineAttribs['container'];
        }
        else {
            $containerType = $this->defaultContainer; 
        }

        if(!isset($this->cacheHandlers[$containerType]) || !is_object($this->cacheHandlers[$containerType])){
            $this->cacheHandlers[$containerType] =& new Cache($containerType, $this->getContainerOptions($containerType, $pipelineAttribs));
        }

        $this->currentContainer = $containerType;
    }
    
    /**
     * Initalizer
     *
     * This is some kind of pseudo constructor. Hands the current component to this class.
     * This means that init() _has_ to be called for every component that should be cached.
     *
     * @param   object  $component  Component to be cached
     * @access public
     */
    function init(&$component){
        
        //only work with components implementing the 'cachable' interface
        if(!$this->implementscachable($component)) {
            $this->iscachable = false;
            popoon::raiseError("You're trying to use a non-cachable component inside of a pipeline marked cachable.\n".
                               "This should be supported somehow but isn't implemented yet. Sorry.",
                               POPOON_ERROR_WARNING,
                               __FILE__, __LINE__, null);
            return(false);
        }
        else{
            $this->currentComponent =& $component;
            $this->lastCacheKey = $this->currentCacheKey;
            $this->componentTypes[] = $this->currentComponent->attribs['type'];
            $this->currentCacheKey = $component->generateKey($component->attribs, $this->lastCacheKey);
            $this->setCacheGroup();
            return(true);
        }
    }


    
    /**
     * Try to get cached component output
     *
     * Tries to get a validityObject for the current request. If there's one and if the component does verify it's validity,
     * the cached content is fetched and returned. Works only with Components implementing the 'cachable' interface, of course.
     * In any other case than success returns null.
     *
     * @return   string   cached content
     * @access   private
     * @see implementscachable(), loadValidityObject(), checkValidity(), loadCachedContent()
     */
    function _load($what = 'content'){

        $component =& $this->currentComponent;
                   
        if(!$this->miss && !is_null($validityObject = $this->loadValidityObject())){  //there's a candidate in the cache!
            //test if this candidate is a valid one    
            if($component->checkValidity($validityObject)){
                //if so, we do really have a valid cache hit!
                if($what == 'content') {
					 return($this->loadCachedContent());
				}
                else {
					return($this->currentCacheKey);
				}
            }
        }
        //in any other case
        $this->miss = true;
        return(null);
    }

    /**
     * Load content associated with last cache key
     *
     * When we come to a point in the pipeline where we need the output of the last component, we call loadLast(). 
     * It resets the cache object to the state it was when handling the last component, fetches the content wanted and
     * sets back the state of this object.
     *
     * @param  $target  reference to the variable you wan't to have the content you're looking for put into.
     */
    function loadLast(&$target){         

        //get last componentType
        $pos = count($this->componentTypes) - 2; if($pos < 0) $pos = 0; $lastComponentType = $this->componentTypes[$pos];

        //backup currentCacheKey
        $t = $this->currentCacheKey;

        //last is now current
        $this->currentCacheKey = $this->lastCacheKey;

        //also set back currentCacheGroup
        $this->setCacheGroup($lastComponentType, $this->lastCacheKey);

        /*load it
           but just, if target is_null, if it's not null, we already have something in 
           $this->xml. This is mostly set, when a component before also had to regenerate
           its data and can be mostly seen when we create a whole pipeline (eg. the generator
            loads xml-data, then passes that to a transformer. There is no need to read the
            content from the cache, if something like that happens. Moreover it leads to
            problems with libxslt2/libxml internal handling, as it looses some info when 
            we serialize and deserialize xml-data (this has to be tested more thoroughly)
            *by chregu*
        */
        
        if (is_null($target)) {
            $target = $this->loadCachedContent();
        }
       
        //reset all
        $this->currentCacheKey = $t;
        $this->setCacheGroup();
    }

    /**
     * Load the content associated to the current cache Key
     *
     * You'd use this rather seldom, cause most of the time you'll wan't to use loadLast()
     * @param bool $printsDirectly if header and result from cache should be printed directly (needed for serializers)
     * @see loadLast, $currentCacheKey
     */
    function load($printsDirectly = false){        return($this->loadCachedContent($printsDirectly));}

    /**
     * Check if there's something in the cache for the current cache key
     *
     * Behind this hides all the validity checking between the cache system and the components, 
     * so you should call this alway. Perhaps I could in some way call this from init() to have
     * one "must-do" call less..  
     *
     * @return bool   true if the content associated with there's a valid cache entry for $currentCacheKey.
     * @see $currentCacheKey
     */
    function isCached(){    return(!is_null($this->_load('id')));}

    /**
     * Returns the current cache key.
     *
     * Just a getter-method. I don't remember where I wanted to use this.
     *
     * @return string
     * @see $currentCacheKey
     */
    function getCacheKey(){ return($this->currentCacheKey);}

    
    /**
     * Store component generated data 
     *
     * Tells the current component to produce the data and call the methods to store that content and the according validityObject.
     *
     * @param   string   $launchMethod     Method to call to make the component generate the content
     * @param   bool     $printsDirectly  Indicates wether $launchMethod prints the content directly (as e.g. the readers do) or wether we have to get the content from $content
     * @access  public
     * @see storeContent(), $storeValidityObhect()
     */
    function store($content = null, $launchMethod = null, $param = null, $printsDirectly = false){
        
        if(!$this->iscachable) return(false); //we could also support a mix of cachable and non-cachable compos.,
        
        if($printsDirectly) {
            ob_start();
        } 

        if(!is_null($launchMethod) && method_exists($this->currentComponent, $launchMethod)) {
              $retVal = $this->currentComponent->$launchMethod($param);
        }
        else { /*add error handling here?*/ }
        
        if($printsDirectly){  // if $printsDirectly, then it must be some kind of serializer, so we need the headers on output
            $this->sitemap->printHeader();
            $content = ob_get_contents();
            ob_end_flush();
        }
        // else{ popoon_sitemap::var2XMLString($content); }
        if ( strtolower(get_class($content)) == "domdocument")
        {
            $content = $content->dumpmem();
        }
        
        $done = $this->storeContent($content) &&  $this->storeValidityObject()  ;   //only store validityObject after content was saved successfully.
        if(!$done)  popoon::raiseError('Could not write to cache! Is cache_dir writable? Is your component outputting?',
                                       POPOON_ERROR_WARNING,
                                       __FILE__, __LINE__, null);
        return($content);
        
    }
    
    /**
     * Checks wether a component implements the cachable interface
     *
     * Tests if all methods in $interface_methods are implemented by $component.
     *
     * @access  public
     * @param   object  poopon component object
     * @returns bool
     * @access public
     */
    function implementscachable(&$component){
        foreach($this->cachableInterface as $mustHaveMethod)
            if(!method_exists($component, $mustHaveMethod)) return(false);
        return(true);
    }

         
    /**
     * Try to load  validityObject from $cacheKey.meta
     *
     * @return   object  validityObject
     */
    function loadValidityObject(){
        $metafileKey   = $this->currentCacheKey . $this->metafileSuffix;
        $metafileGroup = $this->currentCacheGroup;
        $this->currentValidityObject = $this->cacheHandlers[$this->currentContainer]->get($metafileKey, $metafileGroup);        
       
        return($this->currentValidityObject["validityObject"] );
    }

    /**
     * Try to load  globals 
     *
     * @return   bool
     */
    function loadGlobals(){
		if (isset($this->currentMetaObject["globals"] ) && $globals = $this->currentMetaObject["globals"] ) {
			popoon_sitemap::setGlobalOptionsAll($globals);
		}
        return(true);
    }
    

    /**
     * Try to load header
     *
     * @return   bool
     */
    function loadHeader(){
		if (isset($this->currentMetaObject["header"] ) ) {
            $this->sitemap->header = array_merge($this->currentMetaObject["header"],$this->sitemap->header);
		}
        return(true);
    }
    
    
    
    /**
     * Stores validityObject in Cache
     *
     * Tinkers file and group name and calls save() method of current PEAR::Cache container.
     *
     * @return  bool  
     */
    function storeValidityObject(){
        $metafileKey   = $this->currentCacheKey . $this->metafileSuffix;
        $metafileGroup = $this->currentCacheGroup;
        $this->currentValidityObject["validityObject"] = $this->currentComponent->generateValidity();
        return($this->cacheHandlers[$this->currentContainer]->save($metafileKey,
                                                                   $this->currentValidityObject,
                                                                   '',   //hm.. perhaps we'll have to talk aboout expiration once..  
                                                                   $metafileGroup
                                                                   ));
                                                                
    }

    /**
     * Stores Globals in currentMetaObject
     *
     * @return  bool  
     * @see storeContent()
     */
    function storeGlobals() {
		if (is_array($globals = popoon_sitemap::getGlobalOptionsAll())) {
		    $this->currentMetaObject["globals"] = $globals;
        } 
		return true;
    }

    /**
     * Stores Header in currentMetaObject
     *
     * @return  bool  
     * @see storeContent()
     */
    function storeHeader() {
        if (isset($this->sitemap->header) && count($this->sitemap->header) > 0 ) {
	    	$this->currentMetaObject["header"] = $this->sitemap->header;
        }
	    return true;
    }

    /**
     * Stores component data in cache
     *
     * @param  $content  string representation of the output of the current component 
     * @return bool
     * @see storeGlobals()
     */
    function storeContent($content = '') {
        // store the globals in the currentMetaObject
        $this->storeGlobals();
        $this->storeHeader();        
        if(!empty($content)){ 
            return($this->cacheHandlers[$this->currentContainer]->extSave($this->currentCacheKey, $content, serialize($this->currentMetaObject), null, $this->setCacheGroup()));
        }
        else { /* else we should do popoon::error, I guess..*/ }
    }
    
    /**
     * Tries to load string content belonging to the current cache key 
     *
     * @param bool $printsDirectly if header and result from cache should be printed directly (needed for serializers)
     * @return string
     * @see $currentCacheKey, loadGlobals()
     */
    function loadCachedContent($printsDirectly = false){
        $this->currentMetaObject = unserialize($this->cacheHandlers[$this->currentContainer]->getUserData($this->currentCacheKey,$this->currentCacheGroup));
        $this->loadGlobals();
        $this->loadHeader();
        if ($printsDirectly) {
            $this->sitemap->printHeader();
            print $this->cacheHandlers[$this->currentContainer]->get($this->currentCacheKey, $this->currentCacheGroup);
            return true;
        } else {
            return($this->cacheHandlers[$this->currentContainer]->get($this->currentCacheKey, $this->currentCacheGroup));
        }
    }


    /**
     * Determine group of current component
     *
     * Cache objects are grouped primarily to avoid too large directories, which would mean too long a lookup time to get
     * a particular file. 
     *
     * @return   string   Group name
     * @access private
     */
    function setCacheGroup($type = null, $cacheKey = null){
        if(is_null($type))     $type = $this->currentComponent->attribs['type'];
        if(is_null($cacheKey)) $cacheKey = $this->currentCacheKey;
        $this->currentCacheGroup = $type . $cacheKey[0];
        return($this->currentCacheGroup);
    }
    

    /**
     * Determines the option to use for container $containerType
     *
     * This is quite a bad method. It relies halfway on the bitfluxcms environment, which it shouldn't. 
     * I did not know where to put popoon-specific configuration, so I added
     * my stuff to the bitfluxcms $BX_config hash.. dunno if that's good.
     * If you don't use bitfluxcms but standalone popoon, you can put container options such as
     * 'cachedir' into your sitemap.xml as attributes of a pipeline. You'd better take absolute path names
     * there, which is something I should add to the popoon documentation, if there's such a thing :)
     *
     * @return   array   PEAR::Cache container options
     * @param    string  $containerType
     * @param    array   $pipelineAttribs
     */
    function getContainerOptions($containerType = 'file', $pipelineAttribs = array()){
  
        // SETTINGS FOR PEAR::Cache/Container/File
        if(strtolower($containerType) == 'file'){
            if(isset($pipelineAttribs['cachedir'])) {
                $pipelineAttribs['cache_dir'] = $pipelineAttribs['cachedir']; unset($pipelineAttribs['cachedir']);
            }
            else if(isset($GLOBALS['BX_config']['componentCaching']['params']['cache_dir'])){
                $pipelineAttribs['cache_dir'] = $GLOBALS['BX_config']['componentCaching']['params']['cache_dir']; 
            }
            else if (isset($GLOBALS['BX_config']['popoon']['cacheParams'])) {
                $pipelineAttribs['cache_dir']  = $GLOBALS['BX_config']['popoon']['cacheParams']['cache_dir'];
                $pipelineAttribs['encoding_mode'] = $GLOBALS['BX_config']['popoon']['cacheParams']['encoding_mode']; 				
            } 
            else if (isset($GLOBALS['BX_config']['caching']['params'])) {
                $pipelineAttribs['cache_dir']  = $GLOBALS['BX_config']['caching']['params']['cache_dir'];
                $pipelineAttribs['encoding_mode'] = $GLOBALS['BX_config']['caching']['params']['encoding_mode']; 				
            } 
            else if (defined("BX_PROJECT_DIR")) {
                $pipelineAttribs['cache_dir']  = BX_PROJECT_DIR."/tmp/cache/";
            }
            else {
                $pipelineAttribs['cache_dir'] = "./tmp/cache/";
            }
            $pipelineAttribs['max_userdata_linelength'] = 0;

            //don't bother PEAR::Cache with our own attributes
            if(isset($pipelineAttribs['cachable'])) unset($pipelineAttribs['cachable']);

            //componentCaching gets a directory for itself
            $pipelineAttribs['cache_dir'] = str_replace('//', '/', $pipelineAttribs['cache_dir'] . '/components/');


        }              


        else{
            //no other containers looked at yet..
        }


        return($pipelineAttribs); 
    }
}



?>
