1<?php 2 3namespace Wikimedia\Purtle; 4 5use InvalidArgumentException; 6 7/** 8 * XML/RDF implementation of RdfWriter 9 * 10 * @license GPL-2.0-or-later 11 * @author Daniel Kinzler 12 */ 13class XmlRdfWriter extends RdfWriterBase { 14 15 /** 16 * @param string $role 17 * @param BNodeLabeler|null $labeler 18 */ 19 public function __construct( $role = parent::DOCUMENT_ROLE, BNodeLabeler $labeler = null ) { 20 parent::__construct( $role, $labeler ); 21 22 $this->transitionTable[self::STATE_START][self::STATE_DOCUMENT] = function () { 23 $this->beginDocument(); 24 }; 25 $this->transitionTable[self::STATE_DOCUMENT][self::STATE_FINISH] = function () { 26 $this->finishDocument(); 27 }; 28 $this->transitionTable[self::STATE_OBJECT][self::STATE_DOCUMENT] = function () { 29 $this->finishSubject(); 30 }; 31 $this->transitionTable[self::STATE_OBJECT][self::STATE_SUBJECT] = function () { 32 $this->finishSubject(); 33 }; 34 } 35 36 /** 37 * @param string $text 38 * 39 * @return string 40 */ 41 private function escape( $text ) { 42 return htmlspecialchars( $text, ENT_QUOTES ); 43 } 44 45 protected function expandSubject( &$base, &$local ) { 46 $this->expandQName( $base, $local ); 47 } 48 49 protected function expandPredicate( &$base, &$local ) { 50 $this->expandShorthand( $base, $local ); 51 } 52 53 protected function expandResource( &$base, &$local ) { 54 $this->expandQName( $base, $local ); 55 } 56 57 protected function expandType( &$base, &$local ) { 58 $this->expandQName( $base, $local ); 59 } 60 61 /** 62 * @param string $ns 63 * @param string $name 64 * @param string[] $attributes 65 * @param string|null $content 66 */ 67 private function tag( $ns, $name, $attributes = [], $content = null ) { 68 $sep = $ns === '' ? '' : ':'; 69 $this->write( '<' . $ns . $sep . $name ); 70 71 foreach ( $attributes as $attr => $value ) { 72 if ( is_int( $attr ) ) { 73 // positional array entries are passed verbatim, may be callbacks. 74 $this->write( $value ); 75 continue; 76 } 77 78 $this->write( " $attr=\"" . $this->escape( $value ) . '"' ); 79 } 80 81 if ( $content === null ) { 82 $this->write( '>' ); 83 } elseif ( $content === '' ) { 84 $this->write( '/>' ); 85 } else { 86 $this->write( '>' . $content ); 87 $this->close( $ns, $name ); 88 } 89 } 90 91 /** 92 * @param string $ns 93 * @param string $name 94 */ 95 private function close( $ns, $name ) { 96 $sep = $ns === '' ? '' : ':'; 97 $this->write( '</' . $ns . $sep . $name . '>' ); 98 } 99 100 /** 101 * Generates an attribute list, containing the attribute given by $name, or rdf:nodeID 102 * if $target is a blank node id (starting with "_:"). If $target is a qname, an attempt 103 * is made to resolve it into a full IRI based on the namespaces registered by calling 104 * prefix(). 105 * 106 * @param string $name the attribute name (without the 'rdf:' prefix) 107 * @param string|null $base 108 * @param string|null $local 109 * 110 * @throws InvalidArgumentException 111 * @return string[] 112 */ 113 private function getTargetAttributes( $name, $base, $local ) { 114 if ( $base === null && $local === null ) { 115 return []; 116 } 117 118 // handle blank 119 if ( $base === '_' ) { 120 $name = 'nodeID'; 121 $value = $local; 122 } elseif ( $local !== null ) { 123 throw new InvalidArgumentException( "Expected IRI, got QName: $base:$local" ); 124 } else { 125 $value = $base; 126 } 127 128 return [ 129 "rdf:$name" => $value 130 ]; 131 } 132 133 /** 134 * Emit a document header. 135 */ 136 private function beginDocument() { 137 $this->write( "<?xml version=\"1.0\"?>\n" ); 138 139 // define a callback for generating namespace attributes 140 $namespaceAttrCallback = function () { 141 $attr = ''; 142 143 $namespaces = $this->getPrefixes(); 144 foreach ( $namespaces as $ns => $uri ) { 145 $escapedUri = htmlspecialchars( $uri, ENT_QUOTES ); 146 $nss = $ns === '' ? '' : ":$ns"; 147 $attr .= " xmlns$nss=\"$escapedUri\""; 148 } 149 150 return $attr; 151 }; 152 153 $this->tag( 'rdf', 'RDF', [ $namespaceAttrCallback ] ); 154 $this->write( "\n" ); 155 } 156 157 /** 158 * @param string $base 159 * @param string|null $local 160 */ 161 protected function writeSubject( $base, $local = null ) { 162 $attr = $this->getTargetAttributes( 'about', $base, $local ); 163 164 $this->write( "\t" ); 165 $this->tag( 'rdf', 'Description', $attr ); 166 $this->write( "\n" ); 167 } 168 169 /** 170 * Emit the root element 171 */ 172 private function finishSubject() { 173 $this->write( "\t" ); 174 $this->close( 'rdf', 'Description' ); 175 $this->write( "\n" ); 176 } 177 178 /** 179 * Write document footer 180 */ 181 private function finishDocument() { 182 // close document element 183 $this->close( 'rdf', 'RDF' ); 184 $this->write( "\n" ); 185 } 186 187 /** 188 * @param string $base 189 * @param string|null $local 190 */ 191 protected function writePredicate( $base, $local = null ) { 192 // noop 193 } 194 195 /** 196 * @param string $base 197 * @param string|null $local 198 */ 199 protected function writeResource( $base, $local = null ) { 200 $attr = $this->getTargetAttributes( 'resource', $base, $local ); 201 202 $this->write( "\t\t" ); 203 $this->tag( $this->currentPredicate[0], $this->currentPredicate[1], $attr, '' ); 204 $this->write( "\n" ); 205 } 206 207 /** 208 * @param string $text 209 * @param string|null $language 210 */ 211 protected function writeText( $text, $language = null ) { 212 $attr = $this->isValidLanguageCode( $language ) 213 ? [ 'xml:lang' => $language ] 214 : []; 215 216 $this->write( "\t\t" ); 217 $this->tag( 218 $this->currentPredicate[0], 219 $this->currentPredicate[1], 220 $attr, 221 $this->escape( $text ) 222 ); 223 $this->write( "\n" ); 224 } 225 226 /** 227 * @param string $literal 228 * @param string|null $typeBase 229 * @param string|null $typeLocal 230 */ 231 public function writeValue( $literal, $typeBase, $typeLocal = null ) { 232 $attr = $this->getTargetAttributes( 'datatype', $typeBase, $typeLocal ); 233 234 $this->write( "\t\t" ); 235 $this->tag( 236 $this->currentPredicate[0], 237 $this->currentPredicate[1], 238 $attr, 239 $this->escape( $literal ) 240 ); 241 $this->write( "\n" ); 242 } 243 244 /** 245 * @param string $role 246 * @param BNodeLabeler $labeler 247 * 248 * @return RdfWriterBase 249 */ 250 protected function newSubWriter( $role, BNodeLabeler $labeler ) { 251 $writer = new self( $role, $labeler ); 252 253 return $writer; 254 } 255 256 /** 257 * @return string a MIME type 258 */ 259 public function getMimeType() { 260 return 'application/rdf+xml; charset=UTF-8'; 261 } 262 263} 264