Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1433107
JsonLdRdfWriter.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
JsonLdRdfWriter.php
View Options
<?php
namespace
Wikimedia\Purtle
;
use
LogicException
;
/**
* RdfWriter implementation for generating JSON-LD output.
*
* @license GPL-2.0-or-later
* @author C. Scott Ananian
*/
class
JsonLdRdfWriter
extends
RdfWriterBase
{
/**
* The JSON-LD "@context", which maps terms to IRIs. This is shared with all sub-writers, and a
* single context is emitted when the writer is finalized.
*
* @see https://www.w3.org/TR/json-ld/#the-context
*
* @var array[]
*/
protected
$context
=
[];
/**
* A set of predicates which rely on the default typing rules for
* JSON-LD; that is, values for the predicate have been emitted which
* would be broken if an explicit "@type" was added to the context
* for the predicate.
*
* @var bool[]
*/
protected
$defaulted
=
[];
/**
* The JSON-LD "@graph", which lists all the nodes described by this JSON-LD object.
* We apply an optimization eliminating the "@graph" entry if it consists
* of a single node; in that case, we will set $this->graph to null in
* #finishJson() to ensure that the deferred callback in #finishDocument()
* doesn't later emit "@graph".
*
* @see https://www.w3.org/TR/json-ld/#named-graphs
*
* @var array|null
*/
private
$graph
=
[];
/**
* A collection of predicates about a specific subject. The
* subject is identified by the "@id" key in this array; the other
* keys identify JSON-LD properties.
*
* @see https://www.w3.org/TR/json-ld/#dfn-edge
*
* @var array
*/
private
$predicates
=
[];
/**
* A sequence of zero or more IRIs, nodes, or values, which are the
* destination targets of the current predicates.
*
* @see https://www.w3.org/TR/json-ld/#dfn-list
*
* @var array
*/
private
$values
=
[];
/**
* True iff we have written the opening of the "@graph" field.
*
* @var bool
*/
private
$wroteGraph
=
false
;
/**
* JSON-LD objects describing a single node can omit the "@graph" field;
* this variable remains false only so long as we can guarantee that
* only a single node has been described.
*
* @var bool
*/
private
$disableGraphOpt
=
false
;
/**
* The IRI for the RDF `type` property.
*/
private
const
RDF_TYPE_IRI
=
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
;
/**
* The type internally used for "default type", which is a string or
* otherwise default-coerced type.
*/
private
const
DEFAULT_TYPE
=
'@purtle@default@'
;
/**
* @param string $role
* @param BNodeLabeler|null $labeler
*/
public
function
__construct
(
$role
=
parent
::
DOCUMENT_ROLE
,
?
BNodeLabeler
$labeler
=
null
)
{
parent
::
__construct
(
$role
,
$labeler
);
// The following named methods are protected, not private, so we
// can invoke them directly w/o function wrappers.
$this
->
transitionTable
[
self
::
STATE_START
][
self
::
STATE_DOCUMENT
]
=
[
$this
,
'beginJson'
];
$this
->
transitionTable
[
self
::
STATE_DOCUMENT
][
self
::
STATE_FINISH
]
=
[
$this
,
'finishJson'
];
$this
->
transitionTable
[
self
::
STATE_OBJECT
][
self
::
STATE_PREDICATE
]
=
[
$this
,
'finishPredicate'
];
$this
->
transitionTable
[
self
::
STATE_OBJECT
][
self
::
STATE_SUBJECT
]
=
[
$this
,
'finishSubject'
];
$this
->
transitionTable
[
self
::
STATE_OBJECT
][
self
::
STATE_DOCUMENT
]
=
[
$this
,
'finishDocument'
];
}
/**
* Emit $val as JSON, with $indent extra indentations on each line.
* @param array $val
* @param int $indent
* @return string the JSON string for $val
*/
public
function
encode
(
$val
,
$indent
)
{
$str
=
json_encode
(
$val
,
JSON_PRETTY_PRINT
|
JSON_UNESCAPED_SLASHES
);
// Strip outermost open/close braces/brackets
$str
=
preg_replace
(
'/^[[{]
\n
?|
\n
?[}
\]
]$/'
,
''
,
$str
);
if
(
$indent
>
0
)
{
// add extra indentation
$str
=
preg_replace
(
'/^/m'
,
str_repeat
(
' '
,
$indent
),
$str
);
}
return
$str
;
}
/**
* Return a "compact IRI" corresponding to the given base/local pair.
* This adds entries to the "@context" key when needed to allow use
* of a given prefix.
* @see https://www.w3.org/TR/json-ld/#dfn-compact-iri
*
* @param string $base A QName prefix if $local is given, or an IRI if $local is null.
* @param string|null $local A QName suffix, or null if $base is an IRI.
*
* @return string A compact IRI.
*/
private
function
compactify
(
$base
,
$local
=
null
)
{
$this
->
expandShorthand
(
$base
,
$local
);
if
(
$local
===
null
)
{
return
$base
;
}
else
{
if
(
$base
!==
'_'
&&
isset
(
$this
->
prefixes
[
$base
]
)
)
{
if
(
$base
===
''
)
{
// Empty prefixes are not supported; use full IRI
return
$this
->
prefixes
[
$base
]
.
$local
;
}
if
(
!
isset
(
$this
->
context
[
$base
]
)
)
{
$this
->
context
[
$base
]
=
$this
->
prefixes
[
$base
];
}
if
(
$this
->
context
[
$base
]
!==
$this
->
prefixes
[
$base
]
)
{
// Context name conflict; use full IRI
return
$this
->
prefixes
[
$base
]
.
$local
;
}
}
return
$base
.
':'
.
$local
;
}
}
/**
* Return an absolute IRI from the given base/local pair.
* @see https://www.w3.org/TR/json-ld/#dfn-absolute-iri
*
* @param string $base A QName prefix if $local is given, or an IRI if $local is null.
* @param string|null $local A QName suffix, or null if $base is an IRI.
*
* @return string|null An absolute IRI, or null if it cannot be constructed.
*/
private
function
toIRI
(
$base
,
$local
)
{
$this
->
expandShorthand
(
$base
,
$local
);
$this
->
expandQName
(
$base
,
$local
);
if
(
$local
!==
null
)
{
throw
new
LogicException
(
'Unknown prefix: '
.
$base
);
}
return
$base
;
}
/**
* Return a appropriate term for the current predicate value.
*
* @return string
*/
private
function
getCurrentTerm
()
{
[
$base
,
$local
]
=
$this
->
currentPredicate
;
$predIRI
=
$this
->
toIRI
(
$base
,
$local
);
if
(
$predIRI
===
self
::
RDF_TYPE_IRI
)
{
return
$predIRI
;
}
$this
->
expandShorthand
(
$base
,
$local
);
if
(
$local
===
null
)
{
return
$base
;
}
elseif
(
$base
!==
'_'
&&
!
isset
(
$this
->
prefixes
[
$local
]
)
)
{
// Prefixes get priority over field names in @context
$pred
=
$this
->
compactify
(
$base
,
$local
);
if
(
!
isset
(
$this
->
context
[
$local
]
)
)
{
$this
->
context
[
$local
]
=
[
'@id'
=>
$pred
];
}
if
(
$this
->
context
[
$local
][
'@id'
]
===
$pred
)
{
return
$local
;
}
return
$pred
;
}
return
$this
->
compactify
(
$base
,
$local
);
}
/**
* Write document header.
*/
protected
function
beginJson
()
{
if
(
$this
->
role
===
self
::
DOCUMENT_ROLE
)
{
$this
->
write
(
"{
\n
"
);
$this
->
write
(
function
()
{
// If this buffer is drained early, disable @graph optimization
$this
->
disableGraphOpt
=
true
;
return
''
;
}
);
}
}
/**
* Write document footer.
*/
protected
function
finishJson
()
{
// If we haven't drained yet, and @graph has only 1 element, then we
// can optimize our output and hoist the single node to top level.
if
(
$this
->
role
===
self
::
DOCUMENT_ROLE
)
{
if
(
(
!
$this
->
disableGraphOpt
)
&&
count
(
$this
->
graph
)
===
1
)
{
$this
->
write
(
$this
->
encode
(
$this
->
graph
[
0
],
0
)
);
$this
->
graph
=
null
;
// We're done with @graph.
}
else
{
$this
->
disableGraphOpt
=
true
;
$this
->
write
(
"
\n
]"
);
}
}
if
(
count
(
$this
->
context
)
)
{
// Write @context field.
$this
->
write
(
",
\n
"
);
$this
->
write
(
$this
->
encode
(
[
'@context'
=>
$this
->
context
],
0
)
);
}
$this
->
write
(
"
\n
}"
);
}
protected
function
finishDocument
()
{
$this
->
finishSubject
();
$this
->
write
(
function
()
{
// if this is drained before finishJson(), then disable
// the graph optimization and dump what we've got so far.
$str
=
''
;
if
(
$this
->
graph
!==
null
&&
count
(
$this
->
graph
)
>
0
)
{
$this
->
disableGraphOpt
=
true
;
if
(
$this
->
role
===
self
::
DOCUMENT_ROLE
&&
!
$this
->
wroteGraph
)
{
$str
.=
"
\"
@graph
\"
: [
\n
"
;
$this
->
wroteGraph
=
true
;
}
else
{
$str
.=
",
\n
"
;
}
$str
.=
$this
->
encode
(
$this
->
graph
,
1
);
$this
->
graph
=
[];
return
$str
;
}
// Delay; maybe we'll be able to optimize this later.
return
$str
;
}
);
}
/**
* @param string $base
* @param string|null $local
*/
protected
function
writeSubject
(
$base
,
$local
=
null
)
{
$this
->
predicates
=
[
'@id'
=>
$this
->
compactify
(
$base
,
$local
)
];
}
protected
function
finishSubject
()
{
$this
->
finishPredicate
();
$this
->
graph
[]
=
$this
->
predicates
;
}
/**
* @param string $base
* @param string|null $local
*/
protected
function
writePredicate
(
$base
,
$local
=
null
)
{
// no op
}
/**
* @param string $base
* @param string|null $local
*/
protected
function
writeResource
(
$base
,
$local
=
null
)
{
$pred
=
$this
->
getCurrentTerm
();
$value
=
$this
->
compactify
(
$base
,
$local
);
$this
->
addTypedValue
(
'@id'
,
$value
,
[
'@id'
=>
$value
],
(
$pred
===
self
::
RDF_TYPE_IRI
)
);
}
/**
* @param string $text
* @param string|null $language
*/
protected
function
writeText
(
$text
,
$language
=
null
)
{
if
(
!
$this
->
isValidLanguageCode
(
$language
)
)
{
$this
->
addTypedValue
(
self
::
DEFAULT_TYPE
,
$text
);
}
else
{
$expanded
=
[
'@language'
=>
$language
,
'@value'
=>
$text
];
$this
->
addTypedValue
(
self
::
DEFAULT_TYPE
,
$expanded
,
$expanded
);
}
}
/**
* @param string $literal
* @param string|null $typeBase
* @param string|null $typeLocal
*/
public
function
writeValue
(
$literal
,
$typeBase
,
$typeLocal
=
null
)
{
if
(
$typeBase
===
null
&&
$typeLocal
===
null
)
{
$this
->
addTypedValue
(
self
::
DEFAULT_TYPE
,
$literal
);
return
;
}
switch
(
$this
->
toIRI
(
$typeBase
,
$typeLocal
)
)
{
case
'http://www.w3.org/2001/XMLSchema#string'
:
$this
->
addTypedValue
(
self
::
DEFAULT_TYPE
,
strval
(
$literal
)
);
return
;
case
'http://www.w3.org/2001/XMLSchema#integer'
:
$this
->
addTypedValue
(
self
::
DEFAULT_TYPE
,
intval
(
$literal
)
);
return
;
case
'http://www.w3.org/2001/XMLSchema#boolean'
:
$this
->
addTypedValue
(
self
::
DEFAULT_TYPE
,
(
$literal
===
'true'
)
);
return
;
case
'http://www.w3.org/2001/XMLSchema#double'
:
$v
=
floatval
(
$literal
);
// Only "numbers with fractions" are xsd:double. We need
// to verify that the JSON string will contain a decimal
// point, otherwise the value would be interpreted as an
// xsd:integer.
// TODO: consider instead using JSON_PRESERVE_ZERO_FRACTION
// in $this->encode() once our required PHP >= 5.6.6.
// OTOH, the spec language is ambiguous about whether "5."
// would be considered an integer or a double.
if
(
strpos
(
json_encode
(
$v
),
'.'
)
!==
false
)
{
$this
->
addTypedValue
(
self
::
DEFAULT_TYPE
,
$v
);
return
;
}
}
$type
=
$this
->
compactify
(
$typeBase
,
$typeLocal
);
$literal
=
strval
(
$literal
);
$this
->
addTypedValue
(
$type
,
$literal
,
[
'@type'
=>
$type
,
'@value'
=>
$literal
]
);
}
/**
* Add a typed value for the given predicate. If possible, adds a
* default type to the context to avoid having to repeat type information
* in each value for this predicate. If there is already a default
* type which conflicts with this one, or if $forceExpand is true,
* then use the "expanded" value which will explicitly override any
* default type.
*
* @param string $type The compactified JSON-LD @type for this value, or
* self::DEFAULT_TYPE to indicate the default JSON-LD type coercion rules
* should be used.
* @param string|int|float|bool $simpleVal The "simple" representation
* for this value, used if the type can be hoisted into the context.
* @param array|null $expandedVal The "expanded" representation for this
* value, used if the context @type conflicts with this value; or null
* to use "@value" for the expanded representation.
* @param bool $forceExpand If true, don't try to add this type to the
* context. Defaults to false.
*/
protected
function
addTypedValue
(
$type
,
$simpleVal
,
$expandedVal
=
null
,
$forceExpand
=
false
)
{
if
(
!
$forceExpand
)
{
$pred
=
$this
->
getCurrentTerm
();
if
(
$type
===
self
::
DEFAULT_TYPE
)
{
if
(
!
isset
(
$this
->
context
[
$pred
][
'@type'
]
)
)
{
$this
->
defaulted
[
$pred
]
=
true
;
}
if
(
isset
(
$this
->
defaulted
[
$pred
]
)
)
{
$this
->
values
[]
=
$simpleVal
;
return
;
}
}
elseif
(
!
isset
(
$this
->
defaulted
[
$pred
]
)
)
{
if
(
!
isset
(
$this
->
context
[
$pred
]
)
)
{
$this
->
context
[
$pred
]
=
[];
}
if
(
!
isset
(
$this
->
context
[
$pred
][
'@type'
]
)
)
{
$this
->
context
[
$pred
][
'@type'
]
=
$type
;
}
if
(
$this
->
context
[
$pred
][
'@type'
]
===
$type
)
{
$this
->
values
[]
=
$simpleVal
;
return
;
}
}
}
if
(
$expandedVal
===
null
)
{
$this
->
values
[]
=
[
'@value'
=>
$simpleVal
];
}
else
{
$this
->
values
[]
=
$expandedVal
;
}
}
protected
function
finishPredicate
()
{
$name
=
$this
->
getCurrentTerm
();
if
(
$name
===
self
::
RDF_TYPE_IRI
)
{
$name
=
'@type'
;
$this
->
values
=
array_map
(
static
function
(
array
$val
)
{
return
$val
[
'@id'
];
},
$this
->
values
);
}
if
(
isset
(
$this
->
predicates
[
$name
]
)
)
{
$was
=
$this
->
predicates
[
$name
];
// Wrap $was into a numeric indexed array if it isn't already.
// Note that $was could have non-numeric indices, eg
// [ "@id" => "foo" ], in which was it still needs to be wrapped.
if
(
!(
is_array
(
$was
)
&&
isset
(
$was
[
0
]
)
)
)
{
$was
=
[
$was
];
}
$this
->
values
=
array_merge
(
$was
,
$this
->
values
);
}
$cnt
=
count
(
$this
->
values
);
if
(
$cnt
===
0
)
{
throw
new
LogicException
(
'finishPredicate can
\'
t be called without at least one value'
);
}
elseif
(
$cnt
===
1
)
{
$this
->
predicates
[
$name
]
=
$this
->
values
[
0
];
}
else
{
$this
->
predicates
[
$name
]
=
$this
->
values
;
}
$this
->
values
=
[];
}
/**
* @param string $role
* @param BNodeLabeler $labeler
*
* @return RdfWriterBase
*/
protected
function
newSubWriter
(
$role
,
BNodeLabeler
$labeler
)
{
$writer
=
new
self
(
$role
,
$labeler
);
// Have subwriter share context with this parent.
$writer
->
context
=
&
$this
->
context
;
$writer
->
defaulted
=
&
$this
->
defaulted
;
// We can't use the @graph optimization.
$this
->
disableGraphOpt
=
true
;
return
$writer
;
}
/**
* @return string a MIME type
*/
public
function
getMimeType
()
{
return
'application/ld+json; charset=UTF-8'
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 22:39 (1 h, 47 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d1/58/a0d4baf8b7237f24e7d3716303e0
Default Alt Text
JsonLdRdfWriter.php (13 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment