Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rdf] Support for translated literal object values #383

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ script:
- docker version
- docker-compose version
- docker-compose run --rm zotonic make
- docker-compose run zotonic bin/zotonic runtests mod_ginger_activity mod_ginger_collection mod_ginger_rdf
- docker-compose run zotonic bin/zotonic runtests mod_ginger_activity mod_ginger_collection mod_ginger_rdf m_rdf

notifications:
slack:
Expand Down
71 changes: 64 additions & 7 deletions modules/mod_ginger_rdf/models/m_rdf.erl
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ m_find_value(id, #m{value = #rdf_resource{id = Id}}, _Context) ->
Id;
m_find_value(uri, #m{value = #rdf_resource{id = Id}}, _Context) ->
Id;
m_find_value(Predicate, #m{value = #rdf_resource{triples = Triples}}, _Context) ->
m_find_value(Predicate, #m{value = #rdf_resource{triples = Triples}}, Context) ->
case lookup_triple(Predicate, Triples) of
undefined -> undefined;
#triple{object = Object} -> literal_object_value(Object)
#triple{object = Object} -> literal_object_value(Object, Context)
end.

m_to_list(_, _Context) ->
Expand Down Expand Up @@ -622,10 +622,67 @@ to_json_ld(Id, Context) ->
jsx:encode(ginger_json_ld:serialize_to_map(to_triples(Id, Context))).

%% @doc Extract literal value from triple object(s).
-spec literal_object_value([#rdf_value{}] | #rdf_value{} | any()) -> binary() | list().
literal_object_value(Objects) when is_list(Objects) ->
[literal_object_value(Object) || Object <- Objects];
literal_object_value(#rdf_value{value = Value}) ->
-spec literal_object_value([#rdf_value{}] | #rdf_value{} | any(), #context{}) -> binary() | list().
literal_object_value(Objects, Context) when is_list(Objects) ->
literal_object_value_translation(Objects, Context);
literal_object_value(#rdf_value{value = Value}, _Context) ->
Value;
literal_object_value(Other) ->
literal_object_value(Other, _Context) ->
Other.

%% @doc Determine whether a given language matches the expected (or desired) language.
%% E.g. given language <<"en-GB">> should match :en as well as <<"en">>.
-spec language_matches(binary(), atom() | binary()) -> boolean().
language_matches(LanguageGiven, LanguageExpected) when is_atom(LanguageExpected) ->
language_matches(LanguageGiven, atom_to_binary(LanguageExpected, utf8));
language_matches(LanguageGiven, LanguageExpected) when is_binary(LanguageGiven), is_binary(LanguageExpected) ->
case binary:match(LanguageGiven, LanguageExpected) of
{0, _} ->
true;
_ ->
false
end;
language_matches(_, _) ->
false.

%% @doc Select matching translations and untranslated values from a list of #rdf_value.
-spec literal_object_value_translation([#rdf_value{}] | any(), #context{}) -> binary() | list().
literal_object_value_translation([#rdf_value{}|_] = Objects, #context{} = Context) ->
Language = z_context:language(Context),
% First try to match the preferred language
% nl as set language should match values like nl-NL
{ Translations, OtherValues } =
lists:foldr(
fun( Object, { Ts, OVs } ) ->
case Object of
#rdf_value{value = V, language = undefined} ->
{ Ts, [V|OVs] };
#rdf_value{value = _, language = _} ->
{ [Object|Ts], OVs };
_ ->
{ Ts, [Object|OVs] }
end
end,
{ [], [] },
Objects
),
Translated =
case [V || #rdf_value{value = V, language = L} <- Translations, language_matches(L, Language)] of
[] ->
% Fall back to default language
% Should we also see if we have a z_context:fallback_language()?
DefaultLanguage = z_trans:default_language(Context),
[V || #rdf_value{value = V, language = L} <- Translations, language_matches(L, DefaultLanguage)];
Vs -> Vs
end,
% Include any untranslated values
case Translated ++ OtherValues of
[] ->
undefined;
[SingleValue] ->
SingleValue;
Values ->
Values
end;
literal_object_value_translation(Objects, _Context) ->
Objects.
98 changes: 98 additions & 0 deletions modules/mod_ginger_rdf/tests/m_rdf_tests.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
-module(m_rdf_tests).

-include_lib("eunit/include/eunit.hrl").
-include("zotonic.hrl").
-include("../include/rdf.hrl").

m_find_value_test() ->
Input = #rdf_resource{
id = <<"http://dinges.com/123">>,
triples = [
#triple{
subject = <<"http://dinges.com/123">>,
predicate = <<"dc:author">>,
object = [
#rdf_value{
value = <<"Author 1">>
},
#rdf_value{
value = <<"Author 2">>
}
]
Copy link
Member

@ddeboer ddeboer Jun 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct: currently a triple’s object is defined as

object :: binary() | #rdf_value{},

so either a single URI (binary) or a single #rdf_value{}, but never a list.

In RDF, you would have 2 separate triples to describe this data.

},
#triple{
subject = <<"http://dinges.com/123">>,
predicate = <<"single_value">>,
object = #rdf_value{
value = <<"Value1">>
}
},
#triple{
subject = <<"http://dinges.com/123">>,
predicate = <<"single_value_in_list">>,
object = [#rdf_value{
value = <<"Value2">>
}]
},
#triple{
subject = <<"http://dinges.com/123">>,
predicate = <<"nonrecord_content_in_list">>,
object = [<<"Value3">>]
},
#triple{
subject = <<"http://dinges.com/123">>,
predicate = <<"empty_list_object">>,
object = []
},
#triple{
subject = <<"http://dinges.com/123">>,
predicate = <<"nonrecord_content">>,
object = <<"Value">>
},
#triple{
subject = <<"http://dinges.com/123">>,
predicate = <<"dc:title">>,
object = [
#rdf_value{
language = <<"nl-NL">>,
value = <<"Goed verhaal broer!">>
},
#rdf_value{
language = <<"en-US">>,
value = <<"Good story bro!">>
}
]
}
]
},
Context = z_context:new(testsandboxdb),
ContextWithNonPresentLanguage = z_context:set_language(de, Context),
?assertEqual(
<<"Good story bro!">>,
m_rdf:m_find_value(<<"dc:title">>, #m{value = Input}, ContextWithNonPresentLanguage)
),
ContextWithLanguage = z_context:set_language(nl, Context),
?assertEqual(
[<<"Author 1">>, <<"Author 2">>],
m_rdf:m_find_value(<<"dc:author">>, #m{value = Input}, ContextWithLanguage)
),
?assertEqual(
<<"Value">>,
m_rdf:m_find_value(<<"nonrecord_content">>, #m{value = Input}, ContextWithLanguage)
),
?assertEqual(
<<"Value1">>,
m_rdf:m_find_value(<<"single_value">>, #m{value = Input}, ContextWithLanguage)
),
?assertEqual(
<<"Value2">>,
m_rdf:m_find_value(<<"single_value_in_list">>, #m{value = Input}, ContextWithLanguage)
),
?assertEqual(
[<<"Value3">>],
m_rdf:m_find_value(<<"nonrecord_content_in_list">>, #m{value = Input}, ContextWithLanguage)
),
?assertEqual(
<<"Goed verhaal broer!">>,
m_rdf:m_find_value(<<"dc:title">>, #m{value = Input}, ContextWithLanguage)
).