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