diff --git a/Gemfile b/Gemfile index edb00975..3564fe3b 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem "cube-ruby", require: "cube" gem "faraday", '~> 1.9' gem "rake" gem "uuid" +gem "request_store" group :test do gem "minitest", '< 5.0' diff --git a/Gemfile.lock b/Gemfile.lock index 34a6c39c..9fe7bd02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,7 +34,7 @@ GEM public_suffix (>= 2.0.2, < 6.0) builder (3.2.4) coderay (1.1.3) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.2) connection_pool (2.3.0) cube-ruby (0.0.3) daemons (1.4.1) @@ -76,10 +76,10 @@ GEM method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) minitest (4.7.5) multi_json (1.15.0) - multipart-post (2.2.3) + multipart-post (2.3.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) net-http-persistent (2.9.4) @@ -88,7 +88,7 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.1) - rack (2.2.6.2) + rack (2.2.6.3) rack-accept (0.4.5) rack (>= 0.4) rack-post-body-to-params (0.1.8) @@ -100,8 +100,10 @@ GEM addressable (>= 2.2) redis (5.0.6) redis-client (>= 0.9.0) - redis-client (0.12.1) + redis-client (0.13.0) connection_pool + request_store (1.5.1) + rack (>= 1.4) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) @@ -132,7 +134,7 @@ GEM eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) thread_safe (0.3.6) - tilt (2.0.11) + tilt (2.1.0) tzinfo (0.3.61) unf (0.1.4) unf_ext @@ -155,6 +157,7 @@ DEPENDENCIES rack-accept rack-post-body-to-params rake + request_store simplecov simplecov-cobertura sinatra diff --git a/lib/goo.rb b/lib/goo.rb index bb81541d..db863d2a 100644 --- a/lib/goo.rb +++ b/lib/goo.rb @@ -28,6 +28,7 @@ module Goo # Define the languages from which the properties values will be taken # It choose the first language that match otherwise return all the values @@main_languages = %w[en] + @@requested_language = nil @@configure_flag = false @@sparql_backends = {} @@ -54,6 +55,14 @@ def self.main_languages=(lang) @@main_languages = lang end + def self.requested_language + @@requested_language + end + + def self.requested_language=(lang) + @@requested_language = lang + end + def self.language_includes(lang) lang_str = lang.to_s main_languages.index { |l| lang_str.downcase.eql?(l) || lang_str.upcase.eql?(l)} diff --git a/lib/goo/base/resource.rb b/lib/goo/base/resource.rb index 13c4b61a..88bbc8ce 100644 --- a/lib/goo/base/resource.rb +++ b/lib/goo/base/resource.rb @@ -15,7 +15,7 @@ class Resource attr_reader :modified_attributes attr_reader :errors attr_reader :aggregates - attr_reader :unmapped + attr_writer :unmapped attr_reader :id @@ -134,17 +134,29 @@ def missing_load_attributes def unmapped_set(attribute,value) @unmapped ||= {} - (@unmapped[attribute] ||= Set.new) << value + @unmapped[attribute] ||= Set.new + @unmapped[attribute].merge(Array(value)) unless value.nil? + end + + def unmapped_get(attribute) + @unmapped[attribute] end def unmmaped_to_array cpy = {} + @unmapped.each do |attr,v| cpy[attr] = v.to_a end @unmapped = cpy end + def unmapped(*args) + @unmapped.transform_values do |language_values| + self.class.not_show_all_languages?(language_values, args) ? language_values.values.flatten: language_values + end + end + def delete(*args) if self.kind_of?(Goo::Base::Enum) raise ArgumentError, "Enums cannot be deleted" unless args[0] && args[0][:init_enum] @@ -341,13 +353,13 @@ def self.range_object(attr,id) - def self.map_attributes(inst,equivalent_predicates=nil) + def self.map_attributes(inst,equivalent_predicates=nil, include_languages: false) if (inst.kind_of?(Goo::Base::Resource) && inst.unmapped.nil?) || (!inst.respond_to?(:unmapped) && inst[:unmapped].nil?) raise ArgumentError, "Resource.map_attributes only works for :unmapped instances" end klass = inst.respond_to?(:klass) ? inst[:klass] : inst.class - unmapped = inst.respond_to?(:klass) ? inst[:unmapped] : inst.unmapped + unmapped = inst.respond_to?(:klass) ? inst[:unmapped] : inst.unmapped(include_languages: include_languages) list_attrs = klass.attributes(:list) unmapped_string_keys = Hash.new unmapped.each do |k,v| @@ -378,31 +390,18 @@ def self.map_attributes(inst,equivalent_predicates=nil) object = unmapped_string_keys[attr_uri] end - lang_filter = Goo::SPARQL::Solution::LanguageFilter.new - - object = object.map do |o| - if o.is_a?(RDF::URI) - o - else - literal = o - index, lang_val = lang_filter.main_lang_filter inst.id.to_s, attr, literal - lang_val.to_s if index.eql? :no_lang - end - end - - object = object.compact - - other_languages_values = lang_filter.other_languages_values - other_languages_values = other_languages_values[inst.id.to_s][attr] unless other_languages_values.empty? - unless other_languages_values.nil? - object = lang_filter.languages_values_to_set(other_languages_values, object) + if object.is_a?(Hash) + object = object.transform_values{|values| Array(values).map{|o|o.is_a?(RDF::URI) ? o : o.object}} + else + object = object.map {|o| o.is_a?(RDF::URI) ? o : o.object} end if klass.range(attr) object = object.map { |o| o.is_a?(RDF::URI) ? klass.range_object(attr,o) : o } end - object = object.first unless list_attrs.include?(attr) + + object = object.first unless list_attrs.include?(attr) || include_languages if inst.respond_to?(:klass) inst[attr] = object else @@ -411,11 +410,6 @@ def self.map_attributes(inst,equivalent_predicates=nil) else inst.send("#{attr}=", list_attrs.include?(attr) ? [] : nil, on_load: true) - if inst.id.to_s == "http://purl.obolibrary.org/obo/IAO_0000415" - if attr == :definition - # binding.pry - end - end end end diff --git a/lib/goo/base/settings/settings.rb b/lib/goo/base/settings/settings.rb index ce3e9a21..a58daae0 100644 --- a/lib/goo/base/settings/settings.rb +++ b/lib/goo/base/settings/settings.rb @@ -255,9 +255,18 @@ def shape_attribute(attr) self.instance_variable_set("@#{attr}",value) end define_method("#{attr}") do |*args| + attr_value = self.instance_variable_get("@#{attr}") + + if self.class.not_show_all_languages?(attr_value, args) + is_array = attr_value.values.first.is_a?(Array) + attr_value = attr_value.values.flatten + attr_value = attr_value.first unless is_array + end + + if self.class.handler?(attr) if @loaded_attributes.include?(attr) - return self.instance_variable_get("@#{attr}") + return attr_value end value = self.send("#{self.class.handler(attr)}") self.instance_variable_set("@#{attr}",value) @@ -266,7 +275,7 @@ def shape_attribute(attr) end if (not @persistent) or @loaded_attributes.include?(attr) - return self.instance_variable_get("@#{attr}") + return attr_value else # TODO: bug here when no labels from one of the main_lang available... (when it is called by ontologies_linked_data ontologies_submission) raise Goo::Base::AttributeNotLoaded, "Attribute `#{attr}` is not loaded for #{self.id}. Loaded attributes: #{@loaded_attributes.inspect}." @@ -372,6 +381,28 @@ def read_only(attributes) instance end + def show_all_languages?(args) + args.first.is_a?(Hash) && args.first.keys.include?(:include_languages) && args.first[:include_languages] + end + + def not_show_all_languages?(values, args) + values.is_a?(Hash) && !show_all_languages?(args) + end + + private + + def set_no_list_by_default(options) + if options[:enforce].nil? or !options[:enforce].include?(:list) + options[:enforce] = options[:enforce] ? (options[:enforce] << :no_list) : [:no_list] + end + end + def set_data_type(options) + if options[:type] + options[:enforce] += Array(options[:type]) + options[:enforce].uniq! + options.delete :type + end + end end end end diff --git a/lib/goo/sparql/loader.rb b/lib/goo/sparql/loader.rb index 821aba26..094fbba2 100644 --- a/lib/goo/sparql/loader.rb +++ b/lib/goo/sparql/loader.rb @@ -1,3 +1,4 @@ +require 'request_store' module Goo module SPARQL module Loader @@ -6,8 +7,10 @@ class << self def model_load(*options) options = options.last + set_request_lang(options) if options[:models] && options[:models].is_a?(Array) && \ (options[:models].length > Goo.slice_loading_size) + options = options.dup models = options[:models] include_options = options[:include] @@ -96,6 +99,9 @@ def model_load_sliced(*options) private + def set_request_lang(options) + options[:requested_lang] = RequestStore.store[:requested_lang] + end def expand_equivalent_predicates(properties_to_include, eq_p) return unless eq_p && !eq_p.empty? diff --git a/lib/goo/sparql/mixins/solution_lang_filter.rb b/lib/goo/sparql/mixins/solution_lang_filter.rb index b5254786..8980dcdc 100644 --- a/lib/goo/sparql/mixins/solution_lang_filter.rb +++ b/lib/goo/sparql/mixins/solution_lang_filter.rb @@ -1,99 +1,176 @@ -module Goo - module SPARQL - module Solution - class LanguageFilter - - def initialize - @other_languages_values = {} - end - - attr_reader :other_languages_values - - def main_lang_filter(id, attr, value) - index, value = lang_index value - save_other_lang_val(id, attr, index, value) unless index.nil? ||index.eql?(:no_lang) - [index, value] - end - - def fill_models_with_other_languages(models_by_id, list_attributes) - @other_languages_values.each do |id, languages_values| - languages_values.each do |attr, index_values| - model_attribute_val = models_by_id[id].instance_variable_get("@#{attr.to_s}") - values = languages_values_to_set(index_values, model_attribute_val) - m = models_by_id[id] - value = nil - is_struct = m.respond_to?(:klass) - if !values.nil? && list_attributes.include?(attr) - value = values || [] - - elsif !values.nil? - value = values.first || nil - end - - if value - if is_struct - m[attr] = value - else - m.send("#{attr}=", value, on_load: true) - end - end - end - end - end - - def languages_values_to_set(language_values, no_lang_values) - - values = nil - matched_lang, not_matched_lang = matched_languages(language_values, no_lang_values) - if !matched_lang.empty? - main_lang = Array(matched_lang[:'0']) + Array(matched_lang[:no_lang]) - if main_lang.empty? - secondary_languages = matched_lang.select { |key| key != :'0' && key != :no_lang }.sort.map { |x| x[1] } - values = secondary_languages.first - else - values = main_lang - end - elsif !not_matched_lang.empty? - values = not_matched_lang - end - values&.uniq - end - - private - - def lang_index(object) - return [nil, object] unless object.is_a?(RDF::Literal) - - lang = object.language - - if lang.nil? - [:no_lang, object] - else - index = Goo.language_includes(lang) - index = index ? index.to_s.to_sym : :not_matched - [index, object] - end - end - - def save_other_lang_val(id, attr, index, value) - @other_languages_values[id] ||= {} - @other_languages_values[id][attr] ||= {} - @other_languages_values[id][attr][index] ||= [] - - unless @other_languages_values[id][attr][index].include?(value.to_s) - @other_languages_values[id][attr][index] += Array(value.to_s) - end - end - - def matched_languages(index_values, model_attribute_val) - not_matched_lang = index_values[:not_matched] - matched_lang = index_values.reject { |key| key == :not_matched } - unless model_attribute_val.nil? || Array(model_attribute_val).empty? - matched_lang[:no_lang] = Array(model_attribute_val) - end - [matched_lang, not_matched_lang] - end - end - end - end -end +module Goo + module SPARQL + module Solution + class LanguageFilter + + attr_reader :requested_lang, :unmapped, :objects_by_lang + + def initialize(requested_lang: RequestStore.store[:requested_lang], unmapped: false, list_attributes: []) + @list_attributes = list_attributes + @objects_by_lang = {} + @unmapped = unmapped + @requested_lang = get_language(requested_lang) + end + + def fill_models_with_all_languages(models_by_id) + objects_by_lang.each do |id, predicates| + model = models_by_id[id] + predicates.each do |predicate, values| + + if values.values.all? { |v| v.all? { |x| literal?(x) && x.plain?} } + pull_stored_values(model, values, predicate, @unmapped) + end + end + end + end + + + def set_model_value(model, predicate, values) + set_value(model, predicate, values) do + model.send("#{predicate}=", values, on_load: true) + end + end + + def set_unmapped_value(model, predicate, value) + set_value(model, predicate, value) do + return add_unmapped_to_model(model, predicate, value) + end + end + + def models_unmapped_to_array(m) + if show_all_languages? + model_group_by_lang(m) + else + m.unmmaped_to_array + end + end + + private + + + def set_value(model, predicate, value, &block) + language = object_language(value) + + if requested_lang.eql?(:ALL) || !literal?(value) || language_match?(language) + block.call + end + + if requested_lang.eql?(:ALL) || requested_lang.is_a?(Array) + language = "@none" if language.nil? || language.eql?(:no_lang) + store_objects_by_lang(model.id, predicate, value, language) + end + end + + def model_group_by_lang(model) + unmapped = model.unmapped + cpy = {} + + unmapped.each do |attr, v| + cpy[attr] = group_by_lang(v) + end + + model.unmapped = cpy + end + + def group_by_lang(values) + + return values.to_a if values.all?{|x| x.is_a?(RDF::URI) || !x.respond_to?(:language) } + + values = values.group_by { |x| x.respond_to?(:language) && x.language ? x.language.to_s.downcase : :none } + + no_lang = values[:none] || [] + return no_lang if !no_lang.empty? && no_lang.all? { |x| x.respond_to?(:plain?) && !x.plain? } + + values + end + + + def object_language(new_value) + new_value.language || :no_lang if new_value.is_a?(RDF::Literal) + end + + def language_match?(language) + # no_lang means that the object is not a literal + return true if language.eql?(:no_lang) + + return requested_lang.include?(language) if requested_lang.is_a?(Array) + + language.eql?(requested_lang) + end + + def literal?(object) + !object_language(object).nil? + end + + def store_objects_by_lang(id, predicate, object, language) + # store objects in this format: [id][predicate][language] = [objects] + return if requested_lang.is_a?(Array) && !requested_lang.include?(language) + + language_key = language.downcase + + objects_by_lang[id] ||= {} + objects_by_lang[id][predicate] ||= {} + objects_by_lang[id][predicate][language_key] ||= [] + + objects_by_lang[id][predicate][language_key] << object + end + + + def add_unmapped_to_model(model, predicate, value) + + if model.respond_to? :klass # struct + model[:unmapped] ||= {} + model[:unmapped][predicate] ||= [] + model[:unmapped][predicate] << value unless value.nil? + else + model.unmapped_set(predicate, value) + end + end + + def pull_stored_values(model, values, predicate, unmapped) + if unmapped + add_unmapped_to_model(model, predicate, values) + else + values = values.map do |language, values_literals| + values_string = values_literals.map{|x| x.object} + values_string = values_string.first unless list_attributes?(predicate) + [language, values_string] + end.to_h + + model.send("#{predicate}=", values, on_load: true) + end + + end + + def unmapped_get(model, predicate) + if model && model.respond_to?(:klass) # struct + model[:unmapped]&.dig(predicate) + else + model.unmapped_get(predicate) + end + + end + + def list_attributes?(predicate) + @list_attributes.include?(predicate) + end + + + def show_all_languages? + @requested_lang.is_a?(Array) || @requested_lang.eql?(:ALL) + end + + def get_language(languages) + languages = portal_language if languages.nil? || languages.empty? + lang = languages.to_s.split(',').map { |l| l.upcase.to_sym } + lang.length == 1 ? lang.first : lang + end + + def portal_language + Goo.main_languages.first + end + + end + end + end +end diff --git a/lib/goo/sparql/solutions_mapper.rb b/lib/goo/sparql/solutions_mapper.rb index 77b20ae0..879c1ff7 100644 --- a/lib/goo/sparql/solutions_mapper.rb +++ b/lib/goo/sparql/solutions_mapper.rb @@ -1,461 +1,439 @@ -module Goo - module SPARQL - class SolutionMapper - - BNODES_TUPLES = Struct.new(:id, :attribute) - - def initialize(aggregate_projections, bnode_extraction, embed_struct, - incl_embed, klass_struct, models_by_id, - properties_to_include, unmapped, variables, ids, options) - - @aggregate_projections = aggregate_projections - @bnode_extraction = bnode_extraction - @embed_struct = embed_struct - @incl_embed = incl_embed - @klass_struct = klass_struct - @models_by_id = models_by_id - @properties_to_include = properties_to_include - @unmapped = unmapped - @variables = variables - @ids = ids - @klass = options[:klass] - @klass = options[:klass] - @read_only = options[:read_only] - @incl = options[:include] - @count = options[:count] - @collection = options[:collection] - end - - - - def map_each_solutions(select) - - found = Set.new - objects_new = {} - list_attributes = Set.new(@klass.attributes(:list)) - all_attributes = Set.new(@klass.attributes(:all)) - @lang_filter = Goo::SPARQL::Solution::LanguageFilter.new - - select.each_solution do |sol| - next if sol[:some_type] && @klass.type_uri(@collection) != sol[:some_type] - return sol[:count_var].object if @count - - found.add(sol[:id]) - id = sol[:id] - - create_model(id) - - if @bnode_extraction - add_bnode_to_model(sol) - next - end - - if @unmapped - add_unmapped_to_model(sol) - next - end - - if @aggregate_projections - add_aggregations_to_model(sol) - next - end - - predicate = sol[:attributeProperty].to_s.to_sym - - next if predicate.nil? || !all_attributes.include?(predicate) - - object = sol[:attributeObject] - - #bnodes - if bnode_id?(object, predicate) - objects_new = bnode_id_tuple(id, object, objects_new, predicate) - next - end - - # if multiple language values are included for a given property, set the - # corresponding model attribute to the English language value - NCBO-1662 - language, object = get_object_language(id, object, predicate) - object, objects_new = get_value_object(id, objects_new, object, list_attributes, predicate) - add_object_to_model(id, object, predicate, language) - end - @lang_filter.fill_models_with_other_languages(@models_by_id, list_attributes) - init_unloaded_attributes(found, list_attributes) - - return @models_by_id if @bnode_extraction - - model_set_collection_attributes(@models_by_id, objects_new) - - #remove from models_by_id elements that were not touched - @models_by_id.select! { |k, m| found.include?(k) } - - models_set_all_persistent(@models_by_id) unless @read_only - - #next level of embed attributes - include_embed_attributes(@incl_embed, objects_new) if @incl_embed && !@incl_embed.empty? - - #bnodes - blank_nodes = objects_new.select { |id, obj| id.is_a?(RDF::Node) && id.anonymous? } - include_bnodes(blank_nodes, @models_by_id) unless blank_nodes.empty? - - models_unmapped_to_array(@models_by_id) if @unmapped - - @models_by_id - end - - private - - def get_object_language(id, object, predicate) - @lang_filter.main_lang_filter id, predicate, object - end - - def init_unloaded_attributes(found, list_attributes) - return if @incl.nil? - - # Here we are setting to nil all attributes that have been included but not found in the triplestore - found.uniq.each do |model_id| - m = @models_by_id[model_id] - @incl.each do |attr_to_incl| - is_handler = m.respond_to?(:handler?) && m.class.handler?(attr_to_incl) - next if attr_to_incl.to_s.eql?('unmapped') || is_handler - - loaded = m.respond_to?('loaded_attributes') && m.loaded_attributes.include?(attr_to_incl) - is_list = list_attributes.include?(attr_to_incl) - is_struct = m.respond_to?(:klass) - - # Go through all models queried - if is_struct - m[attr_to_incl] = [] if is_list && m[attr_to_incl].nil? - elsif is_list && (!loaded || m.send(attr_to_incl.to_s).nil?) - m.send("#{attr_to_incl}=", [], on_load: true) - elsif !loaded && !is_list && m.respond_to?("#{attr_to_incl}=") - m.send("#{attr_to_incl}=", nil, on_load: true) - end - end - end - end - - def get_value_object(id, objects_new, object, list_attributes, predicate) - object = object.object if object && !(object.is_a? RDF::URI) - range_for_v = @klass.range(predicate) - #binding.pry if v.eql?(:enrolled) - #dependent model creation - - if object.is_a?(RDF::URI) && (predicate != :id) && !range_for_v.nil? - if objects_new.include?(object) - object = objects_new[object] - elsif !range_for_v.inmutable? - pre_val = get_preload_value(id, object, predicate) - object, objects_new = if !@read_only - preloaded_or_new_object(object, objects_new, pre_val, predicate) - else - #depedent read only - preloaded_or_new_struct(object, objects_new, pre_val, predicate) - end - else - object = range_for_v.find(object).first - end - end - - if list_attributes.include?(predicate) - # To handle attr that are lists - pre = if @klass_struct - @models_by_id[id][predicate] - else - @models_by_id[id].instance_variable_get("@#{predicate}") - end - if object.nil? && pre.nil? - object = [] - elsif object.nil? && !pre.nil? - object = pre - elsif object - object = !pre ? [object] : (pre.dup << object) - object.uniq! - end - end - [object, objects_new] - end - - def add_object_to_model(id, object, predicate, lang) - if @models_by_id[id].respond_to?(:klass) - @models_by_id[id][predicate] = object unless object.nil? && !@models_by_id[id][predicate].nil? - elsif !@models_by_id[id].class.handler?(predicate) && - !(object.nil? && !@models_by_id[id].instance_variable_get("@#{predicate}").nil?) && - predicate != :id - - if (lang&.eql?(:no_lang)) || !lang - @models_by_id[id].send("#{predicate}=", object, on_load: true) - end - - end - end - - def get_preload_value(id, object, predicate) - pre_val = nil - if predicate_preloaded?(id, predicate) - pre_val = preloaded_value(id, predicate) - pre_val = pre_val.select { |x| x.id == object }.first if pre_val.is_a?(Array) - end - pre_val - end - - def preloaded_or_new_object(object, objects_new, pre_val, predicate) - object = pre_val || @klass.range_object(predicate, object) - objects_new[object.id] = object - [object, objects_new] - end - - def preloaded_or_new_struct(object, objects_new, pre_val, predicate) - struct = pre_val || @embed_struct[predicate].new - struct.id = object - struct.klass = @klass.range(predicate) - objects_new[struct.id] = struct - [struct, objects_new] - end - - def preloaded_value(id, predicate) - if !@read_only - @models_by_id[id].instance_variable_get("@#{predicate}") - else - @models_by_id[id][predicate] - end - end - - def predicate_preloaded?(id, predicate) - @models_by_id[id] && - (@models_by_id[id].respond_to?(:klass) || @models_by_id[id].loaded_attributes.include?(predicate)) - end - - def bnode_id?(object, predicate) - object.is_a?(RDF::Node) && object.anonymous? && @incl.include?(predicate) - end - - def bnode_id_tuple(id, object, objects_new, predicate) - range = @klass.range(predicate) - if range.respond_to?(:new) - objects_new[object] = BNODES_TUPLES.new(id, predicate) - end - objects_new - end - - def add_bnode_to_model(sol) - id = sol[:id] - struct = create_struct(@bnode_extraction, @models_by_id, sol, @variables) - @models_by_id[id].send("#{@bnode_extraction}=", struct) - end - - def create_model(id) - @models_by_id[id] = create_class_model(id, @klass, @klass_struct) unless @models_by_id.include?(id) - end - - def model_set_unmapped(id, predicate, value) - - if @models_by_id[id].respond_to? :klass #struct - @models_by_id[id][:unmapped] ||= {} - (@models_by_id[id][:unmapped][predicate] ||= []) << value - else - @models_by_id[id].unmapped_set(predicate, value) - end - end - - def create_struct(bnode_extraction, models_by_id, sol, variables) - list_attributes = Set.new(@klass.attributes(:list)) - struct = @klass.range(bnode_extraction).new - variables.each do |v| - next if v == :id - svalue = sol[v] - struct[v] = svalue.is_a?(RDF::Node) ? svalue : svalue.object - end - if list_attributes.include?(bnode_extraction) - pre = models_by_id[sol[:id]].instance_variable_get("@#{bnode_extraction}") - pre = pre ? (pre.dup << struct) : [struct] - struct = pre - end - struct - end - - def create_class_model(id, klass, klass_struct) - klass_model = klass_struct ? klass_struct.new : klass.new - klass_model.id = id - klass_model.persistent = true unless klass_struct - klass_model.klass = klass if klass_struct - klass_model - end - - def models_unmapped_to_array(models_by_id) - models_by_id.each do |idm, m| - m.unmmaped_to_array - end - end - - def include_bnodes(bnodes, models_by_id) - #group by attribute - attrs = bnodes.map { |x, y| y.attribute }.uniq - attrs.each do |attr| - struct = @klass.range(attr) - - #bnodes that are in a range of goo ground models - #for example parents and children in LD class models - #we skip this cases for the moment - next if struct.respond_to?(:model_name) - - bnode_attrs = struct.new.to_h.keys - ids = bnodes.select { |x, y| y.attribute == attr }.map { |x, y| y.id } - @klass.where.models(models_by_id.select { |x, y| ids.include?(x) }.values) - .in(@collection) - .include(bnode: { attr => bnode_attrs }).all - end - end - - def include_embed_attributes(incl_embed, objects_new) - incl_embed.each do |attr, next_attrs| - #anything to join ? - attr_range = @klass.range(attr) - next if attr_range.nil? - range_objs = objects_new.select { |id, obj| - obj.instance_of?(attr_range) || (obj.respond_to?(:klass) && obj[:klass] == attr_range) - }.values - unless range_objs.empty? - range_objs.uniq! - query = attr_range.where().models(range_objs).in(@collection).include(*next_attrs) - query = query.read_only if @read_only - query.all - end - end - end - - def models_set_all_persistent(models_by_id) - return unless @ids - models_by_id.each do |k, m| - m.persistent = true - end - end - - def model_set_collection_attributes(models_by_id, objects_new) - collection_value = get_collection_value - if collection_value - collection_attribute = @klass.collection_opts - models_by_id.each do |id, m| - m.send("#{collection_attribute}=", collection_value) - end - objects_new.each do |id, obj_new| - if obj_new.respond_to?(:klass) - collection_attribute = obj_new[:klass].collection_opts - obj_new[collection_attribute] = collection_value - elsif obj_new.class.respond_to?(:collection_opts) && - obj_new.class.collection_opts.instance_of?(Symbol) - collection_attribute = obj_new.class.collection_opts - obj_new.send("#{collection_attribute}=", collection_value) - end - end - end - end - - def get_collection_value - collection_value = nil - if @klass.collection_opts.instance_of?(Symbol) - if @collection.is_a?(Array) && (@collection.length == 1) - collection_value = @collection.first - end - if @collection.respond_to? :id - collection_value = @collection - end - end - collection_value - end - - - def object_to_array(id, klass_struct, models_by_id, object, predicate) - pre = if klass_struct - models_by_id[id][predicate] - else - models_by_id[id].instance_variable_get("@#{predicate}") - end - if object.nil? && pre.nil? - object = [] - elsif object.nil? && !pre.nil? - object = pre - elsif object - object = !pre ? [object] : (pre.dup << object) - object.uniq! - end - object - end - - def dependent_model_creation(embed_struct, id, models_by_id, object, objects_new, v, options) - - read_only = options[:read_only] - if object.is_a?(RDF::URI) && v != :id - range_for_v = @klass.range(v) - if range_for_v - if objects_new.include?(object) - object = objects_new[object] - elsif !range_for_v.inmutable? - pre_val = get_pre_val(id, models_by_id, object, v, read_only) - object = get_object_from_range(pre_val, embed_struct, object, objects_new, v, options) - else - object = range_for_v.find(object).first - end - end - end - object - end - - def get_object_from_range(pre_val, embed_struct, object, objects_new, predicate) - - range_for_v = @klass.range(predicate) - if !@read_only - object = pre_val || @klass.range_object(predicate, object) - objects_new[object.id] = object - else - #depedent read only - struct = pre_val || embed_struct[predicate].new - struct.id = object - struct.klass = range_for_v - objects_new[struct.id] = struct - object = struct - end - object - end - - def get_pre_val(id, models_by_id, object, predicate) - pre_val = nil - if models_by_id[id] && - ((models_by_id[id].respond_to?(:klass) && models_by_id[id]) || - models_by_id[id].loaded_attributes.include?(predicate)) - pre_val = if !@read_only - models_by_id[id].instance_variable_get("@#{predicate}") - else - models_by_id[id][predicate] - end - - pre_val = pre_val.select { |x| x.id == object }.first if pre_val.is_a?(Array) - end - pre_val - end - - def add_unmapped_to_model(sol) - predicate = sol[:attributeProperty].to_s.to_sym - return unless @properties_to_include[predicate] - - id = sol[:id] - value = sol[:attributeObject] - - model_set_unmapped(id, @properties_to_include[predicate][:uri], value) - end - - def add_aggregations_to_model(sol) - id = sol[:id] - @aggregate_projections&.each do |aggregate_key, aggregate_val| - if @models_by_id[id].respond_to?(:add_aggregate) - @models_by_id[id].add_aggregate(aggregate_val[1], aggregate_val[0], sol[aggregate_key].object) - else - (@models_by_id[id].aggregates ||= []) << Goo::Base::AGGREGATE_VALUE.new(aggregate_val[1], - aggregate_val[0], - sol[aggregate_key].object) - end - end - end - end - end -end - +module Goo + module SPARQL + class SolutionMapper + BNODES_TUPLES = Struct.new(:id, :attribute) + + def initialize(aggregate_projections, bnode_extraction, embed_struct, + incl_embed, klass_struct, models_by_id, + properties_to_include, unmapped, variables, ids, options) + + @aggregate_projections = aggregate_projections + @bnode_extraction = bnode_extraction + @embed_struct = embed_struct + @incl_embed = incl_embed + @klass_struct = klass_struct + @models_by_id = models_by_id + @properties_to_include = properties_to_include + @unmapped = unmapped + @variables = variables + @ids = ids + @klass = options[:klass] + @read_only = options[:read_only] + @incl = options[:include] + @count = options[:count] + @collection = options[:collection] + @options = options + end + + def map_each_solutions(select) + found = Set.new + objects_new = {} + list_attributes = Set.new(@klass.attributes(:list)) + all_attributes = Set.new(@klass.attributes(:all)) + + @lang_filter = Goo::SPARQL::Solution::LanguageFilter.new(requested_lang: @options[:requested_lang].to_s, unmapped: @unmapped, + list_attributes: list_attributes) + + select.each_solution do |sol| + + next if sol[:some_type] && @klass.type_uri(@collection) != sol[:some_type] + return sol[:count_var].object if @count + + found.add(sol[:id]) + id = sol[:id] + + create_model(id) + + if @bnode_extraction + add_bnode_to_model(sol) + next + end + + if @unmapped + add_unmapped_to_model(sol) + next + end + + if @aggregate_projections + add_aggregations_to_model(sol) + next + end + + predicate = sol[:attributeProperty].to_s.to_sym + + next if predicate.nil? || !all_attributes.include?(predicate) + + object = sol[:attributeObject] + + # bnodes + if bnode_id?(object, predicate) + objects_new = bnode_id_tuple(id, object, objects_new, predicate) + next + end + + objects, objects_new = get_value_object(id, objects_new, object, list_attributes, predicate) + add_object_to_model(id, objects, predicate) + end + + # for this moment we are not going to enrich models , maybe we will use it if the results are empty + @lang_filter.fill_models_with_all_languages(@models_by_id) + + init_unloaded_attributes(found, list_attributes) + + return @models_by_id if @bnode_extraction + + model_set_collection_attributes(@models_by_id, objects_new) + + # remove from models_by_id elements that were not touched + @models_by_id.select! { |k, _m| found.include?(k) } + + models_set_all_persistent(@models_by_id) unless @read_only + + # next level of embed attributes + include_embed_attributes(@incl_embed, objects_new) if @incl_embed && !@incl_embed.empty? + + # bnodes + blank_nodes = objects_new.select { |id, _obj| id.is_a?(RDF::Node) && id.anonymous? } + include_bnodes(blank_nodes, @models_by_id) unless blank_nodes.empty? + + models_unmapped_to_array(@models_by_id) if @unmapped + + + @models_by_id + end + + private + + def init_unloaded_attributes(found, list_attributes) + return if @incl.nil? + + # Here we are setting to nil all attributes that have been included but not found in the triplestore + found.uniq.each do |model_id| + m = @models_by_id[model_id] + @incl.each do |attr_to_incl| + is_handler = m.respond_to?(:handler?) && m.class.handler?(attr_to_incl) + next if attr_to_incl.to_s.eql?('unmapped') || is_handler + + loaded = m.respond_to?('loaded_attributes') && m.loaded_attributes.include?(attr_to_incl) + is_list = list_attributes.include?(attr_to_incl) + is_struct = m.respond_to?(:klass) + + # Go through all models queried + if is_struct + m[attr_to_incl] = [] if is_list && m[attr_to_incl].nil? + elsif is_list && (!loaded || m.send(attr_to_incl.to_s).nil?) + m.send("#{attr_to_incl}=", [], on_load: true) + elsif !loaded && !is_list && m.respond_to?("#{attr_to_incl}=") + m.send("#{attr_to_incl}=", nil, on_load: true) + end + end + end + end + + def get_value_object(id, objects_new, object, list_attributes, predicate) + object = object.object if object && !(object.is_a? RDF::URI) + range_for_v = @klass.range(predicate) + + + if object.is_a?(RDF::URI) && (predicate != :id) && !range_for_v.nil? + if objects_new.include?(object) + object = objects_new[object] + elsif !range_for_v.inmutable? + pre_val = get_preload_value(id, object, predicate) + object, objects_new = if !@read_only + preloaded_or_new_object(object, objects_new, pre_val, predicate) + else + # depedent read only + preloaded_or_new_struct(object, objects_new, pre_val, predicate) + end + else + object = range_for_v.find(object).first + end + end + + if list_attributes.include?(predicate) + pre = @klass_struct ? @models_by_id[id][predicate] : @models_by_id[id].instance_variable_get("@#{predicate}") + + if object.nil? + object = pre.nil? ? [] : pre + else + object = pre.nil? ? [object] : (pre.dup << object) + object.uniq! + end + + end + [object, objects_new] + end + + def add_object_to_model(id, objects, predicate) + + if @models_by_id[id].respond_to?(:klass) + @models_by_id[id][predicate] = objects unless objects.nil? && !@models_by_id[id][predicate].nil? + elsif !@models_by_id[id].class.handler?(predicate) && + !(objects.nil? && !@models_by_id[id].instance_variable_get("@#{predicate}").nil?) && + predicate != :id + @lang_filter.set_model_value(@models_by_id[id], predicate, objects) + end + end + + def get_preload_value(id, object, predicate) + pre_val = nil + if predicate_preloaded?(id, predicate) + pre_val = preloaded_value(id, predicate) + pre_val = pre_val.select { |x| x.id == object }.first if pre_val.is_a?(Array) + end + pre_val + end + + def preloaded_or_new_object(object, objects_new, pre_val, predicate) + object = pre_val || @klass.range_object(predicate, object) + objects_new[object.id] = object + [object, objects_new] + end + + def preloaded_or_new_struct(object, objects_new, pre_val, predicate) + struct = pre_val || @embed_struct[predicate].new + struct.id = object + struct.klass = @klass.range(predicate) + objects_new[struct.id] = struct + [struct, objects_new] + end + + def preloaded_value(id, predicate) + if !@read_only + @models_by_id[id].instance_variable_get("@#{predicate}") + + else + @models_by_id[id][predicate] + end + end + + def predicate_preloaded?(id, predicate) + @models_by_id[id] && + (@models_by_id[id].respond_to?(:klass) || @models_by_id[id].loaded_attributes.include?(predicate)) + end + + def bnode_id?(object, predicate) + object.is_a?(RDF::Node) && object.anonymous? && @incl.include?(predicate) + end + + def bnode_id_tuple(id, object, objects_new, predicate) + range = @klass.range(predicate) + objects_new[object] = BNODES_TUPLES.new(id, predicate) if range.respond_to?(:new) + objects_new + end + + def add_bnode_to_model(sol) + id = sol[:id] + struct = create_struct(@bnode_extraction, @models_by_id, sol, @variables) + @models_by_id[id].send("#{@bnode_extraction}=", struct) + end + + def create_model(id) + @models_by_id[id] = create_class_model(id, @klass, @klass_struct) unless @models_by_id.include?(id) + end + + + def create_struct(bnode_extraction, models_by_id, sol, variables) + list_attributes = Set.new(@klass.attributes(:list)) + struct = @klass.range(bnode_extraction).new + variables.each do |v| + next if v == :id + + svalue = sol[v] + struct[v] = svalue.is_a?(RDF::Node) ? svalue : svalue.object + end + if list_attributes.include?(bnode_extraction) + pre = models_by_id[sol[:id]].instance_variable_get("@#{bnode_extraction}") + pre = pre ? (pre.dup << struct) : [struct] + struct = pre + end + struct + end + + def create_class_model(id, klass, klass_struct) + klass_model = klass_struct ? klass_struct.new : klass.new + klass_model.id = id + klass_model.persistent = true unless klass_struct + klass_model.klass = klass if klass_struct + klass_model + end + + def models_unmapped_to_array(models_by_id) + models_by_id.each do |_idm, m| + @lang_filter.models_unmapped_to_array(m) + end + end + + + def is_multiple_langs? + return true if @requested_lang.is_a?(Array) || @requested_lang.eql?(:ALL) + false + end + + def include_bnodes(bnodes, models_by_id) + # group by attribute + attrs = bnodes.map { |_x, y| y.attribute }.uniq + attrs.each do |attr| + struct = @klass.range(attr) + + # bnodes that are in a range of goo ground models + # for example parents and children in LD class models + # we skip this cases for the moment + next if struct.respond_to?(:model_name) + + bnode_attrs = struct.new.to_h.keys + ids = bnodes.select { |_x, y| y.attribute == attr }.map { |_x, y| y.id } + @klass.where.models(models_by_id.select { |x, _y| ids.include?(x) }.values) + .in(@collection) + .include(bnode: { attr => bnode_attrs }).all + end + end + + def include_embed_attributes(incl_embed, objects_new) + incl_embed.each do |attr, next_attrs| + # anything to join ? + attr_range = @klass.range(attr) + next if attr_range.nil? + + range_objs = objects_new.select do |_id, obj| + obj.instance_of?(attr_range) || (obj.respond_to?(:klass) && obj[:klass] == attr_range) + end.values + next if range_objs.empty? + + range_objs.uniq! + query = attr_range.where.models(range_objs).in(@collection).include(*next_attrs) + query = query.read_only if @read_only + query.all + end + end + + def models_set_all_persistent(models_by_id) + return unless @ids + + models_by_id.each do |_k, m| + m.persistent = true + end + end + + def model_set_collection_attributes(models_by_id, objects_new) + collection_value = get_collection_value + return unless collection_value + + collection_attribute = @klass.collection_opts + models_by_id.each do |_id, m| + m.send("#{collection_attribute}=", collection_value) + end + objects_new.each do |_id, obj_new| + if obj_new.respond_to?(:klass) + collection_attribute = obj_new[:klass].collection_opts + obj_new[collection_attribute] = collection_value + elsif obj_new.class.respond_to?(:collection_opts) && + obj_new.class.collection_opts.instance_of?(Symbol) + collection_attribute = obj_new.class.collection_opts + obj_new.send("#{collection_attribute}=", collection_value) + end + end + end + + def get_collection_value + collection_value = nil + if @klass.collection_opts.instance_of?(Symbol) + collection_value = @collection.first if @collection.is_a?(Array) && (@collection.length == 1) + collection_value = @collection if @collection.respond_to? :id + end + collection_value + end + + def object_to_array(id, klass_struct, models_by_id, object, predicate) + pre = if klass_struct + models_by_id[id][predicate] + else + models_by_id[id].instance_variable_get("@#{predicate}") + end + if object.nil? && pre.nil? + object = [] + elsif object.nil? && !pre.nil? + object = pre + elsif object + object = !pre ? [object] : (pre.dup << object) + object.uniq! + end + object + end + + def dependent_model_creation(embed_struct, id, models_by_id, object, objects_new, v, options) + read_only = options[:read_only] + if object.is_a?(RDF::URI) && v != :id + range_for_v = @klass.range(v) + if range_for_v + if objects_new.include?(object) + object = objects_new[object] + elsif !range_for_v.inmutable? + pre_val = get_pre_val(id, models_by_id, object, v, read_only) + object = get_object_from_range(pre_val, embed_struct, object, objects_new, v, options) + else + object = range_for_v.find(object).first + end + end + end + object + end + + def get_object_from_range(pre_val, embed_struct, object, objects_new, predicate) + range_for_v = @klass.range(predicate) + if !@read_only + object = pre_val || @klass.range_object(predicate, object) + objects_new[object.id] = object + else + # depedent read only + struct = pre_val || embed_struct[predicate].new + struct.id = object + struct.klass = range_for_v + objects_new[struct.id] = struct + object = struct + end + object + end + + def get_pre_val(id, models_by_id, object, predicate) + pre_val = nil + if models_by_id[id] && + ((models_by_id[id].respond_to?(:klass) && models_by_id[id]) || + models_by_id[id].loaded_attributes.include?(predicate)) + pre_val = if !@read_only + models_by_id[id].instance_variable_get("@#{predicate}") + else + models_by_id[id][predicate] + end + + pre_val = pre_val.select { |x| x.id == object }.first if pre_val.is_a?(Array) + end + pre_val + end + + def add_unmapped_to_model(sol) + predicate = sol[:attributeProperty].to_s.to_sym + return unless @properties_to_include[predicate] + + id = sol[:id] + value = sol[:attributeObject] + + @lang_filter.set_unmapped_value(@models_by_id[id], @properties_to_include[predicate][:uri], value) + end + + def add_aggregations_to_model(sol) + id = sol[:id] + @aggregate_projections&.each do |aggregate_key, aggregate_val| + if @models_by_id[id].respond_to?(:add_aggregate) + @models_by_id[id].add_aggregate(aggregate_val[1], aggregate_val[0], sol[aggregate_key].object) + else + (@models_by_id[id].aggregates ||= []) << Goo::Base::AGGREGATE_VALUE.new(aggregate_val[1], + aggregate_val[0], + sol[aggregate_key].object) + end + end + end + end + end +end