defmodule RDF.XML.Decoder.ElementNode do @moduledoc !""" see https://www.w3.org/TR/rdf-syntax-grammar/#section-element-node """ defstruct [ :name, :uri, :rdf_attributes, :property_attributes, :base_uri, :ns_declarations, :language, :li_counter ] alias RDF.{PrefixMap, Graph, IRI} alias RDF.XML.Decoder @type t :: %__MODULE__{ name: String.t(), uri: RDF.IRI.t(), rdf_attributes: %{atom => any}, property_attributes: %{RDF.IRI.t() => any}, base_uri: URI.t() | nil, ns_declarations: PrefixMap.t(), language: String.t() | nil, li_counter: pos_integer } def new(name, attributes, parent_element, graph) do with {:ok, attributes, ns_declarations, base_uri, language} <- extract_xml_namespaces(attributes, parent_element, graph), {:ok, base_uri} <- normalize_base_uri(base_uri), {:ok, uri} <- qname_to_iri(name, ns_declarations), {:ok, rdf_attributes, property_attributes, _} <- attributes(attributes, ns_declarations, base_uri) do {:ok, %__MODULE__{ name: name, uri: uri, rdf_attributes: rdf_attributes, property_attributes: property_attributes, ns_declarations: ns_declarations, base_uri: base_uri, language: language, li_counter: 1 }} end end def update_name(element, name) do case qname_to_iri(name, element.ns_declarations) do {:ok, uri} -> %{ element | name: name, uri: uri } end end @doc false def normalize_base_uri("http" <> _ = base_uri) do case String.split(base_uri, "#") do [base_uri] -> {:ok, IRI.parse(base_uri)} [base_uri, _fragment] -> {:ok, IRI.parse(base_uri)} _ -> {:error, %RDF.XML.ParseError{message: "invalid base URI: #{base_uri}"}} end end def normalize_base_uri(%URI{} = base_uri), do: {:ok, base_uri} def normalize_base_uri(%IRI{} = base_uri), do: {:ok, IRI.parse(base_uri)} def normalize_base_uri(base_uri) when is_binary(base_uri), do: {:ok, IRI.parse(base_uri)} def normalize_base_uri(nil), do: {:ok, nil} defp extract_xml_namespaces(attributes, nil, graph) do extract_xml_namespaces( attributes, PrefixMap.new(), (base = Graph.base_iri(graph)) && to_string(base), graph ) end defp extract_xml_namespaces(attributes, parent_element, graph) do extract_xml_namespaces( attributes, parent_element.ns_declarations, parent_element.base_uri, graph ) end defp extract_xml_namespaces(attributes, parent_ns_declarations, parent_base_uri, _graph) do Enum.reduce_while(attributes, {:ok, %{}, parent_ns_declarations, parent_base_uri, nil}, fn {"xml:lang", value}, {:ok, attrs, ns_declarations, base_uri, _} -> {:cont, {:ok, attrs, ns_declarations, base_uri, value}} {"xml:base", value}, {:ok, attrs, ns_declarations, _, language} -> {:cont, {:ok, attrs, ns_declarations, value, language}} {"xmlns:" <> prefix, value}, {:ok, attrs, ns_declarations, base_uri, language} -> {:cont, {:ok, attrs, PrefixMap.put(ns_declarations, prefix, value), base_uri, language}} {"xmlns", value}, {:ok, attrs, ns_declarations, base_uri, language} -> {:cont, {:ok, attrs, PrefixMap.put(ns_declarations, nil, value), base_uri, language}} {name, value}, {:ok, attrs, ns_declarations, base_uri, language} -> {:cont, {:ok, Map.put(attrs, name, value), ns_declarations, base_uri, language}} end) end @exclusive_attributes ~w[node_id about id]a defp attributes(attributes, ns_declarations, base_uri) do Enum.reduce_while(attributes, {:ok, %{}, %{}, false}, fn attribute, {:ok, rdf_attributes, property_attrs, excl_attr_present} -> case attribute(attribute, ns_declarations, base_uri) do {_name, {:error, error}} -> {:halt, {:error, error}} {_, :ignore} -> {:cont, {:ok, rdf_attributes, property_attrs, excl_attr_present}} {name, value} when is_atom(name) -> excl_attr = name in @exclusive_attributes if excl_attr_present and excl_attr do {:halt, {:error, %RDF.XML.ParseError{message: "rdf:nodeID can't be used with rdf:ID and rdf:about"}}} else {:cont, {:ok, Map.put(rdf_attributes, name, value), property_attrs, excl_attr_present || excl_attr}} end {name, value} -> {:cont, {:ok, rdf_attributes, Map.put(property_attrs, name, value), excl_attr_present}} end end) end defp attribute({"rdf:ID", value}, _, base), do: {:id, rdf_id(value, base)} defp attribute({"rdf:nodeID", value}, _, _) do {:node_id, with {:ok, name} <- nc_name(value) do name end} end defp attribute({"rdf:about", value}, ns, base), do: {:about, uri_reference(value, ns, base)} defp attribute({"rdf:resource", value}, ns, base), do: {:resource, uri_reference(value, ns, base)} defp attribute({"rdf:datatype", value}, ns, base), do: {:datatype, uri_reference(value, ns, base)} defp attribute({"rdf:parseType", "Literal"}, _, _), do: {:parseLiteral, true} defp attribute({"rdf:parseType", "Resource"}, _, _), do: {:parseResource, true} defp attribute({"rdf:parseType", "Collection"}, _, _), do: {:parseCollection, true} defp attribute({"rdf:parseType", value}, _, _), do: {:parseOther, value} ~w[rdf:li rdf:RDF rdf:Description] |> Enum.each(fn term -> defp attribute({unquote(term), _}, _, _) do {unquote(term), {:error, %RDF.XML.ParseError{message: "#{unquote(term)} is not allowed as as an attribute"}}} end end) Enum.each(Decoder.old_terms(), fn term -> defp attribute({unquote(term), _}, _, _) do {unquote(term), {:error, %RDF.XML.ParseError{message: "#{unquote(term)} not supported in RDF/XML 1.1"}}} end end) defp attribute({property_attribute_name, value}, ns, _base) do case qname_to_iri(property_attribute_name, ns) do {:ok, property_attribute_uri} -> {property_attribute_uri, value} {:error, _} -> # Unrecognized attributes in the xml namespace should be ignored. {property_attribute_name, :ignore} end end @doc false def uri_reference(value, _ns_decl, base) do cond do IRI.absolute?(value) -> RDF.iri(value) is_nil(base) -> {:error, %RDF.XML.ParseError{ message: "use of a relative URI without providing a base URI: #{value}", help: "specify one in the RDF/XML document or provide one on the decoder call with the :base opt" }} true -> IRI.absolute(value, base) end end defp rdf_id(value, nil) do {:error, %RDF.XML.ParseError{ message: "use of rdf:ID without a base URI #{value}", help: "specify one in the RDF/XML document or provide one on the decoder call with the :base opt" }} end defp rdf_id(value, base) do with {:ok, name} <- nc_name(value) do URI.to_string(base) <> "#" <> name end end defp qname_to_iri(name, ns_declarations) do cond do iri = PrefixMap.prefixed_name_to_iri(ns_declarations, name) -> {:ok, iri} iri = PrefixMap.prefixed_name_to_iri(ns_declarations, ":" <> name) -> {:ok, iri} true -> {:error, %RDF.XML.ParseError{ message: "can't resolve name #{name} to URI reference", help: "provide a xmlns declaration" }} end end # TODO: NCNames are actually more restrictive; see https://www.w3.org/TR/REC-xml-names/#NT-NCName defp nc_name(<> = name) when c in ?a..?z or c in ?A..?Z or c == ?_ do if String.contains?(name, ~w[: /]) do {:error, %RDF.XML.ParseError{message: "invalid NCName #{name}"}} else {:ok, name} end end defp nc_name(name) do {:error, %RDF.XML.ParseError{message: "invalid NCName #{name}"}} end end