app/controllers/hierarchy_controller.rb
# encoding: UTF-8
# Copyright 2011-2013 innoQ Deutschland GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class HierarchyController < ApplicationController
resource_description do
name 'Hierarchy'
end
api :GET, 'hierarchy', 'Retrieve the downwards concepts hierarchy starting '\
'at top concepts.'
formats [:html, :ttl, :rdf]
example <<-DOC
GET /hierarchy.ttl
200
# omitted namespace definitions
:achievement_hobbies a skos:Concept;
skos:topConceptOf :scheme;
skos:prefLabel "Achievement hobbies"@en;
skos:narrower :model_building;
skos:narrower :gardening.
:model_building a skos:Concept;
skos:prefLabel "Model building"@en;
skos:narrower :model_rocketry;
:model_rocketry a skos:Concept;
skos:prefLabel "Model rocketry"@en.
:gardening a skos:Concept;
skos:prefLabel "Gardening"@en.
DOC
def index
authorize! :read, Iqvoc::Concept.base_class
# special-casing to avoid confusion, based on user feedback
if params[:root]
msg = ['to use a specific concept as hierarchy root, please use',
url_for(params.merge 'action' => 'show')].join("\n")
render status: 400, text: msg
return
end
depth = params[:depth] || (unbounded? ? -1 : nil)
render_hierarchy 'scheme', depth, unbounded?
end
api :GET, 'hierarchy/:root', "Retrieve a concept's up- or downwards "\
'hierarchy with optional siblings.'
formats [:html, :ttl, :rdf]
param :dir, ['down', 'up'],
desc: <<-DOC
Direction of the hierarchy.
*down* follow narrower from root to its leaf nodes.
*up* follow broader from root to its top term(s).
DOC
param :depth, [1, 2, 3, 4],
'Number of levels of hierarchy to be included in the response'
param :siblings, ['1', 'true'],
'Siblings of each node will be included even if they are not part of '\
'the hierarchy.'
example <<-DOC
GET /hierarchy/model_building.ttl?dir=down&depth=1
# omitted namespace definitions
:model_rocketry a skos:Concept;
skos:prefLabel "Model rocketry"@en.
:radio-controlled_modeling a skos:Concept;
skos:prefLabel "Radio-controlled modeling"@en.
:scale_modeling a skos:Concept;
skos:prefLabel "Scale modeling"@en.
:model_building a skos:Concept;
skos:prefLabel "Model building"@en;
skos:narrower :model_rocketry;
skos:narrower :radio-controlled_modeling;
skos:narrower :scale_modeling.
DOC
def show
authorize! :read, Iqvoc::Concept.base_class
render_hierarchy params[:root], params[:depth], unbounded?
end
private
def unbounded?
Iqvoc.config['performance.unbounded_hierarchy']
end
def render_hierarchy(root_origin, depth, unbounded = false)
default_depth = 3
max_depth = 4 # XXX: arbitrary
direction = params[:dir] == 'up' ? 'up' : 'down'
depth = depth.blank? ? default_depth : (Float(depth).to_i rescue nil)
include_siblings = ['true', '1'].include?(params[:siblings])
include_unpublished = params[:published] == '0'
scope = Iqvoc::Concept.base_class
scope = include_unpublished ? scope.editor_selectable : scope.published
scope = scope.ordered_by_pref_label
# validate depth parameter
if not depth
error = 'invalid depth parameter' # TODO: i18n
elsif depth > max_depth and not unbounded
error = [403, 'excessive depth'] # TODO: i18n
end
# validate root parameter
error = 'missing root parameter' unless root_origin # TODO: i18n
unless error
root_concepts = root_origin == 'scheme' ? scope.tops.load : # XXX: special-casing
scope.where(origin: root_origin).load
unless root_concepts.length > 0
error = [404, 'no concept matching root parameter'] # TODO: i18n
end
end
# error handling
if error
status, error = error if error.is_a? Array
flash.now[:error] = error
render 'hierarchy/show', status: (status || 400)
return
end
# caching -- NB: invalidated on any in-scope concept modifications
latest = scope.maximum(:updated_at)
response.cache_control[:public] = !include_unpublished # XXX: this should not be necessary!?
return unless stale?(etag: [latest, hierarchy_params.to_h], last_modified: latest,
public: !include_unpublished)
# NB: order matters due to the `where` clause below
if direction == 'up'
scope = scope.includes(:narrower_relations, :broader_relations)
else
scope = scope.includes(:broader_relations, :narrower_relations)
end
@concepts = {}
root_concepts.each do |root_concept|
if include_siblings
determine_siblings(root_concept).each { |sib| @concepts[sib] = {} }
end
@concepts[root_concept] = populate_hierarchy(root_concept, scope, depth,
0, include_siblings)
end
@relation_class = Iqvoc::Concept.broader_relation_class
@relation_class = @relation_class.narrower_class unless direction == 'up'
respond_to do |format|
format.any(:html, :rdf, :ttl, :nt) { render 'hierarchy/show' }
end
end
# returns a hash of concept/relations pairs of arbitrary nesting depth
# NB: recursive, triggering one database query per iteration
def populate_hierarchy(root_concept, scope, max_depth, current_depth = 0,
include_siblings = false)
current_depth += 1
return {} if max_depth != -1 and current_depth > max_depth
rels = scope.where(Concept::Relation::Base.arel_table[:target_id].
eq(root_concept.id)).references(:concept_relations)
results = rels.inject({}) do |memo, concept|
if include_siblings
determine_siblings(concept).each { |sib| memo[sib] = {} }
end
memo[concept] = populate_hierarchy(concept, scope, max_depth,
current_depth, include_siblings)
memo
end
results
end
# NB: includes support for poly-hierarchies -- XXX: untested
def determine_siblings(concept)
concept.broader_relations.map do |rel|
rel.target.narrower_relations.map { |rel| rel.target } # XXX: expensive
end.flatten.uniq.sort { |a, b| a.pref_label <=> b.pref_label }
end
private
def hierarchy_params
params.permit(:lang, :format, :root, :dir, :depth, :siblings, :published)
end
end