From e88e501365863159373f6861ab90ef1e08a73092 Mon Sep 17 00:00:00 2001
From: tomas <tomas@cfc7b370-4200-0410-a6e3-cb6bdb053afe>
Date: Mon, 14 Feb 2005 00:36:23 +0000
Subject: [PATCH] Little big changes: playlist editing methods added to
 GreenBox,  + support methods in Metadata and BasicStor added

---
 .../modules/storageServer/var/BasicStor.php   |  96 ++++-
 .../modules/storageServer/var/GreenBox.php    | 365 +++++++++++++++++-
 .../modules/storageServer/var/MetaData.php    | 190 ++++++---
 3 files changed, 566 insertions(+), 85 deletions(-)

diff --git a/livesupport/modules/storageServer/var/BasicStor.php b/livesupport/modules/storageServer/var/BasicStor.php
index c01c223f7..1fc93e3d0 100644
--- a/livesupport/modules/storageServer/var/BasicStor.php
+++ b/livesupport/modules/storageServer/var/BasicStor.php
@@ -23,7 +23,7 @@
  
  
     Author   : $Author: tomas $
-    Version  : $Revision: 1.27 $
+    Version  : $Revision: 1.28 $
     Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/storageServer/var/BasicStor.php,v $
 
 ------------------------------------------------------------------------------*/
@@ -52,7 +52,7 @@ require_once "Transport.php";
  *  Core of LiveSupport file storage module
  *
  *  @author  $Author: tomas $
