require 'loggable' module OM::XML::TermXpathGenerator include Loggable # Generate relative xpath for a term # @param [OM::XML::Term] term that you want to generate relative xpath for # # In most cases, the resulting xpath will be the Term's path with the appropriate namespace appended to it. # If the Term specifies any attributes, # Special Case: attribute Terms # If the Term's path is set to {:attribute=>attr_name}, the resulting xpath will points to a node attribute named attr_name # ie. a path fo {:attribute=>"lang"} will result in a relative xpath of "@lang" # Special Case: xpath functions # If the Term's path variable is text(), it will be treated as an xpath function (no namespace) and turned into "text()[normalize-space(.)]" def self.generate_relative_xpath(term) template = "" predicates = [] if term.namespace_prefix.nil? complete_prefix = "" else complete_prefix = term.namespace_prefix + ":" end if term.path.kind_of?(Hash) if term.path.has_key?(:attribute) base_path = "@"+term.path[:attribute] else raise "#{term.path} is an invalid path for an OM::XML::Term. You should provide either a string or {:attributes=>XXX}" end else if term.path == "text()" base_path = "#{term.path}[normalize-space(.)]" else unless term.namespace_prefix.nil? template << complete_prefix end base_path = term.path end end template << base_path unless term.attributes.nil? term.attributes.each_pair do |attr_name, attr_value| if attr_value == :none predicates << "not(@#{attr_name})" else predicates << "@#{attr_name}=\"#{attr_value}\"" end end end unless predicates.empty? template << "["+ delimited_list(predicates, " and ")+"]" end return template end # Generate absolute xpath for a Term # @param [OM::XML::Term] term that you want to generate absolute xpath for # # Absolute xpaths always begin with "//". They are generated by relying on the Term's relative xpath and the absolute xpath of its parent node. def self.generate_absolute_xpath(term) relative = generate_relative_xpath(term) if term.parent.nil? return "//#{relative}" else return term.parent.xpath_absolute + "/" + relative end end def self.generate_constrained_xpath(term) if term.namespace_prefix.nil? complete_prefix = "" else complete_prefix = term.namespace_prefix + ":" end absolute = generate_absolute_xpath(term) constraint_predicates = [] arguments_for_contains_function = [] if !term.default_content_path.nil? arguments_for_contains_function << "#{complete_prefix}#{term.default_content_path}" end # If no subelements have been specified to search within, set contains function to search within the current node if arguments_for_contains_function.empty? arguments_for_contains_function << "." end arguments_for_contains_function << "\":::constraint_value:::\"" contains_function = "contains(#{delimited_list(arguments_for_contains_function)})" template = add_predicate(absolute, contains_function) return template.gsub( /:::(.*?):::/ ) { '#{'+$1+'}' }.gsub('"', '\"') end # Generate an xpath of the chosen +type+ for the given Term. # @param [OM::XML::Term] term the term that you want to generate relative xpath for # @param [Symbol] type the type of xpath to generate, :relative, :abolute, or :constrained def self.generate_xpath(term, type) case type when :relative self.generate_relative_xpath(term) when :absolute self.generate_absolute_xpath(term) when :constrained self.generate_constrained_xpath(term) end end # Use the given +terminology+ to generate an xpath with (optional) node indexes for each of the term pointers. # Ex. OM::XML::TermXpathGenerator.xpath_with_indexes(my_terminology, {:conference=>0}, {:role=>1}, :text ) # will yield an xpath similar to this: '//oxns:name[@type="conference"][1]/oxns:role[2]/oxns:roleTerm[@type="text"]' # @param [OM::XML::Terminology] terminology to generate xpath based on # @param [String -- OM term pointer] pointers identifying the node to generate xpath for def self.generate_xpath_with_indexes(terminology, *pointers) if pointers.first.nil? root_term = terminology.root_terms.first if root_term.nil? return "/" else return root_term.xpath end end query_constraints = nil if pointers.length > 1 && pointers.last.kind_of?(Hash) constraints = pointers.pop unless constraints.empty? query_constraints = constraints end end if pointers.length == 1 && pointers.first.instance_of?(String) return xpath_query = pointers.first end # if pointers.first.kind_of?(String) # return pointers.first # end keys = [] xpath = "//" pointers = OM.destringify(pointers) pointers.each_with_index do |pointer, pointer_index| if pointer.kind_of?(Hash) k = pointer.keys.first index = pointer[k] else k = pointer index = nil end keys << k term = terminology.retrieve_term(*keys) # Return nil if there is no term to work with return if term.nil? # If we've encountered a NamedTermProxy, insert path sections corresponding to each entry in its proxy_pointer (rather than just the final term that it points to). # TODO Looks like this only works if the last key is a NamedTermProxy, what if we cross proxies on the way there? if term.kind_of? OM::XML::NamedTermProxy logger.warn "You attempted to call an index value of #{index} on the term \"#{k.inspect}\". However \"#{k.inspect}\" is a proxy so we are ignoring the index. See https://jira.duraspace.org/browse/HYDRA-643" if index current_location = term.parent.nil? ? term.terminology : term.parent relative_path = "" term.proxy_pointer.each_with_index do |proxy_pointer, proxy_pointer_index| begin proxy_term = current_location.retrieve_term(proxy_pointer) if proxy_term.nil? proxy_term = terminology.retrieve_term(proxy_pointer) end proxy_relative_path = proxy_term.xpath_relative if proxy_pointer_index > 0 proxy_relative_path = "/"+proxy_relative_path end relative_path << proxy_relative_path current_location = proxy_term rescue Exception => e raise "There's a problem with the #{term.name} OM::XML::NamedTermProxy, whose proxy pointer is #{term.proxy_pointer}. The #{proxy_pointer} pointer is returning #{proxy_term.inspect}" end end else relative_path = term.xpath_relative unless index.nil? relative_path = add_node_index_predicate(relative_path, index) end end if pointer_index > 0 relative_path = "/"+relative_path end xpath << relative_path end final_term = terminology.retrieve_term(*keys) if query_constraints.kind_of?(Hash) contains_functions = [] query_constraints.each_pair do |k,v| if v.is_a?(Integer) || v == "0" || v.to_i != 0 # lookup sub element xpath = '(' + xpath + '/' + final_term.children[query_constraints.keys.first].xpath_relative + ")[#{v.to_i + 1}]" else if k.instance_of?(Symbol) constraint_path = final_term.children[k].xpath_relative else constraint_path = k end # match for text contains_functions << "#{constraint_path}[text()=\"#{v}\"]" xpath = add_predicate(xpath, delimited_list(contains_functions, " and ") ) end end end # return xpath end # @see OM::XML.delimited_list def self.delimited_list(*args) OM::XML.delimited_list(*args) end # Adds xpath xpath node index predicate to the end of your xpath query # Example: # add_node_index_predicate("//oxns:titleInfo",0) # => "//oxns:titleInfo[1]" # # add_node_index_predicate("//oxns:titleInfo[@lang=\"finnish\"]",0) # => "//oxns:titleInfo[@lang=\"finnish\"][1]" def self.add_node_index_predicate(xpath_query, array_index_value) modified_query = xpath_query.dup modified_query << "[#{array_index_value + 1}]" end # Adds xpath:position() method call to the end of your xpath query # Examples: # # add_position_predicate("//oxns:titleInfo",0) # => "//oxns:titleInfo[position()=1]" # # add_position_predicate("//oxns:titleInfo[@lang=\"finnish\"]",0) # => "//oxns:titleInfo[@lang=\"finnish\" and position()=1]" def self.add_position_predicate(xpath_query, array_index_value) position_function = "position()=#{array_index_value + 1}" self.add_predicate(xpath_query, position_function) end def self.add_predicate(xpath_query, predicate) modified_query = xpath_query.dup # if xpath_query.include?("]") if xpath_query[xpath_query.length-1..xpath_query.length] == "]" modified_query.insert(xpath_query.rindex("]"), " and #{predicate}") else modified_query << "[#{predicate}]" end return modified_query end end