diff --git a/lib/oaipmh/COPYRIGHT.txt b/lib/oaipmh/COPYRIGHT.txt new file mode 100644 index 0000000..79e709e --- /dev/null +++ b/lib/oaipmh/COPYRIGHT.txt @@ -0,0 +1,14 @@ +Copyright (c) 2011 Jianfeng Li + +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 3 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, see . diff --git a/lib/oaipmh/ands_rifcs.php b/lib/oaipmh/ands_rifcs.php new file mode 100644 index 0000000..41625bd --- /dev/null +++ b/lib/oaipmh/ands_rifcs.php @@ -0,0 +1,260 @@ +oai_pmh = $ands_response_doc; + $this->working_node = $metadata_node; + $this->create_regObjects(); + } + + /** + * A worker function for easily adding a newly created node to current XML Doc. + * @param $mom_node Type: DOMElement. Node the new child will be attached to. + * @param $name Type: sting. The name of the child node is being added. + * @param $value Type: sting. The text content of the child node is being added. The default is ''. + * @return DOMElement. The added child node + */ + protected function addChild($mom_node,$name, $value='') { + return $this->oai_pmh->addChild($mom_node,$name, $value); + } + + /** Create a registryObjects node to hold individual registryObject's. + * This is only a holder node. + */ + protected function create_regObjects() { + $this->working_node = $this->oai_pmh->addChild($this->working_node,'registryObjects'); + $this->working_node->setAttribute('xmlns',"http://ands.org.au/standards/rif-cs/registryObjects"); + $this->working_node->setAttribute('xmlns:xsi',"http://www.w3.org/2001/XMLSchema-instance"); + $this->working_node->setAttribute('xsi:schemaLocation','http://ands.org.au/standards/rif-cs/registryObjects http://services.ands.org.au/documentation/rifcs/1.2.0/schema/registryObjects.xsd'); + } + + /** Create a single registryObject node. Each set has its own structure but they all have an attribute of group, a key node and an originatingSource node. The newly created node will be used as the working node. + * + * \param $group string, group attribute of the new registryObject node . + * \param $key string, key node, used as an identifier. + * \param $originatingSource string, an url of the data provider. + */ + protected function create_regObject($group, $key, $originatingSource) { + $regObj_node = $this->addChild($this->working_node,'registryObject'); + $regObj_node->setAttribute('group',$group); + $this->addChild($regObj_node,'key',$key); + $this->addChild($regObj_node,'originatingSource',$originatingSource); + $this->working_node = $regObj_node; + } + + /** RIF-CS node is the content node of RIF-CS metadata node which starts from regObjects. + * Each set supportted in RIF-CS has its own content model. The created node will be used as the + * root node of this record for following nodes will be created. + * + * \param $set_name string, the name of set. For ANDS, they are Activity, Party and Collection + * \param $set_type string, the type of set. For example, Activity can have project as a type. + */ + protected function create_rifcs_node($set_name, $set_type) { + $this->working_node = $this->addChild($this->working_node, $set_name); + $this->working_node->setAttribute('type', $set_type); + } + + /** + * Create a top level name node. + * @param $name_type string. Text for the types, can be either primary or abbreviated. Default: primary + * + * @return DOMElement $added_name_node. + * The newly created node, it will be used for further expansion by adding namePart. + */ + protected function create_name_node($name_type = 'primary') { + $c = $this->addChild($this->working_node, 'name'); + $c->setAttribute('type', $name_type); + return $c; + } + + /** + * Create a namePart of a name node. + * @param $name_node + * Type: DOMElement. Node of name_node created previously + * + * @param $value + * Type: string. Text fror this namePart + * + * @param $part_type Type: string, used for group:person record. Types can be: titile, given, family + * + */ + protected function create_namePart($name_node, $value, $part_type = '') { + $c = $this->addChild($name_node, 'namePart', $value); + if (!empty($part_type)) { + $c->setAttribute('type', $part_type); + } + } + + /** Create related object. One RIF-CS can have more than one related object nodes, + * each object is described by one node. + * \param $key + * Type: string. The identifier of the related object. + * \param $relation_type + * Type: string. Type of relationship. + * + */ + protected function create_relatedObject($key,$relation_type) { + $c = $this->addChild($this->working_node, 'relatedObject'); + $this->addChild($c,'key',$key); + $c = $this->addChild($c, 'relation'); + // Mimick ANDS with enpty value to get both tags for relation. Only for better display + // $c = $this->addChild($c, 'relation',' '); + $c->setAttribute('type', $relation_type); + } + + /** Create description node. One RIF-CS can have more than one description nodes. + * Each description node has only one description. + * \param $value Type: string. The content of the description. + * \param $des_type Type: string. Type of the description. Types can be brief, full, acessRights and note. Default is 'brief'. + */ + protected function create_description_node($value, $des_type='brief') { + $c = $this->addChild($this->working_node, 'description', $value); + $c->setAttribute('type', $des_type); + } + + /** Create local or other type of identifier inside of RIF-CS metadata node + * \param $key + * Type string. The indentifier itself. + * \param $i_type + * Type string. Type of identifier. Can be abn, uri, local, etc.. Default is local. + */ + protected function create_identifier_node($key, $i_type='local') { + $c = $this->addChild($this->working_node, 'identifier',$key); + $c->setAttribute('type', $i_type); + } + + /** Location node is a holder node for either address or spatial nodes + * \return DOMElement node, for adding address or spatial nodes. + */ + protected function create_location_node() { + return $this->addChild($this->working_node, 'location'); + } + + /** Address node is a holder node for phiscal or electrical nodes. + * \param $location_node Type: DOMElement. Location node created previously. + * \return DOMElement + */ + protected function create_address_node($location_node) { + return $this->addChild($location_node, 'address'); + } + + /** Electrical address node. Used for email, url, etc + * \param $addr_node Type: DOMElement. Previously created address node. + * \param $e_node Type: string. The content of the adding node. + * \param $e_type Type: string. Default is email. + */ + protected function create_e_node($addr_node, $e_node, $e_type = 'email') { + $c = $this->addChild($addr_node, 'electronic'); + $c->setAttribute('type', $e_type); + $this->addChild($c,'value',$e_node); + } + + /** Physical node is a holder node for phone or fax nodes. + * \param $addr_node Type: DOMelement. Address node created before to which the new phiscial->addressPart will be attached. + * \param $number Type: string. Telephone or fax number as a string. + * \param $fone_fax Type: string. Either telehoneNumber or faxNumber. + */ + protected function create_physcial_fone_fax($addr_node, $number,$fone_fax='telephoneNumber') { + $c = $this->addChild($addr_node, 'physical'); + $c = $this->addChild($c, 'addressPart', $number); + $c->setAttribute('type', $fone_fax); + } + + /** create address node under location node, either streetAddress or postalAddress. + * But they are in text (one block) format. + * \param $addr_node Type: DOMelement. Address node created before to which the new phiscial->addressPart will be attached. + * \param $txt_addr string, full street address in text block format + * \param $phys_type string, default is 'streetAddress', can be 'postalAddress' + */ + protected function create_physcial_addr_txt($addr_node, $txt_addr,$phys_type='streetAddress') { + $c = $this->addChild($addr_node, 'physical'); + $c->setAttribute('type', $phys_type); + $c = $this->addChild($c, 'addressPart', $txt_addr); + $c->setAttribute('type', 'text'); + } + + /** Create spatial node under a location node. + * \param $location_node Type: DOMElement. Location node where spatial node is being added to. + * \param $value Type: string. The value of spatial information. Default is local latitude and longitude. + * \param $sp_type Type: string. Type of spaitial informaion. Default is kmlPolyCoords. + */ + protected function create_spatial_node($location_node, $value = '138.6396,-34.97063', $sp_type = 'kmlPolyCoords') { + $c = $this->addChild($location_node, 'spatial',$value); + $c->setAttribute('type',$sp_type); + } + + /** Create temporal coverage node for collection or activity records. + * \param $values Type: 2-D array. The values of temporal coverage. It can has maximal two elements: one from 'dateFrom' and another for 'dateTo'. + * Either can be ommited according to RIF-CS schema. Each element of $values is an array and has keys: date, type and format. + * ['date'] is a string represents date. It has to be in W3CDTF or UTC format. + * ['type'] has to be either 'dateFrom' or 'dateTo'. + * ['format'] is optional and its default is 'W3CDTF'. UTC format requires date has to be in UTC: dateTtimeZ. + * It throws an exception if the input parameter is not an array. + */ + protected function create_coverage_tempo($values) { + // Non array is not acceptable. + if (!is_array($values)) { throw new Exception('The input of temporal coverage has to be an array of arraies with keys.');} + $c = $this->addChild($this->working_node,'coverage'); + $t = $this->addChild($c,'temporal'); + foreach($values as $value) $this->create_coverage_tempo_date($t, $value); + } + + /** Create temporal coverage node for collection or activity records. + * \param $t Type: DOMElement. The \\\ node to which \ nodes will be attached to. + * \param $value Type: array. The value of temporal coverage. It has maxmimal three elements with keys: type, date and format. + * It throws an exception if the input parameter is not an array. + * \see create_coverage_tempo + */ + private function create_coverage_tempo_date($t, $value) { + if (!is_array($value)) { throw new Exception('The input of temporal coverage has to be an array with keys.');} + $d = $this->addChild($t,'date',$value['date']); + $d->setAttribute('type',$value['type']); + if (isset($value['format'])) $d->setAttribute('dateFormat',$value['format']); + else $d->setAttribute('dateFormat','W3CDTF'); + } + + /** Create a subject node for a researcher, project, project, etc + * \param $value Type: string. A string representing the new namePart. + * \param $subject_type Type: string. A string representing the type of subject. The default value is anzsrc-for. + */ + protected function create_subject_node($value, $subject_type = 'anzsrc-for') { + if (empty($value)) return; + $c = $this->addChild($this->working_node,'subject',$value); + $c->setAttribute('type',$subject_type); + } +} // end of class ANDS_RIFCS + diff --git a/lib/oaipmh/ands_tpa.php b/lib/oaipmh/ands_tpa.php new file mode 100644 index 0000000..e86c031 --- /dev/null +++ b/lib/oaipmh/ands_tpa.php @@ -0,0 +1,349 @@ +create_metadata($cur_record); + * $obj_node = new ANDS_TPA($outputObj, $metadata_node, $db); + * try { + * $obj_node->create_obj_node($record[$SQL['set']], $identifier); + * } catch (Exception $e) { + * echo 'Caught exception: ', $e->getMessage(), " when adding $identifier\n"; + * } + * \endcode + * \see Code in action can be seen in record_rif.php + */ + +class ANDS_TPA extends ANDS_RIFCS { + //! Type: PDO. The database connection of the data source. + //! \see __construct. + private $db; + + /** + * Constructor + * The first two parameters are used by its parent class ANDS_RIFCS. The third is its own private property. + * + * \param $ands_response_doc ANDS_Response_XML. A XML Doc acts as the parent node. + * \param $metadata_node DOMElement. The meta node which all subsequent nodes will be added to. + * \param $db Type: PDO. The database connection of the data source. + */ + function __construct($ands_response_doc, $metadata_node, $db) { + parent::__construct($ands_response_doc, $metadata_node); + $this->db = $db; + } + + + /** + * This is the general entrence of creating actual content. It calls different functions for different type of RIF-CS model. + * When anything goes wrong, e.g. found no record, or $set_name is not recognised, an exception will be thrown. + * And for this implementation, data are stored in a database therefore a PDO is needed. But the source can be any. + * + * \param $set_name Type: string. The name of set is going to be created. Can be one of activity, collection or party. + * \param $key Type: string. The main identifier used in ANDS system. There can be other identifier. + * + * \see create_activity, create_collection, create_party + */ + function create_obj_node($set_name, $key) { + $db = $this->db; + $set_name = strtolower($set_name); + if (in_array($set_name,prepare_set_names())) { + try { + // Get ori_id and which the original table is: + $query = "select ori_table_name, ori_id from oai_headers where oai_identifier = '".$key."'"; + $res = exec_pdo_query($db, $query); + $record = $res->fetch(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + echo "$key returned no record.\n"; + echo $e->getMessage(); + } + + $processor = 'create_'.substr($set_name,6); + $this->create_regObject(REG_OBJ_GROUP, $key, MY_URI); + $this->$processor($record['ori_table_name'],$record['ori_id']); + $this->create_identifier_node($key); + $this->create_identifier_node('table='.$record['ori_table_name'].'+id='.$record['ori_id']); + } else { + throw new Exception('Wrong set name was used: '.$set_name); + } + } + + /** The processor for creating metadata node of Activity. Called from create_obj_node. + * \param $table_name Type: string. The table name will be used to retrieve data from. + * \param $id_project Type: integer. Internal project id associated to this activity-project. + * \see Function create_obj_node. + */ + private function create_activity($table_name, $id_project) { + $db = $this->db; +# // Get ori_id and which the original table is: +# $query = "select ori_table_name, ori_id from oai_headers where oai_identifier = '".$key."'"; +# $res = exec_pdo_query($db, $query); +# $record = $res->fetch(PDO::FETCH_ASSOC); +# // $id_project will e used later, so save it: +# $id_project = $record['ori_id']; + // Get the content using the previously obtained infor: + $query = sprintf("select inter_no,start_date, end_date,pub_descrip from %s where id_project = %s",$table_name,$id_project); + + try { + $res = exec_pdo_query($db,$query); + $record = $res->fetch(PDO::FETCH_ASSOC); + } catch (Exception $e) { + echo $e->getMessage(); + } + + $this->create_rifcs_node('activity','project'); + $c = $this->create_name_node(); + $this->create_namePart($c,'The Plant Accelerator Project '.$record['inter_no']); +// Test codes for rich format. +# // \n works +# $this->create_description_node(sprintf("Line one:%s,\nLine two:%s.\nThird",'a','b')); + $this->create_description_node(str_replace("\r\n","\n",$record['pub_descrip'])); + + $this->create_description_node('The experiment was carried out between '.$record['start_date'].' and '.$record['end_date'],'note'); + $query = sprintf("select idr,stype from list_prj_ids_v2(%d) where stype in ('dataset','person')",$id_project); + // echo $query; + try { + $res = $db->query($query,PDO::FETCH_ASSOC); + if ($res==false) { + throw new Exception($query."\nIt found nothing.\n"); + } + foreach ($res as $record) { + switch ($record['stype']) { + case 'dataset': + $this->create_relatedObject($record['idr'],'hasOutput'); + break; + case 'person': + $this->create_relatedObject($record['idr'],'isManagedBy'); + break; + } + } + // The Plant Accelerator always participates in Activity + $this->create_relatedObject('0874ad60-ab4d-11df-aebd-0002a5d5c51b','hasParticipant'); + } catch (PDOException $e) { + process_pdo_error($query, $e); + }// end of try-catch block + } // end of function create_activity($key, $id_project) + + /** The processor for creating metadata node of Collection. Called from create_obj_node. + * \param $table_name Type: string. The table name will be used to retrieve data from. + * \param $id_collect Type: integer. Internal collection id associated to this collection-dataset. + * \see Function create_obj_node. + */ + private function create_collection($table_name, $id_collect) { + $db = $this->db; + try { + $query = sprintf("select plant,variety,start_date,end_date,img_freq,anzsrc from %s where id_collect = %s",$table_name,$id_collect); + $res = exec_pdo_query($db, $query); + $dataset = $res->fetch(PDO::FETCH_ASSOC); + + $res = exec_pdo_query($db, $query); + $record = $res->fetch(PDO::FETCH_ASSOC); + + $query = 'select id_rep, inter_no, id_project from tpa_project_ids where id_collect = '.$id_collect; + $res = exec_pdo_query($db, $query); + $prj_info = $res->fetch(PDO::FETCH_ASSOC); + + $query = 'select email from tpa_person where id_rep = '.$prj_info['id_rep']; + $res = exec_pdo_query($db, $query); + $email = $res->fetch(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + echo $query.' was failed\n'; + echo $e->getMessage(); + } + + $this->create_rifcs_node('collection','dataset'); + // Get the project inter_no as the name of this dataset + $c = $this->create_name_node(); + $this->create_namePart($c,'Data set of Plant Accelerator Project '.$prj_info['inter_no']); + + // locatin node: contact person + $l_node = $this->create_location_node(); + $a_node = $this->create_address_node($l_node); + $this->create_e_node($a_node, $email['email']); + // location node: TPA's physical address + $l_node = $this->create_location_node(); + $a_node = $this->create_address_node($l_node); + $this->create_physcial_addr_txt($a_node, 'The Plant Accelerator, Hartley Grove, Urrbrae, SA 5064') ; + // Temporal coverage of colletion + $dates = array(array('date'=>$dataset['start_date'],'type'=>'dateFrom'),array('date'=>$dataset['end_date'],'type'=>'dateTo')); + $this->create_coverage_tempo($dates); + // subject + $this->create_subject_node($dataset['aznsrc']); + // relatedOjbects + $query = sprintf("select idr,stype from list_prj_ids_v2(%d) where stype in ('project','person')",$prj_info['id_project']); + try { + $res = $db->query($query,PDO::FETCH_ASSOC); + if ($res==false) { + throw new Exception($query."\nIt found nothing.\n"); + } + foreach ($res as $record) { + switch ($record['stype']) { + case 'project': + $this->create_relatedObject($record['idr'],'isOutputOf'); + break; + case 'person': + $this->create_relatedObject($record['idr'],'isOwnedBy'); + break; + } + } + } catch (PDOException $e) { + process_pdo_error($query, $e); + }// end of try-catch block + + // right of accessing + $this->create_description_node('For information on rights and access to this dataset, please contact the owner.','accessRights'); + + // image data: + $imgs = ''; $ex_conf = ''; + $dic = array('im_type_rgb'=>'RGB','im_type_nir'=>'NIR','im_type_fir'=>'FIR','im_type_nir_roots'=>'NIR Roots','im_type_fluo'=>'Fluorescence'); + $query = 'select im_type_rgb,im_type_nir,im_type_fir,im_type_nir_roots,im_type_fluo, lines, treatments, replicates, total from ands_collection where id_collect = '. $id_collect; + $res = $db->query($query,PDO::FETCH_ASSOC); + if ($res==false) { + throw new Exception($query."\nIt found nothing.\n"); + } + $info = $res->fetch(); + foreach ($info as $item => $v) { + switch ($item) { + case 'im_type_rgb': + case 'im_type_nir': + case 'im_type_fir': + case 'im_type_nir_roots': + case 'im_type_fluo': + if (!empty($v)) { $imgs .= $dic[$item].', '; } + break; + default: + if (!empty($v)) { $ex_conf .= ' '.$item.' = '.$v.', '; } + break; + } + } + if (empty($imgs)) $imgs = "Images data of RGB, FIR, NIR, NIR Roots and Fluorescence cameras., "; + $imgs = substr($imgs,0,-2); + if (!empty($ex_conf)) $imgs = $imgs."\n".substr($ex_conf,0,-2); + $this->create_description_node($imgs); + // imaging frequency + $this->create_description_node('Imaging frequency: '.$dataset['img_freq'],'note'); + } // end of function create_collection($key,$id_collect) + + /** The processor for creating metadata node of Party. Called from create_obj_node. As party-person is different to party-group, there are two sub-functions are called accordingly. + * \param $table_name Type: string. The table name will be used to retrieve data from. + * \param $id_party Type: integer. Internal party id associated to this party. + * \see Function create_obj_node. + */ + private function create_party($table_name, $id_party) { + $db = $this->db; + $query = sprintf("SELECT set_type FROM oai_headers WHERE ori_table_name = '%s' AND ori_id = %s",$table_name,$id_party); + $res = exec_pdo_query($db, $query); + $party_type = $res->fetch(PDO::FETCH_ASSOC); + + if (in_array($party_type['set_type'],array('person','group'))) { + $this->create_rifcs_node('party',$party_type['set_type']); + + if ($party_type['set_type']=='person') { + $this->create_person($table_name, $id_party); + } elseif ($party_type['set_type']=='group') { + $this->create_group($table_name, $id_party); } + } else { + throw new Exception('Unsupported set_type: '.$party_type['set_type']); + } + } // end of function create_part($key,$id_party) + + /** The processor for creating metadata node of Party. Called from create_obj_node. As party-person is different to party-group, there are two sub-functions are called accordingly. + * \param $table_name Type: string. The table name will be used to retrieve data from. + * \param $id_party Type: integer. Internal party id associated to this party-person. + * \see Function create_party. + */ + private function create_person($table_name, $id_party) { + $db = $this->db; + $query = sprintf("SELECT id_org, title, first_name, family_name, tel, fax, email, www, address, post_code, city,state,country,duty FROM %s WHERE id_rep = %s",$table_name, $id_party); + $res = exec_pdo_query($db, $query); + $info = $res->fetch(PDO::FETCH_ASSOC); + $c = $this->create_name_node(); + if (!empty($info['title'])) $this->create_namePart($c,$info['title'],'title'); + $this->create_namePart($c,$info['family_name'],'family'); + $this->create_namePart($c,$info['first_name'],'given'); + + // locatin node: contact person + $l_node = $this->create_location_node(); + $a_node = $this->create_address_node($l_node); + $this->create_e_node($a_node, $info['email']); + if (!empty($info['www'])) $this->create_e_node($a_node, $info['www'],'url'); + $this->create_physcial_fone_fax($a_node, $info['tel'],'telephoneNumber'); + if (!empty($info['fax'])) $this->create_physcial_fone_fax($a_node, $info['fax'],'faxNumber'); + $add_txt = trim($info['address']).', '.$info['city'].' '.$info['state'].' '.$info['post_code'].', '.$info['country']; + // the strlength of ', , ' is 6 + if (strlen($add_txt)>6) $this->create_physcial_addr_txt($a_node,$add_txt); + + // related objects: + // their group: id_customer is a foreign key of tpa_organisation + $query = sprintf("SELECT get_identifier('tpa_organisation',%s)",$info['id_org']); + $res = exec_pdo_query($db, $query); + $info = $res->fetch(PDO::FETCH_NUM); + $this->create_relatedObject($info[0],'isMemberOf'); + + // their activities + $query = "SELECT list_persons_objs($id_party,'project')"; + $res = exec_pdo_query($db, $query); + $info = $res->fetch(PDO::FETCH_NUM); + foreach ($info as $item) { + $this->create_relatedObject($item,'isManagerOf'); + } + // their collections + $query = "SELECT list_persons_objs($id_party,'dataset')"; + $res = exec_pdo_query($db, $query); + $info = $res->fetch(PDO::FETCH_NUM); + foreach ($info as $item) { + $this->create_relatedObject($item,'isOwnerOf'); + } + } + + /** The processor for creating metadata node of Party. Called from create_obj_node. As party-person is different to party-group, there are two sub-functions are called accordingly. + * \param $table_name Type: string. The table name will be used to retrieve data from. + * \param $id_party Type: integer. Internal party id associated to this party-group. + * \see Function create_party. + */ + private function create_group($table_name, $id_party) { + $db = $this->db; + // echo 'table: ',$table_name,' party: ',$id_party,"\n"; + $query = sprintf("SELECT customer_name, abn, post_code, address, city, state, country, tel, fax, email, www, description FROM %s WHERE id_org = %s",$table_name, $id_party); + //echo $query; + $res = exec_pdo_query($db, $query); + $info = $res->fetch(PDO::FETCH_ASSOC); + $c = $this->create_name_node(); + $this->create_namePart($c,$info['customer_name']); + if (!empty($info['abn'])) $this->create_identifier_node($info['abn'],'abn'); + + if (!empty($info['description'])) $this->create_description_node($info['description']); + + $l_node = $this->create_location_node(); + $a_node = $this->create_address_node($l_node); + $this->create_physcial_fone_fax($a_node, $info['tel'],'telephoneNumber'); + $this->create_physcial_fone_fax($a_node, $info['fax'],'faxNumber'); + $add_txt = trim($info['address']).', '.$info['city'].' '.$info['state'].' '.$info['post_code'].', '.$info['country']; + $this->create_physcial_addr_txt($a_node,$add_txt); + + // related objects: + // their members: + $query = "SELECT list_pub_members($id_party)"; + $res = exec_pdo_query($db, $query); + $info = $res->fetch(PDO::FETCH_NUM); + foreach ($info as $item) { + $this->create_relatedObject($item,'hasMember'); + } + } +} // end of class ANDS_TPA + diff --git a/lib/oaipmh/getrecord.php b/lib/oaipmh/getrecord.php new file mode 100644 index 0000000..2c8cc2b --- /dev/null +++ b/lib/oaipmh/getrecord.php @@ -0,0 +1,80 @@ +$metadataPrefix and $identifier need to be provided through global array variable $args + * by their indexes 'metadataPrefix' and 'identifier'. + * The reset of information will be extracted from database based those two parameters. + */ + +debug_message("\nI am debuging". __FILE__) ; + +$metadataPrefix = $args['metadataPrefix']; +// myhandler is a php file which will be included to generate metadata node. +// $inc_record = $METADATAFORMATS[$metadataPrefix]['myhandler']; + +if (is_array($METADATAFORMATS[$metadataPrefix]) + && isset($METADATAFORMATS[$metadataPrefix]['myhandler'])) { + $inc_record = $METADATAFORMATS[$metadataPrefix]['myhandler']; +} else { + $errors[] = oai_error('cannotDisseminateFormat', 'metadataPrefix', $metadataPrefix); +} + +$identifier = $args['identifier']; +$query = selectallQuery($metadataPrefix, $identifier); + +debug_message("Query: $query") ; + +$res = $db->query($query); + +if ($res===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + die($db->errorInfo()); + } else { + $errors[] = oai_error('idDoesNotExist', '', $identifier); + } +} elseif (!$res->rowCount()) { // based on PHP manual, it might only work for some DBs + $errors[] = oai_error('idDoesNotExist', '', $identifier); +} + + +if (!empty($errors)) { + oai_exit(); +} + +$record = $res->fetch(PDO::FETCH_ASSOC); +if ($record===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + } + $errors[] = oai_error('idDoesNotExist', '', $identifier); +} + +$identifier = $record[$SQL['identifier']];; + +$datestamp = formatDatestamp($record[$SQL['datestamp']]); + +if (isset($record[$SQL['deleted']]) && ($record[$SQL['deleted']] == 'true') && + ($deletedRecord == 'transient' || $deletedRecord == 'persistent')) { + $status_deleted = TRUE; +} else { + $status_deleted = FALSE; +} + +$outputObj = new ANDS_Response_XML($args); +$cur_record = $outputObj->create_record(); +$cur_header = $outputObj->create_header($identifier, $datestamp,$record[$SQL['set']],$cur_record); +// return the metadata record itself +if (!$status_deleted) { + include($inc_record); // where the metadata node is generated. + create_metadata($outputObj, $cur_record, $identifier, $record[$SQL['set']], $db); +} else { + $cur_header->setAttribute("status","deleted"); +} +?> diff --git a/lib/oaipmh/identify.php b/lib/oaipmh/identify.php new file mode 100644 index 0000000..1811bff --- /dev/null +++ b/lib/oaipmh/identify.php @@ -0,0 +1,139 @@ + $val) { + $outputObj->add2_verbNode($key, $val); +} + +foreach($adminEmail as $val) { + $outputObj->add2_verbNode("adminEmail", $val); +} + +if(isset($compression)) { + foreach($compression as $val) { + $outputObj->add2_verbNode("compression", $val); + } +} + +// A description MAY be included. +// Use this if you choose to comply with a specific format of unique identifiers +// for items. +// See http://www.openarchives.org/OAI/2.0/guidelines-oai-identifier.htm +// for details + +// As they will not be changed, using string for simplicity. +$output = ''; +if ($show_identifier && $repositoryIdentifier && $delimiter && $sampleIdentifier) { + $output .= +' + + oai + '.$repositoryIdentifier.' + '.$delimiter.' + '.$sampleIdentifier.' + + '."\n"; +} + +// A description MAY be included. +// This example from arXiv.org is used by the e-prints community, please adjust +// see http://www.openarchives.org/OAI/2.0/guidelines-eprints.htm for details + +// To include, change 'false' to 'true'. +if (false) { + $output .= +' + + + Author self-archived e-prints + + + + + + '."\n"; +} + +// If you want to point harvesters to other repositories, you can list their +// base URLs. Usage of friends container is RECOMMENDED. +// see http://www.openarchives.org/OAI/2.0/guidelines-friends.htm +// for details + +// To include, change 'false' to 'true'. +if (false) { + $output .= +' + + http://naca.larc.nasa.gov/oai2.0/ + http://techreports.larc.nasa.gov/ltrs/oai2.0/ + http://physnet.uni-oldenburg.de/oai/oai2.php + http://cogprints.soton.ac.uk/perl/oai + http://ub.uni-duisburg.de:8080/cgi-oai/oai.pl + http://rocky.dlib.vt.edu/~jcdlpix/cgi-bin/OAI1.1/jcdlpix.pl + + '."\n"; +} + +// If you want to provide branding information, adjust accordingly. +// Usage of friends container is OPTIONAL. +// see http://www.openarchives.org/OAI/2.0/guidelines-branding.htm +// for details + +// To include, change 'false' to 'true'. +if (false) { + $output .= +' + + + http://my.site/icon.png + http://my.site/homepage.html + MySite(tm) + 88 + 31 + + http://some.where/DCrender.xsl + http://another.place/MARCrender.css + + '."\n"; +} + +if(strlen($output)>10) { + $des = $outputObj->doc->createDocumentFragment(); + $des->appendXML($output); + $outputObj->verbNode->appendChild($des); +} +?> diff --git a/lib/oaipmh/index.php b/lib/oaipmh/index.php new file mode 100644 index 0000000..862f00a --- /dev/null +++ b/lib/oaipmh/index.php @@ -0,0 +1,141 @@ +Query functions + * to reflect it and even develop your own code. + *- Check your oai site through a web browser. e.g. : \code http://localhost/oai/ \endcode + *- SELinux needs special treatments for database connection and other permission. + * + * \section struct_sec Structure +The system includes files for individual functionality and utility classes and functions to get it work. +- Controller + - oai2.php +- Individual functionalities: + - identify.php: identifies the data provider. Responses to Identify. + - listmetadataformats.php: lists supported metadata formats, e.g. dc or rif-cs. Responses to ListMetadataFormats. + - listsets.php: lists supported sets, e.g. Activity, Collection or Party. Responses to ListSets. + - listrecords.php: lists a group of records without details. Responses to ListRecords. It also serves to ListIdentifiers which only returns identifiers. + - getrecord.php: gets an individual record. Responses to GetRecord. +- Utility classes + - xml_creater.php which includes classess ANDS_XML, ANDS_Error_XML, ANDS_Response_XML +- Utility functions + - oaidp-util.php + - Support to different metadataformats in your own systems. Two examples provided with the package are: record_dc.php and record_rif.php. They are helpers and need information from the real records. They need to be devloped for your particular system. +- Configurations + - oaidp-config.php + + * + * \author Jianfeng Li + * \version 1.1 + * \date 2010-2011 + */ + +/** + * \file + * \brief + * Default starting point of OAI Data Provider for a human to check. + * + * OAI Data Provider is not designed for human to retrieve data but it is possible to use this page to test and check the functionality of current implementation. + * This page provides a summary of the OAI-PMH and the implementation. + * +*/ + +$MY_URI = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME']; +$pos = strrpos($MY_URI, '/'); +$MY_URI = substr($MY_URI, 0, $pos). '/oai2.php'; + +?> + + +php-oai2 Data Provider + + + +

php-oai2 Data Provider

+

This is an implementation of an OAI-PMH 2.0 Data Provider, written in PHP and has been tested with version 5.3.

+ +

This implementation completely complies to OAI-PMH 2.0, including the support of on-the-fly output compression which may significantly +reduce the amount of data being transfered.

+ +

This package has been inspired by PHP OAI Data Provider developed by Heinrich Stamerjohanns at University of Oldenburg. + Some of the functions and algorithms used in this code were transplanted from his implementation.

+ +

Database is supported through PDO, so almost any popular SQL-database which has PDO driver can be used without any change in the code.

+ +

It uses DOM extension,an extension included in every PHP installation since version 5, for generating XML files. With PHP 5 or above no extra extension is needed.

+ +

The repository can be quite easily configured by just editing oai2/oaidp-config.php, most possible values and options are explained. +For requirements and instructions to install and configure, please reference the documentation.

+ +

Once you have setup your Data Provider, you can the easiliy check the generated answers (it will be XML) of your Data Provider +by clicking on the test links below.

+ +

For simple visual tests set $SHOW_QUERY_ERROR to TRUE and $CONTENT_TYPE to text/plain, so you can easily read the generated XML responses in your browser.

+ +

Remember, GetRecord needs identifier to work. +So please change it use your own or you should see a response with error message.

+ +

+

+
Example Tables +
OAI Records (mysql)
+
OAI Records (pgsql)
+ +
Query and check your Data-Provider
+
Identify
+
ListMetadataFormats
+
ListSets
+
ListIdentifiers
+
ListRecords
+
GetRecord
+ +
+

+ +

+For other tests on your own provider or other providers, please use the Repository Explorer. +

+

+Jianfeng Li
+The Plant Accelerator
+University of Adelaide +

+ + + + + diff --git a/lib/oaipmh/listmetadataformats.php b/lib/oaipmh/listmetadataformats.php new file mode 100644 index 0000000..7c1bf3c --- /dev/null +++ b/lib/oaipmh/listmetadataformats.php @@ -0,0 +1,67 @@ +idFormatQuery. + * \sa idFormatQuery + */ + +/** + * Add a metadata format node to an ANDS_Response_XML + * \param &$outputObj + * type: ANDS_Response_XML. The ANDS_Response_XML object for output. + * \param $key + * type string. The name of new node. + * \param $val + * type: array. Values accessable through keywords 'schema' and 'metadataNamespace'. + * + */ +function addMetedataFormat(&$outputObj,$key,$val) { + $cmf = $outputObj->add2_verbNode("metadataFormat"); + $outputObj->addChild($cmf,'metadataPrefix',$key); + $outputObj->addChild($cmf,'schema',$val['schema']); + $outputObj->addChild($cmf,'metadataNamespace',$val['metadataNamespace']); +} + +if (isset($args['identifier'])) { + $identifier = $args['identifier']; + $query = idFormatQuery($identifier); + $res = $db->query($query); + if ($res==false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + die($db->errorInfo()); + } else { + $errors[] = oai_error('idDoesNotExist','', $identifier); + } + } else { + $record = $res->fetch(); + if($record===false) { + $errors[] = oai_error('idDoesNotExist', '', $identifier); + } else { + $mf = explode(",",$record[$SQL['metadataPrefix']]); + } + } +} + +//break and clean up on error +if (!empty($errors)) oai_exit(); + +$outputObj = new ANDS_Response_XML($args); +if (isset($mf)) { + foreach($mf as $key) { + $val = $METADATAFORMATS[$key]; + addMetedataFormat($outputObj,$key, $val); + } +} elseif (is_array($METADATAFORMATS)) { + foreach($METADATAFORMATS as $key=>$val) { + addMetedataFormat($outputObj,$key, $val); + } +} +else { // a very unlikely event + $errors[] = oai_error('noMetadataFormats'); + oai_exit(); +} +?> diff --git a/lib/oaipmh/listrecords.php b/lib/oaipmh/listrecords.php new file mode 100644 index 0000000..0cf5226 --- /dev/null +++ b/lib/oaipmh/listrecords.php @@ -0,0 +1,190 @@ +$args by keywords. + */ + +debug_message("\nI am debuging". __FILE__) ; + +// Resume previous session? +if (isset($args['resumptionToken'])) { + $readings = readResumToken($args['resumptionToken']); + + if ($readings == false) { + $errors[] = oai_error('badResumptionToken', '', $args['resumptionToken']); + } else { + debug_var_dump('readings',$readings); + list($deliveredrecords, $extquery, $metadataPrefix) = $readings; + } +} else { // no, we start a new session + $deliveredrecords = 0; + $extquery = ''; + + $metadataPrefix = $args['metadataPrefix']; + + if (isset($args['from'])) { + $from = checkDateFormat($args['from']); + $extquery .= fromQuery($from); + } + + if (isset($args['until'])) { + $until = checkDateFormat($args['until']); + $extquery .= untilQuery($until); + } + + if (isset($args['set'])) { + if (is_array($SETS)) { + $extquery .= setQuery($args['set']); + } else { + $errors[] = oai_error('noSetHierarchy'); + } + } +} + +if (!empty($errors)) { + oai_exit(); +} + +// Load the handler +if (is_array($METADATAFORMATS[$metadataPrefix]) + && isset($METADATAFORMATS[$metadataPrefix]['myhandler'])) { + $inc_record = $METADATAFORMATS[$metadataPrefix]['myhandler']; + include($inc_record); +} else { + $errors[] = oai_error('cannotDisseminateFormat', 'metadataPrefix', $metadataPrefix); +} + +if (!empty($errors)) { + oai_exit(); +} + +if (empty($errors)) { + $query = selectallQuery($metadataPrefix) . $extquery . " ORDER BY " . $SQL['identifier'] . " ASC "; + + // workaround for mysql + if (isset($deliveredrecords)){ + $query .= " LIMIT " . MAXRECORDS . " OFFSET $deliveredrecords "; + } + + debug_message("Query: $query") ; + + $res = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $r = $res->execute(); + if ($r===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + print_r($db->errorInfo()); + exit(); + } else { + $errors[] = oai_error('noRecordsMatch'); + } + } else { + $r = $res->setFetchMode(PDO::FETCH_ASSOC); + if ($r===false) { + exit("FetchMode is not supported"); + } + $num_rows = rowCount($metadataPrefix, $extquery, $db); + if ($num_rows==0) { + echo "Cannot find records: $query\n"; + $errors[] = oai_error('noRecordsMatch'); + } + } +} + +if (!empty($errors)) { + oai_exit(); +} + +// Will we need a new ResumptionToken? +if($args['verb']=='ListRecords') { + $maxItems = MAXRECORDS; +} elseif($args['verb']=='ListIdentifiers') { + $maxItems = MAXIDS; +} else { + exit("Check ".__FILE__." ".__LINE__.", there is something wrong."); +} +$maxrec = min($num_rows - $deliveredrecords, $maxItems); + +if ($num_rows - $deliveredrecords > $maxItems) { + $cursor = (int)$deliveredrecords + $maxItems; + $restoken = createResumToken($cursor, $extquery, $metadataPrefix); + $expirationdatetime = gmstrftime('%Y-%m-%dT%TZ', time()+TOKEN_VALID); +} +// Last delivery, return empty ResumptionToken +elseif (isset($args['resumptionToken'])) { + $restoken = $args['resumptionToken']; // just used as an indicator + unset($expirationdatetime); +} + + +// this don't work on mysql +/* +if (isset($args['resumptionToken'])) { + debug_message("Try to resume because a resumptionToken supplied.") ; + $record = $res->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_ABS, $deliveredrecords); +} +*/ + +// Record counter +$countrec = 0; + +// Publish a batch to $maxrec number of records +$outputObj = new ANDS_Response_XML($args); +while ($countrec++ < $maxrec) { + $record = $res->fetch(PDO::FETCH_ASSOC); + //print_r($record); + if ($record===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.",". __LINE__."
"; + print_r($db->errorInfo()); + exit(); + } + } + + $identifier = $record[$SQL['identifier']]; + $datestamp = formatDatestamp($record[$SQL['datestamp']]); + $setspec = $record[$SQL['set']]; + + // debug_var_dump('record', $record); + if (isset($record[$SQL['deleted']]) && ($record[$SQL['deleted']] === true) && + ($deletedRecord == 'transient' || $deletedRecord == 'persistent')) { + $status_deleted = TRUE; + } else { + $status_deleted = FALSE; + } + + //debug_var_dump('status_deleted', $status_deleted); + if($args['verb']=='ListRecords') { + $cur_record = $outputObj->create_record(); + $cur_header = $outputObj->create_header($oaiprefix.'-'.$identifier, $datestamp,$setspec,$cur_record); + // return the metadata record itself + if (!$status_deleted) { + debug_var_dump('inc_record',$inc_record); + create_metadata($outputObj, $cur_record, $identifier, $setspec, $db); + } + } else { // for ListIdentifiers, only identifiers will be returned. + $cur_header = $outputObj->create_header($oaiprefix.'-'.$identifier, $datestamp,$setspec); + } + if ($status_deleted) { + $cur_header->setAttribute("status","deleted"); + } +} + +// ResumptionToken +if (isset($restoken)) { + if(isset($expirationdatetime)) { + $outputObj->create_resumpToken($restoken,$expirationdatetime,$num_rows,$cursor); + } else { + $outputObj->create_resumpToken('',null,$num_rows,$deliveredrecords); + } +} + +// end ListRecords +if (SHOW_QUERY_ERROR) {echo "Debug listrecord.php reached to the end.\n\n";} +?> diff --git a/lib/oaipmh/listsets.php b/lib/oaipmh/listsets.php new file mode 100644 index 0000000..8c47918 --- /dev/null +++ b/lib/oaipmh/listsets.php @@ -0,0 +1,30 @@ +add2_verbNode("set"); + foreach($set as $key => $val) { + if($key=='setDescription') { + $desNode = $outputObj->addChild($setNode,$key); + $des = $outputObj->doc->createDocumentFragment(); + $des->appendXML($val); + $desNode->appendChild($des); + } else { + $outputObj->addChild($setNode,$key,$val); + } + } + } +} else { + $errors[] = oai_error('noSetHierarchy'); + oai_exit(); +} + +?> diff --git a/lib/oaipmh/oaidp-config.php b/lib/oaipmh/oaidp-config.php new file mode 100644 index 0000000..edbd712 --- /dev/null +++ b/lib/oaipmh/oaidp-config.php @@ -0,0 +1,305 @@ +$adminEmail: the e-mail addresses of administrators of the repository. + * + * - $repositoryIdentifier : For a data provider there is only one. For repositories to comply with the oai + * format it has to be unique identifiers for items records. Basically using domainname will be fine. + * See: http://www.openarchives.org/OAI/2.0/guidelines-oai-identifier.htm. + * + * - $SETS: An array with key words . List of supported SETs. + * + * - $METADATAFORMATS: List of supported metadata formats. It is a two-dimensional array with keys. + * Each supported format is one element of this array at the first dimension. + * The key is the name of a metadata format. + * The exact number of items within each format associated array depends on the nature of a metadata format. + * Most definitions are done here but handlers themselves are defined in separated files because only the names of PHP script are listed here. + * - metadataPrefix + * - schema + * - metadataNamespace + * - myhandler + * - other optional items: record_prefix, record_namespace and etc. + * + * - $SQL: Settings for database and queries from database + * + * - $DSN: DSN for connecting your database. Reference PDO for details. + * + * The rest of settings will not normally need to be adjusted. Read source code for details. +*/ + +/** + * Whether to show error message for dubug. + * For installation, testing and debuging set SHOW_QUERY_ERROR to TRUE + * If set to TRUE, application will die and display query and database error message + * as soon as there is a problem. Do not set this to TRUE on a production site, + * since it will show error messages to everybody. + * If set FALSE, will create XML-output, no matter what happens. + */ +// If everything is running ok, you should use this +define('SHOW_QUERY_ERROR',FALSE); + +/** + * \property CONTENT_TYPE + * The content-type the WWW-server delivers back. For debug-puposes, "text/plain" + * is easier to view. On a production site you should use "text/xml". + */ +#define('CONTENT_TYPE','Content-Type: text/plain'); +// If everything is running ok, you should use this +define('CONTENT_TYPE', 'Content-Type: text/xml'); + +/** + * Identifier settings. It needs to have proper values to reflect the settings of the data provider. + * + * - $identifyResponse['repositoryName'] : compulsory. A human readable name for the repository; + * - $identifyResponse['baseURL'] : compulsory. The base URL of the repository; + * - $identifyResponse['protocolVersion'] : compulsory. The version of the OAI-PMH supported by the repository; + * - $identifyResponse['earliestDatestamp'] : compulsory. A UTCdatetime that is the guaranteed lower limit of all datestamps recording changes, modifications, or deletions in the repository. A repository must not use datestamps lower than the one specified by the content of the earliestDatestamp element. earliestDatestamp must be expressed at the finest granularity supported by the repository. + * - $identifyResponse['deletedRecord'] : the manner in which the repository supports the notion of deleted records. Legitimate values are no ; transient ; persistent with meanings defined in the section on deletion. + * - $identifyResponse['granularity'] : the finest harvesting granularity supported by the repository. The legitimate values are YYYY-MM-DD and YYYY-MM-DDThh:mm:ssZ with meanings as defined in ISO8601. + * + */ +$identifyResponse = array(); + +// MUST (only one) +// please adjust +$identifyResponse["repositoryName"] = $_SERVER['SERVER_NAME']; + +// For ANDS to harvest of RIF-CS, originatingSource is plantaccelerator.org.au +// $dataSource = "plantaccelerator.org.au"; +define('DATASOURCE',$_SERVER['SERVER_NAME']); + +// do not change +define('MY_URI','http://'.$_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME']); +// You can use a static URI as well. +// $baseURL = "http://my.server.org/oai/oai2.php"; +$identifyResponse["baseURL"] = MY_URI; + +// By default SLIMS_SERVER_NAME is taken from the $_SERVER array. If it is different than your +// expected name, please change its value to your actual server name such as: my.server.name.com +define('SLIMS_SERVER_NAME', $_SERVER['SERVER_NAME']); + +define('SLIMS_BASE_URL', 'http://'.SLIMS_SERVER_NAME.dirname($_SERVER['SCRIPT_NAME'])); + +// do not change +$identifyResponse["protocolVersion"] = '2.0'; + +// MUST (only one) +// the earliest datestamp in your repository, +// please adjust +// Only date is needed even later it will be formatted according to the granularity. +$identifyResponse["earliestDatestamp"] = '2011-01-01'; + +// How your repository handles deletions +// no: The repository does not maintain status about deletions. +// It MUST NOT reveal a deleted status. +// persistent: The repository persistently keeps track about deletions +// with no time limit. It MUST consistently reveal the status +// of a deleted record over time. +// transient: The repository does not guarantee that a list of deletions is +// maintained. It MAY reveal a deleted status for records. +// +// If your database keeps track of deleted records change accordingly. +// Currently if $record['deleted'] is set to 'true', $status_deleted is set. +// Some lines in listidentifiers.php, listrecords.php, getrecords.php +// must be changed to fit the condition for your database. +$identifyResponse["deletedRecord"] = 'no'; +$deletedRecord = $identifyResponse["deletedRecord"]; // a shorthand for checking the configuration of Deleted Records + +// MAY (only one) +//granularity is days +//$granularity = 'YYYY-MM-DD'; +// granularity is seconds +$identifyResponse["granularity"] = 'YYYY-MM-DDThh:mm:ssZ'; + +// this is appended if your granularity is seconds. +// do not change +if (strcmp($identifyResponse["granularity"],'YYYY-MM-DDThh:mm:ssZ')==0) { + $identifyResponse["earliestDatestamp"] = $identifyResponse["earliestDatestamp"].'T00:00:00Z'; +} + +// MUST (multiple) +// please adjust +$adminEmail = array('some.one@contact.com'); + +/** Compression methods supported. Optional (multiple). Default: null. +* +* Currently only gzip is supported (you need output buffering turned on, +* and php compiled with libgz). +* The client MUST send "Accept-Encoding: gzip" to actually receive +*/ +// $compression = array('gzip'); +$compression = null; + +// MUST (only one) +// You may choose any name, but for repositories to comply with the oai +// format it has to be unique identifiers for items records. +// see: http://www.openarchives.org/OAI/2.0/guidelines-oai-identifier.htm +// Basically use domainname +// please adjust +$repositoryIdentifier = $_SERVER['SERVER_NAME']; + +// For RIF-CS, especially with ANDS, each registryObject much has a group for the ownership of data. +// For detail please see ANDS guide on its web site. Each data provider should have only one REG_OBJ_GROUP +// for this purpose. +define('REG_OBJ_GROUP','Something agreed on'); + +// If Identifier needs to show NODE description. It is defined in identify.php +// You may include details about your community and friends (other +// data-providers). +// Please check identify.php for other possible containers +// in the Identify response +$show_identifier = false; +// MUST (only one) +// should not be changed. Only useful when NODE description is included in the response to Identifier +$delimiter = ':'; + + +/** Maximum mumber of the records to deliver + * (verb is ListRecords) + * If there are more records to deliver + * a ResumptionToken will be generated. + */ +define('MAXRECORDS',100); + +/** Maximum mumber of identifiers to deliver + * (verb is ListIdentifiers) + * If there are more identifiers to deliver + * a ResumptionToken will be generated. + */ +define('MAXIDS',100); + +/** After 24 hours resumptionTokens become invalid. Unit is second. */ +define('TOKEN_VALID',24*3600); +$expirationdatetime = gmstrftime('%Y-%m-%dT%TZ', time()+TOKEN_VALID); +/** Where token is saved and path is included */ +define('TOKEN_PREFIX','/tmp/ANDS_DBPD-'); + +// define all supported sets in your repository +$SETS = array ( + array('setSpec'=>'class:activity', 'setName'=>'Activities'), + array('setSpec'=>'class:collection', 'setName'=>'Collections'), + array('setSpec'=>'class:party', 'setName'=>'Parties')/*, + array('setSpec'=>'phdthesis', 'setName'=>'PHD Thesis', 'setDescription'=>' + This set contains metadata describing electronic music recordings made during the 1950ies + ') //, + // array('setSpec'=>'math', 'setName'=>'Mathematics') , + // array('setSpec'=>'phys', 'setName'=>'Physics') + */); + +// define all supported metadata formats, has to be an array +// +// myhandler is the name of the file that handles the request for the +// specific metadata format. +// [record_prefix] describes an optional prefix for the metadata +// [record_namespace] describe the namespace for this prefix + +$METADATAFORMATS = array ( + /*'rif' => array('metadataPrefix'=>'rif', + 'schema'=>'http://services.ands.org.au/sandbox/orca/schemata/registryObjects.xsd', + 'metadataNamespace'=>'http://ands.org.au/standards/rif-cs/registryObjects/', + 'myhandler'=>'record_rif.php' + ),*/ + 'oai_dc' => array('metadataPrefix'=>'oai_dc', + 'schema'=>'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', + 'metadataNamespace'=>'http://www.openarchives.org/OAI/2.0/oai_dc/', + 'myhandler'=>'record_dc.php', + 'record_prefix'=>'dc', + 'record_namespace' => 'http://purl.org/dc/elements/1.1/' + ) + ); + +if (!is_array($METADATAFORMATS)) { exit("Configuration of METADATAFORMAT has been wrongly set. Correct your ".__FILE__);} + +// The shorthand of xml schema namespace, no need to change this +define('XMLSCHEMA', 'http://www.w3.org/2001/XMLSchema-instance'); + +// +// DATABASE SETUP +// + +// change according to your local DB setup. +$DB_HOST = DB_HOST; +$DB_USER = DB_USERNAME; +$DB_PASSWD = DB_PASSWORD; +$DB_NAME = DB_NAME; + +// Data Source Name: This is the universal connection string +// if you use something other than mysql edit accordingly. +// Example for MySQL +//$DSN = "mysql://$DB_USER:$DB_PASSWD@$DB_HOST/$DB_NAME"; +$DSN = "mysql:host=$DB_HOST;dbname=$DB_NAME"; +// Example for Oracle +// $DSN = "oci8://$DB_USER:$DB_PASSWD@$DB_NAME"; + +//$DSN = "pgsql:host=$DB_HOST;port=5432;dbname=$DB_NAME;user=$DB_USER;password=$DB_PASSWD"; + +// the charset you store your metadata in your database +// currently only utf-8 and iso8859-1 are supported +$charset = "iso8859-1"; + +// if entities such as < > ' " in your metadata has already been escaped +// then set this to true (e.g. you store < as < in your DB) +$xmlescaped = false; + +// We store multiple entries for one element in a single row +// in the database. SQL['split'] lists the delimiter for these entries. +// If you do not do this, do not define $SQL['split'] +// $SQL['split'] = ';'; + +// the name of the table where your store your metadata's header +$SQL['table'] = 'biblio'; + +// the name of the column where you store the unique identifiers +// pointing to your item. +// this is your internal identifier for the item +$SQL['identifier'] = 'biblio_id'; + +$SQL['metadataPrefix'] = 'oai_metadataprefix'; + +// If you want to expand the internal identifier in some way +// use this (but not for OAI stuff, see next line) +$idPrefix = 'slims'; + +// this is your external (OAI) identifier for the item +// this will be expanded to +// oai:$repositoryIdentifier:$idPrefix$SQL['identifier'] +// should not be changed +// +// Commented out 24/11/10 14:19:09 +$oaiprefix = "oai".$delimiter.$repositoryIdentifier.$delimiter.$idPrefix; +//$oaiprefix = ""; + +// adjust anIdentifier with sample contents an identifier +// $sampleIdentifier = $oaiprefix.'anIdentifier'; + +// the name of the column where you store your datestamps +$SQL['datestamp'] = 'input_date'; + +// the name of the column where you store information whether +// a record has been deleted. Leave it as it is if you do not use +// this feature. +$SQL['deleted'] = 'deleted'; + +// to be able to quickly retrieve the sets to which one item belongs, +// the setnames are stored for each item +// the name of the column where you store sets +$SQL['set'] = 'publish_year'; +?> diff --git a/lib/oaipmh/oaidp-util.php b/lib/oaipmh/oaidp-util.php new file mode 100644 index 0000000..d1858b5 --- /dev/null +++ b/lib/oaipmh/oaidp-util.php @@ -0,0 +1,348 @@ +return parameter sets to true. + * \param $msg Type: string Message needs to be shown + * \see SHOW_QUERY_ERROR in oaidp-config.php + */ +function debug_message($msg) { + if (!SHOW_QUERY_ERROR) return; + echo $msg,"\n"; +} + +/** Check if provided correct arguments for a request. + * + * Only number of parameters is checked. + * metadataPrefix has to be checked before it is used. + * set has to be checked before it is used. + * resumptionToken has to be checked before it is used. + * from and until can easily checked here because no extra information + * is needed. + */ +function checkArgs($args, $checkList) { +// global $errors, $TOKEN_VALID, $METADATAFORMATS; + global $errors, $METADATAFORMATS; +// $verb = $args['verb']; + unset($args["verb"]); + + debug_print_r('checkList',$checkList); + debug_print_r('args',$args); + + // "verb" has been checked before, no further check is needed + if(isset($checkList['required'])) { + for($i = 0; $i < count($checkList["required"]); $i++) { + debug_message("Checking: par$i: ". $checkList['required'][$i] . " in "); + debug_var_dump("isset(\$args[\$checkList['required'][\$i]])",isset($args[$checkList['required'][$i]])); + // echo "key exists". array_key_exists($checkList["required"][$i],$args)."\n"; + if(isset($args[$checkList['required'][$i]])==false) { + // echo "caught\n"; + $errors[] = oai_error('missingArgument', $checkList["required"][$i]); + } else { + // if metadataPrefix is set, it is in required section + if(isset($args['metadataPrefix'])) { + $metadataPrefix = $args['metadataPrefix']; + // Check if the format is supported, it has enough infor (an array), last if a handle has been defined. + if (!array_key_exists ($metadataPrefix, $METADATAFORMATS) || !(is_array($METADATAFORMATS[$metadataPrefix]) + || !isset($METADATAFORMATS[$metadataPrefix]['myhandler']))) { + $errors[] = oai_error('cannotDisseminateFormat', 'metadataPrefix', $metadataPrefix); + } + } + unset($args[$checkList["required"][$i]]); + } + } + } + debug_message('Before return'); + debug_print_r('errors',$errors); + if (!empty($errors)) return; + + // check to see if there is unwanted + foreach($args as $key => $val) { + debug_message("checkArgs: $key"); + if(!in_array($key, $checkList["ops"])) { + debug_message("Wrong\n".print_r($checkList['ops'],true)); + //$errors[] = oai_error('badArgument', $key, $val); // ignore it... + } + switch ($key) { + case 'from': + case 'until': + if(!checkDateFormat($val)) { + $errors[] = oai_error('badGranularity', $key, $val); + } + break; + + case 'resumptionToken': + // only check for expairation + // if((int)$val+TOKEN_VALID < time()) + + // only check for value + if (empty($val)) + $errors[] = oai_error('badResumptionToken'); + break; + } + } +} + +/** Validates an identifier. The pattern is: '/^[-a-z\.0-9]+$/i' which means + * it accepts -, letters and numbers. + * Used only by function oai_error code idDoesNotExist. + * \param $url Type: string + */ +function is_valid_uri($url) +{ + return((bool)preg_match('/^[-a-z\.0-9]+$/i', $url)); +} + +/** Validates attributes come with the query. + * It accepts letters, numbers, ':', '_', '.' and -. + * Here there are few more match patterns than is_valid_uri(): ':_'. + * \param $attrb Type: string + */ + function is_valid_attrb($attrb) { + return preg_match("/^[_a-zA-Z0-9\-\:\.\+\>\%]+$/",$attrb); + } + +/** All datestamps used in this system are GMT even + * return value from database has no TZ information + */ +function formatDatestamp($datestamp) +{ + return date("Y-m-d\TH:i:s\Z",strtotime($datestamp)); +} + +/** The database uses datastamp without time-zone information. + * It needs to clean all time-zone informaion from time string and reformat it + */ +function checkDateFormat($date) { + $date = str_replace(array("T","Z")," ",$date); + $time_val = strtotime($date); + if (SHOW_QUERY_ERROR) { echo "timeval: $time_val\n"; } + if(!$time_val) return false; + if(strstr($date,":")) { + return date("Y-m-d H:i:s",$time_val); + } else { + return date("Y-m-d",$time_val); + } +} + +/** Retrieve all defined 'setSpec' from configuraiton of $SETS. + * It is used by ANDS_TPA::create_obj_node(); +*/ +function prepare_set_names() { + global $SETS; + $n = count($SETS); + $a = array_fill(0,$n,''); + for ($i = 0; $i <$n; $i++) { + $a[$i] = $SETS[$i]['setSpec']; + } + return $a; +} + +/** Finish a request when there is an error: send back errors. */ +function oai_exit() +{ +// global $CONTENT_TYPE; + header(CONTENT_TYPE); + global $args,$errors,$compress; + $e = new ANDS_Error_XML($args,$errors); + if ($compress) { + ob_start('ob_gzhandler'); + } + + $e->display(); + + if ($compress) { + ob_end_flush(); + } + + exit(); +} + +// ResumToken section +/** Generate a string based on the current Unix timestamp in microseconds for creating resumToken file name. */ +function get_token() +{ + list($usec, $sec) = explode(" ", microtime()); + return ((int)($usec*1000) + (int)($sec*1000)); +} + +/** Create a token file. + * It has three parts which is separated by '#': cursor, extension of query, metadataPrefix. + * Called by listrecords.php. + */ +function createResumToken($cursor, $extquery, $metadataPrefix) { + + /* + $token = get_token(); + $fp = fopen (TOKEN_PREFIX.$token, 'w'); + if($fp==false) { + exit("Cannot write. Writer permission needs to be changed."); + } + fputs($fp, "$cursor#"); + fputs($fp, "$extquery#"); + fputs($fp, "$metadataPrefix#"); + fclose($fp); + return $token; + */ + + $token = $cursor . "__" . urlencode($extquery) . "__" . $metadataPrefix; + return $token; +} + +/** Read a saved ResumToken */ +function readResumToken($resumptionToken) { + + /* + $rtVal = false; + $fp = fopen($resumptionToken, 'r'); + if ($fp!=false) { + $filetext = fgets($fp, 255); + $textparts = explode('#', $filetext); + fclose($fp); + unlink ($resumptionToken); + $rtVal = array((int)$textparts[0], $textparts[1], $textparts[2]); + } + return $rtVal; + */ + + $parts = explode("__", urldecode($resumptionToken)); + return $parts; +} + +// Here are a couple of queries which might need to be adjusted to +// your needs. Normally, if you have correctly named the columns above, +// this does not need to be done. + +/** this function should generate a query which will return + * all records + * the useless condition id_column = id_column is just there to ease + * further extensions to the query, please leave it as it is. + */ +function selectallQuery ($metadPrefix = "oai_dc", $id = '') +{ + global $SQL; + $query = "SELECT " . $SQL['identifier'] . "," . $SQL['datestamp'] . "," . $SQL['set'] . + " FROM ".$SQL['table'] . " WHERE 1 "; + if ($id != '') { + $query .= " AND ".$SQL['identifier']." ='$id'"; + } + return $query; +} + +/** this function will return metadataFormat of a record */ +function idFormatQuery($id) +{ + global $SQL; + return 'select '.$SQL['metadataPrefix'].' FROM '.$SQL['table']. " WHERE ".$SQL['identifier']." = '".$id."'"; +} + +/** this function will return identifier and datestamp for all records + * not very useful + */ +function idQuery ($metadPrefix = "rif", $id = '') +{ + global $SQL; + + if ($SQL['set'] != '') { + $query = 'select '.$SQL['identifier'].','.$SQL['datestamp'].','.$SQL['set'].' FROM '.$SQL['table']. " WHERE ".$SQL['metadataPrefix']." LIKE '%$metadPrefix%'"; + } else { + $query = 'select '.$SQL['identifier'].','.$SQL['datestamp'].' FROM '.$SQL['table']. " WHERE ".$SQL['metadataPrefix']." LIKE '%$metadPrefix%'"; + } + + if ($id != '') { + $query .= " AND ".$SQL['identifier']." = '$id'"; + } + + return $query; +} + +/** filter for until, appends to the end of SQL query */ +function untilQuery($until) +{ + global $SQL; + + return ' AND '.$SQL['datestamp']." <= '$until'"; +} + +/** filter for from , appends to the end of SQL query */ +function fromQuery($from) +{ + global $SQL; + + return ' AND '.$SQL['datestamp']." >= '$from'"; +} + +/** filter for sets, appends to the end of SQL query */ +function setQuery($set) +{ + global $SQL; + // strip off "class:" which is not saved in database + if(strstr($set,"class:")) $set = substr($set,6); + return ' AND '.$SQL['set']." LIKE '%$set%'"; +} + +/** for accurately to assess how many records satisfy conditions for all DBs */ +function rowCount($metadataPrefix, $extQuery, $db) { + global $SQL; + $n = 0; + $sql = "SELECT COUNT(*) FROM ".$SQL['table'] . " WHERE 1 " . $extQuery; + if ($res = $db->query($sql)) { + $n = $res->fetchColumn(); + } + return $n; +} + +/** A worker function for processing an error when a query was executed + * \param $query string, original query + * \param $e PDOException, the PDOException object +*/ +function process_pdo_error($query, $e) { + echo $query.' was failed\n'; + echo $e->getMessage(); +} + +/** When query return no result, throw an Exception of Not found. + * \param $db PDO + * \param $query string + * \return $res PDOStatement + */ +function exec_pdo_query($db, $query) +{ + $res = $db->query($query); + if ($res===false) { + throw new Exception($query.":\nIt found nothing.\n"); + } else return $res; +} +?> diff --git a/lib/oaipmh/record_dc.php b/lib/oaipmh/record_dc.php new file mode 100644 index 0000000..496d3e5 --- /dev/null +++ b/lib/oaipmh/record_dc.php @@ -0,0 +1,170 @@ +create_metadata($cur_record); + + $oai_node = $outputObj->addChild($metadata_node, "oai_dc:dc"); + $oai_node->setAttribute("xmlns:oai_dc","http://www.openarchives.org/OAI/2.0/oai_dc/"); + $oai_node->setAttribute("xmlns:dc","http://purl.org/dc/elements/1.1/"); + $oai_node->setAttribute("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance"); + $oai_node->setAttribute("xsi:schemaLocation", "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"); + + $record = get_record($identifier, $db); + $authors = get_authors($identifier, $db); + $subjects = get_subjects($identifier, $db); + + $outputObj->addChild($oai_node,'dc:title', xml_safe($record['title'])); + foreach ($authors as $author){ + $outputObj->addChild($oai_node,'dc:creator', xml_safe($author['author_name'])); + } + foreach ($subjects as $subject){ + $outputObj->addChild($oai_node,'dc:subject', xml_safe($subject['topic'])); + } + $outputObj->addChild($oai_node,'dc:publisher', xml_safe($record['publisher_name'])); + $outputObj->addChild($oai_node,'dc:date', date_safe($record['publish_year'])); + $outputObj->addChild($oai_node,'dc:language', $record['language_id']); + $outputObj->addChild($oai_node,'dc:format', $record['gmd_name']); + $outputObj->addChild($oai_node,'dc:identifier', SLIMS_BASE_URL.'/index.php?p=show_detail&id='. $record['biblio_id']); + if (!empty($record['isbn_issn'])) $outputObj->addChild($oai_node,'dc:identifier_isbn', xml_safe($record['isbn_issn'])); + if (!empty($record['notes'])) $outputObj->addChild($oai_node,'dc:description', xml_safe($record['notes'])); + if (!empty($record['publish_place'])) $outputObj->addChild($oai_node,'dc:location', xml_safe($record['publish_place'])); + if (!empty($record['image'])) $outputObj->addChild($oai_node,'dc:identifier', SLIMS_BASE_URL.'/lib/phpthumb/phpThumb.php?src=../../images/docs/'. xml_safe($record['image'])); + if (!empty($record['series_title'])) $outputObj->addChild($oai_node,'dc:series', xml_safe($record['series_title'])); + if (!empty($record['collation'])) $outputObj->addChild($oai_node,'dc:description', xml_safe($record['collation'])); + if (!empty($record['classification'])) $outputObj->addChild($oai_node,'dc:subject', xml_safe($record['classification'])); + + //print_r($record); + //print_r($authors); + //print_r($subjects); +} + +function xml_safe($string){ + return preg_replace('/[\x00-\x1f]/','',htmlspecialchars($string)); +} + +function date_safe($string){ + if (preg_match("/(\d{4})/", $string, $matches)){ + return $matches[0]; + } else { + return xml_safe($string); + } +} + +function get_record ($identifier, $db){ + $query = 'SELECT b.*, l.language_name, p.publisher_name, pl.place_name AS \'publish_place\', gmd.gmd_name, fr.frequency FROM biblio AS b + LEFT JOIN mst_gmd AS gmd ON b.gmd_id=gmd.gmd_id + LEFT JOIN mst_language AS l ON b.language_id=l.language_id + LEFT JOIN mst_publisher AS p ON b.publisher_id=p.publisher_id + LEFT JOIN mst_place AS pl ON b.publish_place_id=pl.place_id + LEFT JOIN mst_frequency AS fr ON b.frequency_id=fr.frequency_id + WHERE biblio_id=' . $identifier; + + $res = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $r = $res->execute(); + if ($r===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + print_r($db->errorInfo()); + exit(); + } else { + return array(); + } + } else { + $record = $res->fetch(PDO::FETCH_ASSOC); + return $record; + } +} + +function get_authors ($identifier, $db) { + $query = 'SELECT a.*,ba.level FROM mst_author AS a' + .' LEFT JOIN biblio_author AS ba ON a.author_id=ba.author_id WHERE ba.biblio_id='.$identifier; + + $res = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $r = $res->execute(); + if ($r===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + print_r($db->errorInfo()); + exit(); + } else { + return array(); + } + } else { + $records = array(); + $hasNext = 1; + while($hasNext){ + $record = $res->fetch(PDO::FETCH_ASSOC); + if ($record){ + array_push($records, $record); + } else { + $hasNext = 0; + } + } + + return $records; + } +} + +function get_subjects($identifier, $db) { + $query = 'SELECT t.topic, t.topic_type, t.auth_list, bt.level FROM mst_topic AS t + LEFT JOIN biblio_topic AS bt ON t.topic_id=bt.topic_id WHERE bt.biblio_id='.$identifier.' ORDER BY t.auth_list'; + + $res = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $r = $res->execute(); + if ($r===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + print_r($db->errorInfo()); + exit(); + } else { + return array(); + } + } else { + $records = array(); + $hasNext = 1; + while($hasNext){ + $record = $res->fetch(PDO::FETCH_ASSOC); + if ($record){ + array_push($records, $record); + } else { + $hasNext = 0; + } + } + + return $records; + } +} + +function get_digital_files($identifier, $db) { + + // digital files + $attachment_q = $this->obj_db->query('SELECT att.*, f.* FROM biblio_attachment AS att + LEFT JOIN files AS f ON att.file_id=f.file_id WHERE att.biblio_id='.$this->detail_id.' AND att.access_type=\'public\' LIMIT 20'); + if ($attachment_q->num_rows > 0) { + while ($attachment_d = $attachment_q->fetch_assoc()) { + $_xml_output .= ''."\n"; + } + } +} diff --git a/lib/oaipmh/record_dc.php~ b/lib/oaipmh/record_dc.php~ new file mode 100644 index 0000000..496d3e5 --- /dev/null +++ b/lib/oaipmh/record_dc.php~ @@ -0,0 +1,170 @@ +create_metadata($cur_record); + + $oai_node = $outputObj->addChild($metadata_node, "oai_dc:dc"); + $oai_node->setAttribute("xmlns:oai_dc","http://www.openarchives.org/OAI/2.0/oai_dc/"); + $oai_node->setAttribute("xmlns:dc","http://purl.org/dc/elements/1.1/"); + $oai_node->setAttribute("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance"); + $oai_node->setAttribute("xsi:schemaLocation", "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"); + + $record = get_record($identifier, $db); + $authors = get_authors($identifier, $db); + $subjects = get_subjects($identifier, $db); + + $outputObj->addChild($oai_node,'dc:title', xml_safe($record['title'])); + foreach ($authors as $author){ + $outputObj->addChild($oai_node,'dc:creator', xml_safe($author['author_name'])); + } + foreach ($subjects as $subject){ + $outputObj->addChild($oai_node,'dc:subject', xml_safe($subject['topic'])); + } + $outputObj->addChild($oai_node,'dc:publisher', xml_safe($record['publisher_name'])); + $outputObj->addChild($oai_node,'dc:date', date_safe($record['publish_year'])); + $outputObj->addChild($oai_node,'dc:language', $record['language_id']); + $outputObj->addChild($oai_node,'dc:format', $record['gmd_name']); + $outputObj->addChild($oai_node,'dc:identifier', SLIMS_BASE_URL.'/index.php?p=show_detail&id='. $record['biblio_id']); + if (!empty($record['isbn_issn'])) $outputObj->addChild($oai_node,'dc:identifier_isbn', xml_safe($record['isbn_issn'])); + if (!empty($record['notes'])) $outputObj->addChild($oai_node,'dc:description', xml_safe($record['notes'])); + if (!empty($record['publish_place'])) $outputObj->addChild($oai_node,'dc:location', xml_safe($record['publish_place'])); + if (!empty($record['image'])) $outputObj->addChild($oai_node,'dc:identifier', SLIMS_BASE_URL.'/lib/phpthumb/phpThumb.php?src=../../images/docs/'. xml_safe($record['image'])); + if (!empty($record['series_title'])) $outputObj->addChild($oai_node,'dc:series', xml_safe($record['series_title'])); + if (!empty($record['collation'])) $outputObj->addChild($oai_node,'dc:description', xml_safe($record['collation'])); + if (!empty($record['classification'])) $outputObj->addChild($oai_node,'dc:subject', xml_safe($record['classification'])); + + //print_r($record); + //print_r($authors); + //print_r($subjects); +} + +function xml_safe($string){ + return preg_replace('/[\x00-\x1f]/','',htmlspecialchars($string)); +} + +function date_safe($string){ + if (preg_match("/(\d{4})/", $string, $matches)){ + return $matches[0]; + } else { + return xml_safe($string); + } +} + +function get_record ($identifier, $db){ + $query = 'SELECT b.*, l.language_name, p.publisher_name, pl.place_name AS \'publish_place\', gmd.gmd_name, fr.frequency FROM biblio AS b + LEFT JOIN mst_gmd AS gmd ON b.gmd_id=gmd.gmd_id + LEFT JOIN mst_language AS l ON b.language_id=l.language_id + LEFT JOIN mst_publisher AS p ON b.publisher_id=p.publisher_id + LEFT JOIN mst_place AS pl ON b.publish_place_id=pl.place_id + LEFT JOIN mst_frequency AS fr ON b.frequency_id=fr.frequency_id + WHERE biblio_id=' . $identifier; + + $res = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $r = $res->execute(); + if ($r===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + print_r($db->errorInfo()); + exit(); + } else { + return array(); + } + } else { + $record = $res->fetch(PDO::FETCH_ASSOC); + return $record; + } +} + +function get_authors ($identifier, $db) { + $query = 'SELECT a.*,ba.level FROM mst_author AS a' + .' LEFT JOIN biblio_author AS ba ON a.author_id=ba.author_id WHERE ba.biblio_id='.$identifier; + + $res = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $r = $res->execute(); + if ($r===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + print_r($db->errorInfo()); + exit(); + } else { + return array(); + } + } else { + $records = array(); + $hasNext = 1; + while($hasNext){ + $record = $res->fetch(PDO::FETCH_ASSOC); + if ($record){ + array_push($records, $record); + } else { + $hasNext = 0; + } + } + + return $records; + } +} + +function get_subjects($identifier, $db) { + $query = 'SELECT t.topic, t.topic_type, t.auth_list, bt.level FROM mst_topic AS t + LEFT JOIN biblio_topic AS bt ON t.topic_id=bt.topic_id WHERE bt.biblio_id='.$identifier.' ORDER BY t.auth_list'; + + $res = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $r = $res->execute(); + if ($r===false) { + if (SHOW_QUERY_ERROR) { + echo __FILE__.','.__LINE__."
"; + echo "Query: $query
\n"; + print_r($db->errorInfo()); + exit(); + } else { + return array(); + } + } else { + $records = array(); + $hasNext = 1; + while($hasNext){ + $record = $res->fetch(PDO::FETCH_ASSOC); + if ($record){ + array_push($records, $record); + } else { + $hasNext = 0; + } + } + + return $records; + } +} + +function get_digital_files($identifier, $db) { + + // digital files + $attachment_q = $this->obj_db->query('SELECT att.*, f.* FROM biblio_attachment AS att + LEFT JOIN files AS f ON att.file_id=f.file_id WHERE att.biblio_id='.$this->detail_id.' AND att.access_type=\'public\' LIMIT 20'); + if ($attachment_q->num_rows > 0) { + while ($attachment_d = $attachment_q->fetch_assoc()) { + $_xml_output .= ''."\n"; + } + } +} diff --git a/lib/oaipmh/record_rif.php b/lib/oaipmh/record_rif.php new file mode 100644 index 0000000..c7be32e --- /dev/null +++ b/lib/oaipmh/record_rif.php @@ -0,0 +1,34 @@ +create_metadata($cur_record); + $obj_node = new ANDS_TPA($outputObj, $metadata_node, $db); + try { + $obj_node->create_obj_node($setspec, $identifier); + } catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), " when adding $identifier\n"; + } +} + diff --git a/lib/oaipmh/xml_creater.php b/lib/oaipmh/xml_creater.php new file mode 100644 index 0000000..a1380af --- /dev/null +++ b/lib/oaipmh/xml_creater.php @@ -0,0 +1,327 @@ +"ListRecords","resumptionToken"=>"9CD1DA87F59C3E960871F4F3C9D093887C17D174"); + * // Example 1: Error response + * $error_array[] = oai_error("badVerb","Rubish"); + * $error_array[] = oai_error("sameVerb"); + * $e = new ANDS_Error_XML($par_array,$error_array); + * $e->display(); + * + * // Example 2: Normal response without error codes + * $par_array = array("verb"=>"ListRecords","resumptionToken"=>"9CD1DA87F59C3E960871F4F3C9D093887C17D174"); + * $test = new ANDS_Response_XML($par_array); + * $record_node = $test->create_record(); + * $test->create_header("function: identifier string",gmdate("Y-m-d\TH:i:s\Z"),"collection",$record_node); + * $test->create_metadata($record_node); + * $test->display(); + * \endcode + * + * \see http://www.openarchives.org/OAI/openarchivesprotocol.html#ErrorConditions + */ + +/* +http://www.openarchives.org/OAI/openarchivesprotocol.html#ErrorConditions + +badArgument: + The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax. Applied to all verbs. + +badResumptionToken: + The value of the resumptionToken argument is invalid or expired. Applied to: ListIdentifiers, ListRecords, ListSets + +badVerb: + Value of the verb argument is not a legal OAI-PMH verb, the verb argument is missing, or the verb argument is repeated. N/A + +cannotDisseminateFormat: + The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository. Applied to GetRecord, ListIdentifiers, ListRecords + +idDoesNotExist: + The value of the identifier argument is unknown or illegal in this repository. Applied to GetRecord, ListMetadataFormats + +noRecordsMatch: + The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list. Applied to ListIdentifiers, ListRecords + +noMetadataFormats: + There are no metadata formats available for the specified item. Applied to ListMetadataFormats. + +noSetHierarchy: + The repository does not support sets. Applied to ListSets, ListIdentifiers, ListRecords + +*/ + +/** utility funciton to mapping error codes to readable messages */ +function oai_error($code, $argument = '', $value = '') +{ + switch ($code) { + case 'badArgument' : + $text = "The argument '$argument' (value='$value') included in the request is not valid."; + break; + + case 'badGranularity' : + $text = "The value '$value' of the argument '$argument' is not valid."; + $code = 'badArgument'; + break; + + case 'badResumptionToken' : + $text = "The resumptionToken '$value' does not exist or has already expired."; + break; + + case 'badRequestMethod' : + $text = "The request method '$argument' is unknown."; + $code = 'badVerb'; + break; + + case 'badVerb' : + $text = "The verb '$argument' provided in the request is illegal."; + break; + + case 'cannotDisseminateFormat' : + $text = "The metadata format '$value' given by $argument is not supported by this repository."; + break; + + case 'exclusiveArgument' : + $text = 'The usage of resumptionToken as an argument allows no other arguments.'; + $code = 'badArgument'; + break; + + case 'idDoesNotExist' : + $text = "The value '$value' of the identifier does not exist in this repository."; + if (!is_valid_uri($value)) { + $code = 'badArgument'; + $text .= ' Invalidated URI has been detected.'; + } + break; + + case 'missingArgument' : + $text = "The required argument '$argument' is missing in the request."; + $code = 'badArgument'; + break; + + case 'noRecordsMatch' : + $text = 'The combination of the given values results in an empty list.'; + break; + + case 'noMetadataFormats' : + $text = 'There are no metadata formats available for the specified item.'; + break; + + case 'noVerb' : + $text = 'The request does not provide any verb.'; + $code = 'badVerb'; + break; + + case 'noSetHierarchy' : + $text = 'This repository does not support sets.'; + break; + + case 'sameArgument' : + $text = 'Do not use the same argument more than once.'; + $code = 'badArgument'; + break; + + case 'sameVerb' : + $text = 'Do not use verb more than once.'; + $code = 'badVerb'; + break; + + case 'notImp' : + $text = 'Not yet implemented.'; + $code = 'debug'; + break; + + default: + $text = "Unknown error: code: '$code', argument: '$argument', value: '$value'"; + $code = 'badArgument'; + } + return $code."|".$text; +} + +/** + * A wraper of DOMDocument for data provider + */ +class ANDS_XML { + + public $doc; /**< Type: DOMDocument. Handle of current XML Document object */ + + /** + * Constructs an ANDS_XML object. + * + * @param $par_array Type: array. + * Array of request parameters for creating an ANDS_XML object. + * \see create_request. + */ + function __construct($par_array) { + $this->doc = new DOMDocument("1.0","UTF-8"); + // oai_node equals to $this->doc->documentElement; + $oai_node = $this->doc->createElement("OAI-PMH"); + $oai_node->setAttribute("xmlns","http://www.openarchives.org/OAI/2.0/"); + $oai_node->setAttribute("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance"); + $oai_node->setAttribute("xsi:schemaLocation","http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd"); + $this->addChild($oai_node,"responseDate",gmdate("Y-m-d\TH:i:s\Z")); + $this->doc->appendChild($oai_node); + $this->create_request($par_array); + } + + /** + * Add a child node to a parent node on a XML Doc: a worker function. + * + * @param $mom_node + * Type: DOMNode. The target node. + * + * @param $name + * Type: string. The name of child nade is being added + * + * @param $value + * Type: string. Text for the adding node if it is a text node. + * + * @return DOMElement $added_node + * The newly created node, can be used for further expansion. + * If no further expansion is expected, return value can be igored. + */ + + function addChild($mom_node,$name, $value='') { + $added_node = @$this->doc->createElement($name,$value); + $added_node = @$mom_node->appendChild($added_node); + return $added_node; + } + + + /** + * Create an OAI request node. + * + * @param $par_array Type: array + * The attributes of a request node. They describe the verb of the request and other associated parameters used in the request. + * Keys of the array define attributes, and values are their content. + */ + + function create_request($par_array) { + $request = $this->addChild($this->doc->documentElement,"request",MY_URI); + foreach($par_array as $key => $value) { + $request->setAttribute($key,$value); + } + } + + /** + * Display a doc in a readable, well-formatted way for display or saving + */ + function display() { + $pr = new DOMDocument(); + $pr->preserveWhiteSpace = false; + $pr->formatOutput = true; + $pr->loadXML($this->doc->saveXML()); + echo $pr->saveXML(); + } +} + +/** + * Generate an XML response when a request cannot be finished + * + * It has only one derived member function + */ +class ANDS_Error_XML extends ANDS_XML { + function __construct($par_array, $error_array) { + parent::__construct($par_array); + + $oai_node = $this->doc->documentElement; + foreach($error_array as $e) { + list($code, $value) = explode("|", $e); + $node = $this->addChild($oai_node,"error",$value); + $node->setAttribute("code",$code); + } + } +} + +/** + * Generate an XML response to a request if no error has occured + * + * This is the class to further develop to suits a publication need + */ +class ANDS_Response_XML extends ANDS_XML { + public $verbNode; /**< Type: DOMElement. Verb node itself. */ + protected $verb; /**< Type: string. The verb in the request */ + + function __construct($par_array) { + parent::__construct($par_array); + $this->verb = $par_array["verb"]; + $this->verbNode = $this->addChild($this->doc->documentElement,$this->verb); + } + +/** Add direct child nodes to verb node (OAI-PMH), e.g. response to ListMetadataFormats. + * Different verbs can have different required child nodes. + * \see create_record, create_header + * \see http://www.openarchives.org/OAI/2.0/openarchivesprotocol.htm. + * + * \param $nodeName Type: string. The name of appending node. + * \param $value Type: string. The content of appending node. + */ + function add2_verbNode($nodeName, $value=null) { + return $this->addChild($this->verbNode,$nodeName,$value); + } + + /** + * Create an empty \ node. Other nodes will be appended to it later. + */ + function create_record() { + return $this->add2_verbNode("record"); + } + + /** Headers are enclosed inside of \ to the query of ListRecords, ListIdentifiers and etc. + * + * \param $identifier Type: string. The identifier string for node \. + * \param $timestamp Type: timestamp. Timestapme in UTC format for node \. + * \param $ands_class Type: mix. Can be an array or just a string. Content of \. + * \param $add_to_node Type: DOMElement. Default value is null. + * In normal cases, $add_to_node is the \ node created previously. When it is null, the newly created header node is attatched to $this->verbNode. + * Otherwise it will be attatched to the desired node defined in $add_to_node. + */ + function create_header($identifier,$timestamp,$ands_class, $add_to_node=null) { + if(is_null($add_to_node)) { + $header_node = $this->add2_verbNode("header"); + } else { + $header_node = $this->addChild($add_to_node,"header"); + } + $this->addChild($header_node,"identifier",$identifier); + $this->addChild($header_node,"datestamp",$timestamp); + if (is_array($ands_class)) { + foreach ($ands_class as $setspec) { + $this->addChild($header_node,"setSpec",$setspec); + } + } else { $this->addChild($header_node,"setSpec",$ands_class); } + return $header_node; + } + + /** Create metadata node for holding metadata. This is always added to \ node. + * + * \param $mom_record_node DOMElement. A node acts as the parent node. + * + * @return $meta_node Type: DOMElement. + * The newly created registryObject node which will be used for further expansion. + * metadata node itself is maintained by internally by the Class. + */ + function create_metadata($mom_record_node) { + $meta_node = $this->addChild($mom_record_node,"metadata"); + return $meta_node; + } + + /** If there are too many records request could not finished a resumpToken is generated to let harvester know + * + * \param $token Type: string. A random number created somewhere? + * \param $expirationdatetime Type: string. A string representing time. + * \param $num_rows Type: integer. Number of records retrieved. + * \param $cursor Type: string. Cursor can be used for database to retrieve next time. + */ + function create_resumpToken($token, $expirationdatetime, $num_rows, $cursor=null) { + $resump_node = $this->addChild($this->verbNode,"resumptionToken",$token); + if(isset($expirationdatetime)) { + $resump_node->setAttribute("expirationDate",$expirationdatetime); + } + $resump_node->setAttribute("completeListSize",$num_rows); + $resump_node->setAttribute("cursor",$cursor); + } +} + diff --git a/oai2.php b/oai2.php new file mode 100644 index 0000000..7d8b7fd --- /dev/null +++ b/oai2.php @@ -0,0 +1,190 @@ +Remember: to define your own classess for generating metadata records. + * In common cases, you have to implement your own code to act fully and correctly. + * For generic usage, you can try the ANDS_Response_XML defined in xml_creater.php. + */ + +// Report all errors except E_NOTICE +// This is the default value set in php.ini +// If anything else, try them. +// error_reporting (E_ALL ^ E_NOTICE); + +/** + * An array for collecting erros which can be reported later. It will be checked before a new action is taken. + */ +$errors = array(); + +ini_set('session.use_cookies', '0'); + +// key to authenticate +define('INDEX_AUTH', '1'); + +// required file +require 'sysconfig.inc.php'; +define('OAI_LIB', realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR. + 'lib'.DIRECTORY_SEPARATOR. + 'oaipmh'.DIRECTORY_SEPARATOR); + +require_once(OAI_LIB.'oaidp-config.php'); +require_once(OAI_LIB.'oaidp-util.php'); +require_once(OAI_LIB.'ands_tpa.php'); + + +/** + * Supported attributes associate to verbs. + */ +$attribs = array ('from', 'identifier', 'metadataPrefix', 'set', 'resumptionToken', 'until'); + +if (in_array($_SERVER['REQUEST_METHOD'],array('GET','POST'))) { + $args = $_REQUEST; +} else { + $errors[] = oai_error('badRequestMethod', $_SERVER['REQUEST_METHOD']); +} + + +// Always using htmlentities() function to encodes the HTML entities submitted by others. +// No one can be trusted. +foreach ($args as $key => $val) { + $checking = urlencode(stripslashes($val)); + if (!is_valid_attrb($checking)) { + $errors[] = oai_error('badArgument', $checking); + } else {$args[$key] = $checking; } +} +if (!empty($errors)) { oai_exit(); } + +foreach($attribs as $val) { + unset($$val); +} + + + +// Create a PDO object +try { + //$db = new PDO($DSN); + $db = new PDO($DSN, $DB_USER, $DB_PASSWD); +} catch (PDOException $e) { + exit('Connection failed: ' . $e->getMessage()); +} + +// For generic usage or just trying: +// require_once('xml_creater.php'); +// In common cases, you have to implement your own code to act fully and correctly. + + +// Default, there is no compression supported +$compress = FALSE; +if (isset($compression) && is_array($compression)) { + if (in_array('gzip', $compression) && ini_get('output_buffering')) { + $compress = TRUE; + } +} + +if (SHOW_QUERY_ERROR) { + echo "Args:\n"; print_r($args); +} + +if (isset($args['verb'])) { + switch ($args['verb']) { + + case 'Identify': + // we never use compression in Identify + $compress = FALSE; + if(count($args)>1) { + foreach($args as $key => $val) { + if(strcmp($key,"verb")!=0) { + $errors[] = oai_error('badArgument', $key, $val); + } + } + } + if (empty($errors)) include OAI_LIB.'identify.php'; + break; + + case 'ListMetadataFormats': + $checkList = array("ops"=>array("identifier")); + checkArgs($args, $checkList); + if (empty($errors)) include OAI_LIB.'listmetadataformats.php'; + break; + + case 'ListSets': + if(isset($args['resumptionToken']) && count($args) > 2) { + $errors[] = oai_error('exclusiveArgument'); + } + $checkList = array("ops"=>array("resumptionToken")); + checkArgs($args, $checkList); + if (empty($errors)) include OAI_LIB.'listsets.php'; + break; + + case 'GetRecord': + $checkList = array("required"=>array("metadataPrefix","identifier")); + checkArgs($args, $checkList); + if (empty($errors)) include OAI_LIB.'getrecord.php'; + break; + + case 'ListIdentifiers': + case 'ListRecords': + if(isset($args['resumptionToken'])) { + if (count($args) > 2) { + $errors[] = oai_error('exclusiveArgument'); + } + $checkList = array("ops"=>array("resumptionToken")); + } else { + $checkList = array("required"=>array("metadataPrefix"),"ops"=>array("from","until","set")); + } + checkArgs($args, $checkList); + if (empty($errors)) include OAI_LIB.'listrecords.php'; + break; + + default: + // we never use compression with errors + $compress = FALSE; + $errors[] = oai_error('badVerb', $args['verb']); + } /*switch */ +} else { + $errors[] = oai_error('noVerb'); +} + +if (!empty($errors)) { oai_exit(); } + +if ($compress) { + ob_start('ob_gzhandler'); +} + +header(CONTENT_TYPE); + +if(isset($outputObj)) { + $outputObj->display(); +} else { + exit("There is a bug in codes"); +} + + if ($compress) { + ob_end_flush(); + } + +?>