- *  @version $Revision: 1.27 $
+ *  @version $Revision: 1.28 $
  *  @see Alib
  */
 class BasicStor extends Alib{
@@ -269,10 +269,30 @@ class BasicStor extends Alib{
         return ($cnt == 1);
     }
     
+    /**
+     *  Get gunid from token
+     *
+     *  @param token string, access/put token
+     *  @param type string 'put'|'access'|'download'
+     *  @return string
+     */
+    function _gunidFromToken($token, $type='put')
+    {
+        $acc = $this->dbc->getRow("
+            SELECT to_hex(gunid)as gunid, ext FROM {$this->accessTable}
+            WHERE token=x'{$token}'::bigint AND type='$type'
+        ");
+        if(PEAR::isError($acc)){ return $acc; }
+        $gunid = StoredFile::_normalizeGunid($acc['gunid']);
+        if(PEAR::isError($gunid)){ return $gunid; }
+        return $gunid;
+    }
+    
     /**
      *  Create and return access link to real file
      *
      *  @param realFname string, local filepath to accessed file
+     *      (NULL for only increase access counter, no symlink)
      *  @param ext string, useful filename extension for accessed file
      *  @param gunid int, global unique id
      *  @param type string 'access'|'download'
@@ -281,16 +301,18 @@ class BasicStor extends Alib{
     function bsAccess($realFname, $ext, $gunid, $type='access')
     {
         $token  = StoredFile::_createGunid();
-        $linkFname = "{$this->accessDir}/$token.$ext";
-        if(!file_exists($realFname)){
-            return PEAR::raiseError(
-                "BasicStor::bsAccess: real file not found ($realFname)",
-                GBERR_FILEIO);
-        }
-        if(! @symlink($realFname, $linkFname)){
-            return PEAR::raiseError(
-                "BasicStor::bsAccess: symlink create failed ($linkFname)",
-                GBERR_FILEIO);
+        if(!is_null($realFname)){
+            $linkFname = "{$this->accessDir}/$token.$ext";
+            if(!file_exists($realFname)){
+                return PEAR::raiseError(
+                    "BasicStor::bsAccess: real file not found ($realFname)",
+                    GBERR_FILEIO);
+            }
+            if(! @symlink($realFname, $linkFname)){
+                return PEAR::raiseError(
+                    "BasicStor::bsAccess: symlink create failed ($linkFname)",
+                    GBERR_FILEIO);
+            }
         }
         $this->dbc->query("BEGIN");
         $res = $this->dbc->query("
@@ -334,7 +356,7 @@ class BasicStor extends Alib{
         $ext = $acc['ext'];
         $gunid = StoredFile::_normalizeGunid($acc['gunid']);
         $linkFname = "{$this->accessDir}/$token.$ext";
-        if(! @unlink($linkFname)){
+        if(file_exists($linkFname)) if(! @unlink($linkFname)){
             return PEAR::raiseError(
                 "BasicStor::bsRelease: unlink failed ($linkFname)",
                 GBERR_FILEIO);
@@ -839,7 +861,7 @@ class BasicStor extends Alib{
         return TRUE;
     }  
     
-    /* ==================================================== "private" methods */
+    /* ================================================== "protected" methods */
     /**
      *  Check authorization - auxiliary method
      *
@@ -944,7 +966,51 @@ class BasicStor extends Alib{
         return TRUE;
     }
 
-    /* ------------------------------------------ redefined "private" methods */
+    /**
+     *  Returns if gunid is free
+     *
+     */
+    function _gunidIsFree($gunid)
+    {
+        $cnt = $this->dbc->getOne("
+            SELECT count(*) FROM {$this->filesTable}
+            WHERE gunid=x'{$this->gunid}'::bigint
+        ");
+        if(PEAR::isError($cnt)) return $cnt;
+        if($cnt > 0) return FALSE;
+        return TRUE;
+    }    
+
+    /**
+     *  Set playlist edit flag
+     *
+     *  @param playlistId string, playlist global unique ID
+     *  @param val boolean, set/clear of edit flag
+     *  @return boolean, previous state
+     */
+    function _setEditFlag($playlistId, $val=TRUE)
+    {
+        $ac =& StoredFile::recallByGunid($this, $playlistId);
+        if(PEAR::isError($ac)) return $ac;
+        $state = $ac->_getState();
+        if($val){ $ac->setState('edited'); }
+        else{ $ac->setState('ready'); }
+        return ($state == 'edited');
+    }
+
+    /**
+     *  Check if playlist is marked as edited
+     *
+     *  @param playlistId string, playlist global unique ID
+     *  @return boolean
+     */
+    function _isEdited($playlistId)
+    {
+        $ac =& StoredFile::recallByGunid($this, $playlistId);
+        return $ac->isEdited($playlistId);
+    }
+
+    /* ---------------------------------------- redefined "protected" methods */
     /**
      *  Copy virtual file.<br>
      *  Redefined from parent class.
diff --git a/livesupport/modules/storageServer/var/GreenBox.php b/livesupport/modules/storageServer/var/GreenBox.php
index 2bdb76de1..758030ab2 100644
--- a/livesupport/modules/storageServer/var/GreenBox.php
+++ b/livesupport/modules/storageServer/var/GreenBox.php
@@ -23,7 +23,7 @@
  
  
     Author   : $Author: tomas $
-    Version  : $Revision: 1.31 $
+    Version  : $Revision: 1.32 $
     Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/storageServer/var/GreenBox.php,v $
 
 ------------------------------------------------------------------------------*/
@@ -35,7 +35,7 @@ require_once "BasicStor.php";
  *  LiveSupport file storage module
  *
  *  @author  $Author: tomas $
- *  @version $Revision: 1.31 $
+ *  @version $Revision: 1.32 $
  *  @see BasicStor
  */
 class GreenBox extends BasicStor{
@@ -109,6 +109,39 @@ class GreenBox extends BasicStor{
         return $oid;
     }
 
+
+    /**
+     *  Access stored file - increase access counter
+     *
+     *  @param id int, virt.file's local id
+     *  @param sessid string, session id
+     *  @return string access token
+     */
+    function accessFile($id, $sessid='')
+    {
+        if(($res = $this->_authorize('read', $id, $sessid)) !== TRUE)
+            return $res;
+        $gunid = $this->_gunidFromId($id);
+        $r = $this->bsAccess(NULL, '', $gunid, 'access');
+        if(PEAR::isError($r)){ return $r; }
+        $token = $r['token'];
+        return $token;
+    }
+
+    /**
+     *  Release stored file - decrease access counter
+     *
+     *  @param token string, access token
+     *  @param sessid string, session id
+     *  @return boolean
+     */
+    function releaseFile($token, $sessid='')
+    {
+        $r = $this->bsRelease($token, 'access');
+        if(PEAR::isError($r)){ return $r; }
+        return FALSE;
+    }
+
     /**
      *  Analyze media file for internal metadata information
      *
@@ -355,31 +388,326 @@ class GreenBox extends BasicStor{
      *  Create a new empty playlist.
      *
      *  @param sessid string, session ID
-     *  @param playlistId string, playlist global unique ID
+     *  @param gunid string, playlist global unique ID
      *  @param fname string, human readable menmonic file name
      *  @return string, playlist global unique ID
      */
-    function createPlaylist($sessid, $playlistId, $fname)
+    function createPlaylist($sessid, $gunid, $fname)
     {
         require_once"LocStor.php";
         $lc =& new LocStor($this->dbc, $this->config);
-        return $lc->createPlaylist($sessid, $playlistId, $fname);
+        return $lc->createPlaylist($sessid, $gunid, $fname);
     }
 
+    /**
+     *  Return playlist as XML string
+     *
+     *  @param sessid string, session ID
+     *  @param id int, local object id
+     *  @return string, XML
+     */
+    function getPlaylistXml($sessid, $id)
+    {
+        return $this->getMdata($id, $sessid);
+    }
+
+    /**
+     *  Return playlist as hierarchical PHP hash-array
+     *
+     *  @param sessid string, session ID
+     *  @param id int, local object id
+     *  @return array
+     */
+    function getPlaylistArray($sessid, $id)
+    {
+        $gunid = $this->_gunidFromId($id);
+        $pl =& StoredFile::recall($this, $id);
+        if(PEAR::isError($pl)){ return $pl; }
+        $gunid = $pl->gunid;
+        return $pl->md->genPhpArray();
+    }
+
+    /**
+     *  Mark playlist as edited and return edit token
+     *
+     *  @param sessid string, session ID
+     *  @param id int, local object id
+     *  @return string, playlist access token
+     */
+    function lockPlaylistForEdit($sessid, $id)
+    {
+        $gunid = $this->_gunidFromId($id);
+        require_once"LocStor.php";
+        $lc =& new LocStor($this->dbc, $this->config);
+        $res = $lc->editPlaylist($sessid, $gunid);
+        if(PEAR::isError($res)) return $res;
+        return $res['token'];
+    }
+
+    /**
+     *  Release token, regenerate XML from DB and clear edit flag.
+     *
+     *  @param sessid string, session ID
+     *  @param token string, playlist access token
+     *  @return string gunid
+     */
+    function releaseLockedPlaylist($sessid, $token)
+    {
+        $gunid = $this->bsCloseDownload($token, 'metadata');
+        if(PEAR::isError($gunid)) return $gunid;
+        $ac =& StoredFile::recallByGunid($this, $gunid);
+        if(PEAR::isError($ac)){ return $ac; }
+        $r = $ac->md->regenerateXmlFile();
+        if(PEAR::isError($r)) return $r;
+        $this->_setEditFlag($gunid, FALSE);
+        return $gunid;
+    }
+
+    /**
+     *  Add audioclip specified by gunid to the playlist
+     *
+     *  @param sessid string, session ID
+     *  @param token string, playlist access token
+     *  @param acGunid string, global unique ID of added file
+     *  @return string, generated playlistElement gunid
+     */
+    function addAudioClipToPlaylist($sessid, $token, $acGunid)
+    {
+        $plGunid = $this->_gunidFromToken($token, 'download');
+        if(PEAR::isError($plGunid)) return $plGunid;
+        if(is_null($plGunid)){
+            return PEAR::raiseError(
+                "GreenBox::addClipToPlaylist: invalid token"
+            );
+        }
+        $pl =& StoredFile::recallByGunid($this, $plGunid);
+        if(PEAR::isError($pl)){ return $pl; }
+        $id = $pl->getId();
+        // get playlist length and record id:
+        $r = $pl->md->getMetadataEl('dcterms:extent');
+        if(PEAR::isError($r)){ return $r; }
+        $plLen = $r[0]['value'];
+        $plLenMid = $r[0]['mid'];
+        if(is_null($plLen)) $plLen = '00:00:00.000000';
+
+        // get audioClip legth and title
+        $ac =& StoredFile::recallByGunid($this, $acGunid);
+        if(PEAR::isError($ac)){ return $ac; }
+        $r = $ac->md->getMetadataEl('dcterms:extent');
+        if(PEAR::isError($r)){ return $r; }
+        $acLen = $r[0]['value'];
+        $r = $ac->md->getMetadataEl('dc:title');
+        if(PEAR::isError($r)){ return $r; }
+        $acTit = $r[0]['value'];
+        
+        // get main playlist container
+        $r = $pl->md->getMetadataEl('playlist');
+        if(PEAR::isError($r)){ return $r; }
+        $parid = $r[0]['mid'];
+        if(is_null($parid)){
+            return PEAR::raiseError(
+                "GreenBox::addClipToPlaylist: can't find main container"
+            );
+        }
+        // get metadata container (optionally insert it)
+        $r = $pl->md->getMetadataEl('metadata');
+        if(PEAR::isError($r)){ return $r; }
+        $metaParid = $r[0]['mid'];
+        if(is_null($metaParid)){
+            $r = $pl->md->insertMetadataEl($parid, 'metadata');
+            if(PEAR::isError($r)){ return $r; }
+            $metaParid = $r;
+        }
+        
+        // insert new palylist element
+        $r = $pl->md->insertMetadataEl($parid, 'playlistElement');
+        if(PEAR::isError($r)){ return $r; }
+        $plElId = $r;
+        $plElGunid = StoredFile::_createGunid();
+        $r = $pl->md->insertMetadataEl($plElId, 'id', $plElGunid, 'A');
+        if(PEAR::isError($r)){ return $r; }
+        $r = $pl->md->insertMetadataEl(
+            $plElId, 'relativeOffset', $plLen, 'A');
+        if(PEAR::isError($r)){ return $r; }
+        $r = $pl->md->insertMetadataEl($plElId, 'audioClip');
+        if(PEAR::isError($r)){ return $r; }
+        $acId = $r;
+        $r = $pl->md->insertMetadataEl($acId, 'id', $acGunid, 'A');
+        if(PEAR::isError($r)){ return $r; }
+        $r = $pl->md->insertMetadataEl($acId, 'playlength', $acLen, 'A');
+        if(PEAR::isError($r)){ return $r; }
+        $r = $pl->md->insertMetadataEl($acId, 'title', $acTit, 'A');
+        if(PEAR::isError($r)){ return $r; }
+        // calculate and insert total length:
+        $newPlLen = $this->_secsToPlTime(
+            $this->_plTimeToSecs($plLen) + $this->_plTimeToSecs($acLen)
+        );
+        if(is_null($plLenMid)){
+            $r = $pl->md->insertMetadataEl(
+                $metaParid, 'dcterms:extent', $newPlLen);
+        }else{
+            $r = $pl->md->setMetadataEl($plLenMid, $newPlLen);
+        }
+        if(PEAR::isError($r)){ return $r; }
+        // set access to audio clip:
+        $r = $this->bsAccess(NULL, '', $acGunid, 'access');
+        if(PEAR::isError($r)){ return $r; }
+        $acToken = $r['token'];
+        // insert token attribute:
+        $r = $pl->md->insertMetadataEl($acId, 'accessToken', $acToken, 'A');
+        if(PEAR::isError($r)){ return $r; }
+        return $plElGunid;
+    }
+
+    /**
+     *  Remove audioclip from playlist
+     *
+     *  @param sessid string, session ID
+     *  @param token string, playlist access token
+     *  @param plElGunid string, global unique ID of deleted playlistElement
+     *  @return boolean
+     */
+    function delAudioClipFromPlaylist($sessid, $token, $plElGunid)
+    {
+        $plGunid = $this->_gunidFromToken($token, 'download');
+        if(PEAR::isError($plGunid)) return $plGunid;
+        if(is_null($plGunid)){
+            return PEAR::raiseError(
+                "GreenBox::addClipToPlaylist: invalid token"
+            );
+        }
+        $pl =& StoredFile::recallByGunid($this, $plGunid);
+        if(PEAR::isError($pl)){ return $pl; }
+        $id = $pl->getId();
+
+        // get main playlist container:
+        $r = $pl->md->getMetadataEl('playlist');
+        if(PEAR::isError($r)){ return $r; }
+        $parid = $r[0]['mid'];
+        if(is_null($parid)){
+            return PEAR::raiseError(
+                "GreenBox::addClipToPlaylist: can't find main container"
+            );
+        }
+        // get playlist length and record id:
+        $r = $pl->md->getMetadataEl('dcterms:extent');
+        if(PEAR::isError($r)){ return $r; }
+        $plLen = $r[0]['value'];
+        $plLenMid = $r[0]['mid'];
+        // get array of playlist elements:
+        $plElArr = $pl->md->getMetadataEl('playlistElement', $parid);
+        if(PEAR::isError($plElArr)){ return $plElArr; }
+        $found = FALSE;
+        foreach($plElArr as $el){
+            $plElGunidArr = $pl->md->getMetadataEl('id', $el['mid']);
+            if(PEAR::isError($plElGunidArr)){ return $plElGunidArr; }
+            // select playlist element to remove
+            if($plElGunidArr[0]['value'] == $plElGunid){
+                $acArr = $pl->md->getMetadataEl('audioClip', $el['mid']);
+                if(PEAR::isError($acArr)){ return $acArr; }
+                $acLenArr = $pl->md->getMetadataEl('playlength', $acArr[0]['mid']);
+                if(PEAR::isError($acLenArr)){ return $acLenArr; }
+                $acLen = $acLenArr[0]['value'];
+                $acTokArr = $pl->md->getMetadataEl('accessToken', $acArr[0]['mid']);
+                if(PEAR::isError($acTokArr)){ return $acTokArr; }
+                $acToken = $acTokArr[0]['value'];
+                // remove playlist element:
+                $r = $pl->md->setMetadataEl($el['mid'], NULL);
+                if(PEAR::isError($r)){ return $r; }
+                // release audioClip:
+                $r = $this->bsRelease($acToken, 'access');
+                if(PEAR::isError($r)){ return $r; }
+                $found = TRUE;
+                continue;
+            }
+            if($found){
+                // corect relative offsets in remaining elements:
+                $acOffArr = $pl->md->getMetadataEl('relativeOffset', $el['mid']);
+                if(PEAR::isError($acOffArr)){ return $acOffArr; }
+                $newOff = $this->_secsToPlTime(
+                    $this->_plTimeToSecs($acOffArr[0]['value'])
+                    -
+                    $this->_plTimeToSecs($acLen)
+                );
+                $r = $pl->md->setMetadataEl($acOffArr[0]['mid'], $newOff);
+                if(PEAR::isError($r)){ return $r; }
+            }
+        }
+        // correct total length:
+        $newPlLen = $this->_secsToPlTime(
+            $this->_plTimeToSecs($plLen) - $this->_plTimeToSecs($acLen)
+        );
+        $r = $pl->md->setMetadataEl($plLenMid, $newPlLen);
+        if(PEAR::isError($r)){ return $r; }
+        return TRUE;
+    }
+
+    /**
+     *  RollBack playlist changes to the locked state
+     *
+     *  @param sessid string, session ID
+     *  @param token string, playlist access token
+     *  @return string gunid of playlist
+     */
+    function revertEditedPlaylist($sessid, $token)
+    {
+        $gunid = $this->bsCloseDownload($token, 'metadata');
+        if(PEAR::isError($gunid)) return $gunid;
+        $ac =& StoredFile::recallByGunid($this, $gunid);
+        if(PEAR::isError($ac)){ return $ac; }
+        $id = $ac->getId();
+        $mdata = $ac->getMetaData();
+        if(PEAR::isError($mdata)){ return $mdata; }
+        $res = $ac->replaceMetaData($mdata, 'string');
+        if(PEAR::isError($res)){ return $res; }
+        $this->_setEditFlag($gunid, FALSE);
+        return $gunid;
+    }
+
+    /**
+     *  Convert playlist time value to float seconds
+     *
+     *  @param plt string, playlist time value (HH:mm:ss.dddddd)
+     *  @return int, seconds
+     */
+    function _plTimeToSecs($plt)
+    {
+        $arr = split(':', $plt);
+        if(isset($arr[2])){ return ($arr[0]*60 + $arr[1])*60 + $arr[2]; }
+        if(isset($arr[1])){ return $arr[0]*60 + $arr[1]; }
+        return $arr[0];
+    }
+
+    /**
+     *  Convert float seconds value to playlist time format
+     *
+     *  @param s0 int, seconds
+     *  @return string, time in playlist time format (HH:mm:ss.dddddd)
+     */
+    function _secsToPlTime($s0)
+    {
+        $m = intval($s0 / 60);
+        $r = $s0 - $m*60;
+        $h = $m  / 60;
+        $m = $m  % 60;
+        return sprintf("%02d:%02d:%09.6f", $h, $m, $r);
+    }
+
+    /* ---------- */
     /**
      *  Open a Playlist metafile for editing.
      *  Open readable URL and mark file as beeing edited.
      *
      *  @param sessid string, session ID
-     *  @param playlistId string, playlist global unique ID
+     *  @param gunid string, playlist global unique ID
      *  @return struct
      *      {url:readable URL for HTTP GET, token:access token, chsum:checksum}
      */
-    function editPlaylist($sessid, $playlistId)
+    function editPlaylist($sessid, $gunid)
     {
         require_once"LocStor.php";
         $lc =& new LocStor($this->dbc, $this->config);
-        return $lc->editPlaylist($sessid, $playlistId);
+        return $lc->editPlaylist($sessid, $gunid);
     }
     
     /**
@@ -388,7 +716,7 @@ class GreenBox extends BasicStor{
      *  @param sessid string, session ID
      *  @param playlistToken string, playlist access token
      *  @param newPlaylist string, new playlist as XML string
-     *  @return string, playlistId
+     *  @return string, gunid
      */
     function savePlaylist($sessid, $playlistToken, $newPlaylist)
     {
@@ -401,28 +729,28 @@ class GreenBox extends BasicStor{
      *  Delete a Playlist metafile.
      *
      *  @param sessid string, session ID
-     *  @param playlistId string, playlist global unique ID
+     *  @param gunid string, playlist global unique ID
      *  @return boolean
      */
-    function deletePlaylist($sessid, $playlistId)
+    function deletePlaylist($sessid, $gunid)
     {
         require_once"LocStor.php";
         $lc =& new LocStor($this->dbc, $this->config);
-        return $lc->deletePlaylist($sessid, $playlistId);
+        return $lc->deletePlaylist($sessid, $gunid);
     }
     
     /**
      *  Check whether a Playlist metafile with the given playlist ID exists.
      *
      *  @param sessid string, session ID
-     *  @param playlistId string, playlist global unique ID
+     *  @param gunid string, playlist global unique ID
      *  @return boolean
      */
-    function existsPlaylist($sessid, $playlistId)
+    function existsPlaylist($sessid, $gunid)
     {
         require_once"LocStor.php";
         $lc =& new LocStor($this->dbc, $this->config);
-        return $lc->existsPlaylist($sessid, $playlistId);
+        return $lc->existsPlaylist($sessid, $gunid);
     }
 
     /**
@@ -431,14 +759,14 @@ class GreenBox extends BasicStor{
      *  beeing edited.
      *
      *  @param sessid string, session ID
-     *  @param playlistId string, playlist global unique ID
+     *  @param gunid string, playlist global unique ID
      *  @return boolean
      */
-    function playlistIsAvailable($sessid, $playlistId)
+    function playlistIsAvailable($sessid, $gunid)
     {
         require_once"LocStor.php";
         $lc =& new LocStor($this->dbc, $this->config);
-        return $lc->playlistIsAvailable($sessid, $playlistId);
+        return $lc->playlistIsAvailable($sessid, $gunid);
     }
     
     /* ============================================== methods for preferences */
@@ -569,6 +897,5 @@ class GreenBox extends BasicStor{
         return $pa;
     }
 
-    /* ==================================================== "private" methods */
 }
 ?>
\ No newline at end of file
diff --git a/livesupport/modules/storageServer/var/MetaData.php b/livesupport/modules/storageServer/var/MetaData.php
index 23650e238..942519e6d 100644
--- a/livesupport/modules/storageServer/var/MetaData.php
+++ b/livesupport/modules/storageServer/var/MetaData.php
@@ -23,7 +23,7 @@
  
  
     Author   : $Author: tomas $
-    Version  : $Revision: 1.17 $
+    Version  : $Revision: 1.18 $
     Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/storageServer/var/MetaData.php,v $
 
 ------------------------------------------------------------------------------*/
@@ -182,6 +182,89 @@ class MetaData{
             return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<metadata/>\n";
     }
 
+    /**
+     *  Get metadata element value and record id
+     *
+     *  @param category string, metadata element name
+     *  @param parid int, metadata record id of parent element
+     *  @return hash {mid: int - record id, value: metadata alue}
+     */
+    function getMetadataEl($category, $parid=NULL)
+    {
+        // handle predicate namespace shortcut
+        $a     = XML_Util::splitQualifiedName(strtolower($category));
+        if(PEAR::isError($a)) return $a;
+        $catNs = $a['namespace'];
+        $cat   = $a['localPart'];
+        $cond = "gunid=x'{$this->gunid}'::bigint AND predicate='$cat'";
+        if(!is_null($catNs)) $cond .= " AND predns='$catNs'";
+        if(!is_null($parid)) $cond .= " AND subjns='_I' AND subject='$parid'";
+        $sql = "
+            SELECT id as mid, object as value
+            FROM {$this->mdataTable}
+            WHERE $cond
+            ORDER BY id
+        ";
+        $all = $this->dbc->getAll($sql);
+        if(PEAR::isError($all)) return $all;
+        return $all;
+    }
+    
+    /**
+     *  Set metadata value / delete metadata record
+     *
+     *  @param mid int, metadata record id
+     *  @param value string, new metadata value (NULL for delete)
+     *  @return boolean
+     */
+    function setMetadataEl($mid, $value=NULL)
+    {
+        if(!is_null($value)){
+            $sql = "
+                UPDATE {$this->mdataTable}
+                SET object='$value'
+                WHERE id={$mid}
+            ";
+            $res = $this->dbc->query($sql);
+        }else{
+            $res = $this->deleteRecord($mid);
+        }
+        if(PEAR::isError($res)) return $res;
+        return TRUE;
+    }
+
+    /**
+     *  Insert new metadata element
+     *
+     *  @param parid int, metadata record id of parent element
+     *  @param category string, metadata element name
+     *  @param value string, new metadata value (NULL for delete)
+     *  @param predxml srring, 'T' | 'A' | 'N' (tag, attr., namespace)
+     *  @return int, new metadata record id
+     */
+    function insertMetadataEl($parid, $category, $value=NULL, $predxml='T')
+    {
+        $cnt = $this->dbc->getOne("
+            SELECT count(*) FROM {$this->mdataTable}
+            WHERE gunid=x'{$this->gunid}'::bigint AND id=$parid
+        ");
+        if(PEAR::isError($cnt)) return $cnt;
+        if($cnt < 1){
+            return PEAR::raiseError(
+                "MetaData::insertMetadataEl: container not found"
+            );
+        }
+        $a     = XML_Util::splitQualifiedName(strtolower($category));
+        if(PEAR::isError($a)) return $a;
+        $catNs = $a['namespace'];
+        $cat   = $a['localPart'];
+        $objns = (is_null($value) ? '_blank' : '_L' );
+        $nid= $this->storeRecord('_I', $parid, $catNs, $cat, $predxml,
+            $objns, $value);
+        if(PEAR::isError($nid)) return $nid;
+        return $nid;
+    }
+    
     /**
      *  Get metadata element value
      *
@@ -199,24 +282,7 @@ class MetaData{
      */
     function getMetadataValue($category, $lang=NULL, $objns='_L')
     {
-        // handle predicate namespace shortcut
-        $a     = XML_Util::splitQualifiedName(strtolower($category));
-        if(PEAR::isError($a)) return $a;
-        $catNs = $a['namespace'];
-        $cat   = $a['localPart'];
-        $cond = "
-                gunid=x'{$this->gunid}'::bigint AND objns='$objns' AND
-                predicate='$cat'
-        ";
-        if(!is_null($catNs)) $cond .= " AND predns='$catNs'";
-        $sql = "
-            SELECT id as mid, object as value
-            FROM {$this->mdataTable}
-            WHERE $cond
-            ORDER BY id
-        ";
-        $all = $this->dbc->getAll($sql);
-        if(PEAR::isError($all)) return $all;
+        $all = $this->getMetadataEl($category);
         $res = array();
         // add attributes to result
         foreach($all as $i=>$rec){
@@ -249,6 +315,7 @@ class MetaData{
     function setMetadataValue($category, $value, $lang=NULL, $mid=NULL,
         $container='metadata')
     {
+        // resolve aktual element:
         $rows   = $this->getMetadataValue($category, $lang);
         $aktual = NULL;
         if(count($rows)>1){
@@ -267,36 +334,22 @@ class MetaData{
             }
         }else $aktual = $rows[0];
         if(!is_null($aktual)){
-            if(!is_null($value)){
-                $sql = "
-                    UPDATE {$this->mdataTable}
-                    SET object='$value'
-                    WHERE id={$aktual['mid']}
-                ";
-                $res = $this->dbc->query($sql);
-            }else{
-                $res = $this->deleteRecord($aktual['mid']);
-            }
+            $res = $this->setMetadataEl($aktual['mid'], $value);
             if(PEAR::isError($res)) return $res;
         }else{
+            // resolve container:
             $contArr = $this->getMetadataValue($container, NULL, '_blank');
             if(PEAR::isError($contArr)) return $contArr;
-            $id = $contArr[0]['mid'];
-            if(is_null($id)){
+            $parid = $contArr[0]['mid'];
+            if(is_null($parid)){
                 return PEAR::raiseError(
                     "MetaData::setMdataValue: container ($container) not found"
                 );
             }
-            $a     = XML_Util::splitQualifiedName(strtolower($category));
-            if(PEAR::isError($a)) return $a;
-            $catNs = $a['namespace'];
-            $cat   = $a['localPart'];
-            $nid= $this->storeRecord('_I', $id, $catNs, $cat, $predxml='T',
-                '_L', $value);
+            $nid = $this->insertMetadataEl($parid, $category, $value);
             if(PEAR::isError($nid)) return $nid;
             if(!is_null($lang)){
-                $res= $this->storeRecord('_I', $nid, 'xml', 'lang', $predxml='A',
-                    '_L', $lang);
+                $res = $this->insertMetadataEl($nid, 'xml:lang', $lang, 'A');
                 if(PEAR::isError($res)) return $res;
             }
         }
@@ -546,6 +599,29 @@ class MetaData{
     }
     
     /* =========================================== XML reconstruction from db */
+    /**
+     *  Generate PHP array from metadata database
+     *
+     *  @return array with metadata tree
+     */
+    function genPhpArray()
+    {
+        $res = array();
+        $row = $this->dbc->getRow("
+            SELECT * FROM {$this->mdataTable}
+            WHERE gunid=x'{$this->gunid}'::bigint
+                AND subjns='_G' AND subject='{$this->gunid}'
+        ");
+        if(PEAR::isError($row)) return $row;
+        if(is_null($row)){
+        }else{
+            $node = $this->genXMLNode($row, FALSE);
+            if(PEAR::isError($node)) return $node;
+        }
+        $res = $node;
+        return $res;
+    }
+
     /**
      *  Generate XML document from metadata database
      *
@@ -580,24 +656,34 @@ class MetaData{
      *  Generate XML element from database
      *
      *  @param row array, hash with metadata record fields
+     *  @param genXML boolean, if TRUE generate XML else PHP array
      *  @return string, XML serialization of node
      */
-    function genXMLNode($row)
+    function genXMLNode($row, $genXML=TRUE)
     {
         if(DEBUG) echo"genXMLNode:\n";
         if(DEBUG) var_dump($row);
         extract($row);
-        $arr = $this->getSubrows($id);
+        $arr = $this->getSubrows($id, $genXML);
         if(PEAR::isError($arr)) return $arr;
         if(DEBUG) var_dump($arr);
         extract($arr);
-        $node = XML_Util::createTagFromArray(array(
-            'namespace' => $predns,
-            'localPart' => $predicate,
-            'attributes'=> $attrs,
-#            'content'   => $object." X ".$children,
-            'content'   => ($object == 'NULL' ? $children : $object),
-        ), FALSE);
+        if($genXML){
+            $node = XML_Util::createTagFromArray(array(
+                'namespace' => $predns,
+                'localPart' => $predicate,
+                'attributes'=> $attrs,
+                'content'   => ($object == 'NULL' ? $children : $object),
+            ), FALSE);
+        }else{
+            $node = array_merge(
+                array(
+                    'elementname'    => ($predns ? "$predns:" : '').$predicate,
+                    'content' => $object,
+                    'children'=> $children,
+                ), $arr
+            );
+        }
         return $node;
     }
 
@@ -606,12 +692,14 @@ class MetaData{
      *  one metadata record
      *
      *  @param parid int, local id of parent metadata record
+     *  @param genXML boolean, if TRUE generate XML else PHP array for
+     *      children
      *  @return hash with three fields:
      *      - attr hash, attributes
      *      - children array, child nodes
      *      - nSpaces hash, namespace definitions
      */
-    function getSubrows($parid)
+    function getSubrows($parid, $genXML=TRUE)
     {
         if(DEBUG) echo" getSubrows:\n";
         $qh = $this->dbc->query($q = "
@@ -641,7 +729,7 @@ class MetaData{
                 $attrs["{$predns}{$sep}{$predicate}"] = $object;
                 break;
             case"T":
-                $children[] = $this->genXMLNode($row);
+                $children[] = $this->genXMLNode($row, $genXML);
                 break;
             default:
                 return PEAR::raiseError(
@@ -649,7 +737,7 @@ class MetaData{
             } // switch
         }
         $qh->free();
-        $children   = join(" ", $children);
+        if($genXML) $children   = join(" ", $children);
         return compact('attrs', 'children', 'nSpaces');
     }