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