-
Notifications
You must be signed in to change notification settings - Fork 55
Sparql interface #2303
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
Open
jjkoehorst
wants to merge
31
commits into
seek4science:main
Choose a base branch
from
jjkoehorst:sparql-interface
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Sparql interface #2303
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
a00c4ba
Add comprehensive SPARQL query interface
jjkoehorst cf00594
style sheet for sparql requirement added
jjkoehorst 05d15a2
Merge commit 'e5d24f69045bd42fb60489ded5bee434d80d7be9' into sparql-i…
jjkoehorst a4cbbb4
cleaning up based on feedback
jjkoehorst 7535fbe
Made some further changes...
jjkoehorst c4aff75
fixed an issue where the original implementation of DESCRIBE was igno…
jjkoehorst 7e7ffea
some "legacy" code where it would listen to /sparql/query while thats…
jjkoehorst 771a62f
added a filter function in case we add many more example queries (whi…
jjkoehorst ec6690c
a integration test to show the index page is working
stuzart 4bfa00f
tidy how it checks the configuration and connection
stuzart ee62a70
an integration test performing a sparql query
stuzart 6213e45
update the sparql query test to be easier to test, and also in future…
stuzart e3e0a3b
test that check you cannot insert and delete triples with a sparql qu…
stuzart bb3e8d6
remove test trace
stuzart 52f25b2
a test for the html response
stuzart 2b0b015
an initial test for an invalid sparql query. response codes and json …
stuzart ce61b66
Added a schema image of the demo dataset. To be updated in the future…
jjkoehorst c3c1453
simplified example view to work on the sparql_queries.yml file.
jjkoehorst 70e5e12
Example query section is now collapsed by default to reduce space
jjkoehorst 75c6885
add a before_action to block access to sparql controller if not confi…
stuzart dc74aab
fix to always use unauthorized access to endpoint, to prevent insert,…
stuzart 4bd076f
sorted out some of the error reporting and response codes. 422 seemed…
stuzart 19c019f
add test for the case where there is a single result
stuzart 0dc71c4
test for when the repo is configured, but for some reason not available
stuzart 27e6c0f
don't default the results, and only set if a query. Also only allow X…
stuzart d23c35e
split the query into its own action
stuzart 5a57d4e
fixes and refactoring for the use of the format param, including a te…
stuzart f371e12
use the bootstrap_helper for the panels and put the examples in its o…
stuzart ad8749d
resolve copilot comments
stuzart e5d951d
don't show the flash[:error] when there is just a query error
stuzart 1fdbe42
default to restricting to the public graph, but added a test to show …
stuzart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
document.addEventListener('DOMContentLoaded', function() { | ||
// Get DOM elements | ||
const clearBtn = document.getElementById('clear-query'); | ||
const queryTextarea = document.getElementById('sparql_query'); | ||
|
||
if (clearBtn && queryTextarea) { | ||
clearBtn.addEventListener('click', function() { | ||
queryTextarea.value = ''; | ||
queryTextarea.focus(); | ||
}); | ||
} | ||
|
||
|
||
// Use example query buttons | ||
const useQueryButtons = document.querySelectorAll('.use-query'); | ||
useQueryButtons.forEach(button => { | ||
button.addEventListener('click', function() { | ||
const queryText = this.dataset.query; | ||
if (queryTextarea) { | ||
queryTextarea.value = queryText; | ||
queryTextarea.focus(); | ||
|
||
// Scroll to the query form | ||
queryTextarea.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||
} | ||
}); | ||
}); | ||
|
||
// Auto-resize textarea | ||
if (queryTextarea) { | ||
function autoResize() { | ||
queryTextarea.style.height = 'auto'; | ||
queryTextarea.style.height = Math.max(queryTextarea.scrollHeight, 200) + 'px'; | ||
} | ||
|
||
queryTextarea.addEventListener('input', autoResize); | ||
autoResize(); // Initial resize | ||
} | ||
|
||
// Handle clicks on URI links to execute DESCRIBE queries | ||
document.addEventListener('click', function(e) { | ||
if (e.target.classList.contains('external-link')) { | ||
e.preventDefault(); // Prevent default link behavior | ||
|
||
const uri = e.target.href; | ||
const describeQuery = `DESCRIBE <${uri}>`; | ||
|
||
if (queryTextarea) { | ||
queryTextarea.value = describeQuery; | ||
queryTextarea.focus(); | ||
|
||
// Auto-resize after setting the query | ||
queryTextarea.style.height = 'auto'; | ||
queryTextarea.style.height = Math.max(queryTextarea.scrollHeight, 200) + 'px'; | ||
|
||
// Scroll to the query form | ||
queryTextarea.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||
|
||
// Auto-execute the DESCRIBE query | ||
const form = queryTextarea.closest('form'); | ||
if (form) { | ||
form.submit(); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
// Handle example query dropdown | ||
const exampleQuerySelect = document.getElementById('example-queries'); | ||
|
||
if (exampleQuerySelect && queryTextarea) { | ||
exampleQuerySelect.addEventListener('change', function() { | ||
const selectedOption = this.options[this.selectedIndex]; | ||
|
||
if (selectedOption.value) { | ||
const rawQuery = selectedOption.dataset.query; | ||
|
||
// Decode HTML entities | ||
const tempDiv = document.createElement('div'); | ||
tempDiv.innerHTML = rawQuery; | ||
const query = tempDiv.textContent || tempDiv.innerText || ''; | ||
|
||
// Set the decoded query in the textarea | ||
queryTextarea.value = query; | ||
queryTextarea.focus(); | ||
|
||
// Auto-resize after setting the query | ||
queryTextarea.style.height = 'auto'; | ||
queryTextarea.style.height = Math.max(queryTextarea.scrollHeight, 200) + 'px'; | ||
|
||
// Reset dropdown to default | ||
this.selectedIndex = 0; | ||
|
||
// Scroll to the query form | ||
queryTextarea.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||
} | ||
}); | ||
} | ||
|
||
// Example query filter functionality | ||
const queryFilter = document.getElementById('query-filter'); | ||
const queriesContainer = document.getElementById('example-queries-container'); | ||
const noQueriesMessage = document.getElementById('no-queries-message'); | ||
|
||
if (queryFilter && queriesContainer) { | ||
queryFilter.addEventListener('input', function() { | ||
const filterText = this.value.toLowerCase().trim(); | ||
const exampleQueries = queriesContainer.querySelectorAll('.example-query'); | ||
let visibleCount = 0; | ||
|
||
exampleQueries.forEach(function(query) { | ||
const title = query.querySelector('.query-title strong'); | ||
const description = query.querySelector('.text-muted'); | ||
const queryCode = query.querySelector('.query-code'); | ||
|
||
const titleText = title ? title.textContent.toLowerCase() : ''; | ||
const descriptionText = description ? description.textContent.toLowerCase() : ''; | ||
const codeText = queryCode ? queryCode.textContent.toLowerCase() : ''; | ||
|
||
const matchesFilter = filterText === '' || | ||
titleText.includes(filterText) || | ||
descriptionText.includes(filterText) || | ||
codeText.includes(filterText); | ||
|
||
if (matchesFilter) { | ||
query.parentElement.style.display = ''; | ||
visibleCount++; | ||
} else { | ||
query.parentElement.style.display = 'none'; | ||
} | ||
}); | ||
|
||
// Show/hide "no results" message | ||
if (noQueriesMessage) { | ||
if (visibleCount === 0 && filterText !== '') { | ||
noQueriesMessage.style.display = 'block'; | ||
} else { | ||
noQueriesMessage.style.display = 'none'; | ||
} | ||
} | ||
}); | ||
|
||
// Clear filter when escape key is pressed | ||
queryFilter.addEventListener('keydown', function(e) { | ||
if (e.key === 'Escape') { | ||
this.value = ''; | ||
this.dispatchEvent(new Event('input')); | ||
} | ||
}); | ||
} | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
.sparql-interface { | ||
.sparql-textarea { | ||
font-family: 'Monaco', 'Menlo', 'Courier New', monospace; | ||
font-size: 12px; | ||
line-height: 1.4; | ||
background-color: #f8f9fa; | ||
border: 2px solid #dee2e6; | ||
|
||
&:focus { | ||
border-color: #007bff; | ||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); | ||
} | ||
} | ||
|
||
.sparql-results { | ||
margin-top: 2rem; | ||
|
||
.table-responsive { | ||
max-width: 100%; | ||
overflow-x: auto; | ||
-webkit-overflow-scrolling: touch; // Smooth scrolling on mobile | ||
} | ||
|
||
table { | ||
font-size: 12px; | ||
min-width: 100%; | ||
white-space: nowrap; // Prevent cell content from wrapping | ||
|
||
th { | ||
background-color: #f8f9fa; | ||
font-weight: 600; | ||
min-width: 150px; // Minimum column width | ||
} | ||
|
||
td { | ||
min-width: 150px; // Minimum column width | ||
} | ||
|
||
.external-link { | ||
word-break: break-all; | ||
max-width: 300px; // Limit URI column width | ||
display: inline-block; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
} | ||
} | ||
|
||
.code-block { | ||
max-height: 500px; | ||
overflow-y: auto; | ||
border: 1px solid #dee2e6; | ||
border-radius: 4px; | ||
|
||
pre { | ||
background: #f8f9fa; | ||
border: none; | ||
border-radius: 0; | ||
margin: 0; | ||
padding: 1rem; | ||
font-size: 11px; | ||
white-space: pre-wrap; | ||
} | ||
} | ||
} | ||
|
||
.sparql-examples { | ||
margin-top: 2rem; | ||
|
||
.example-query-filter { | ||
margin-bottom: 1.5rem; | ||
|
||
.input-group { | ||
max-width: 400px; | ||
} | ||
} | ||
|
||
.example-query { | ||
background: #f8f9fa; | ||
border: 1px solid #dee2e6; | ||
border-radius: 4px; | ||
padding: 1rem; | ||
margin-bottom: 1rem; | ||
|
||
.query-title { | ||
margin-bottom: 0.5rem; | ||
|
||
.label { | ||
margin-left: 0.5rem; | ||
} | ||
} | ||
|
||
.query-code { | ||
background: white; | ||
border: 1px solid #ddd; | ||
border-radius: 3px; | ||
padding: 0.75rem; | ||
font-size: 11px; | ||
font-family: 'Monaco', 'Menlo', 'Courier New', monospace; | ||
margin: 0.5rem 0; | ||
white-space: pre-wrap; | ||
word-wrap: break-word; | ||
} | ||
|
||
.use-query { | ||
margin-top: 0.5rem; | ||
} | ||
} | ||
} | ||
|
||
.sparql-format-select { | ||
width: auto; | ||
display: inline-block; | ||
} | ||
|
||
.form-actions { | ||
margin-top: 25px; | ||
|
||
.btn + .btn { | ||
margin-left: 0.5rem; | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
require 'sparql/client' | ||
require 'net/http' | ||
|
||
class SparqlController < ApplicationController | ||
layout 'application' | ||
|
||
before_action :rdf_repository_configured? | ||
|
||
def index | ||
stuzart marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Main SPARQL interface page | ||
flash.now[:error] = 'SPARQL endpoint is configured, but not currently available.' unless rdf_repository_available? | ||
|
||
@example_queries = load_example_queries | ||
|
||
respond_to(&:html) | ||
end | ||
|
||
def query | ||
@sparql_query = params[:sparql_query] || '' | ||
|
||
if rdf_repository_available? | ||
begin | ||
@results = execute_sparql_query(@sparql_query) | ||
rescue StandardError => e | ||
@error = e.message | ||
Rails.logger.error("SPARQL Query Error: #{e.message}") | ||
end | ||
else | ||
@error = 'SPARQL endpoint is configured, but not currently available.' | ||
flash[:error] = @error | ||
end | ||
|
||
status = @error ? :unprocessable_entity : nil # can't use :success but is the default if nil | ||
@results ||= [] | ||
|
||
respond_to do |format| | ||
format.html do | ||
@example_queries = load_example_queries | ||
render :index, status: status | ||
end | ||
format.json { render json: { 'results': @results, 'error': @error }.compact, status: status } | ||
format.xml { render xml: @results.to_xml, status: status } | ||
end | ||
end | ||
|
||
private | ||
|
||
def execute_sparql_query(query) | ||
public_graph = Seek::Rdf::RdfRepository.instance.get_configuration.public_graph | ||
endpoint = Seek::Rdf::RdfRepository.instance.get_configuration.uri | ||
sparql_client = SPARQL::Client.new(endpoint, graph: public_graph) | ||
results = sparql_client.query(query) | ||
convert_sparql_results(results) | ||
end | ||
|
||
def convert_sparql_results(results) | ||
return [] if results.nil? | ||
|
||
# Handle empty collections | ||
return [] if results.respond_to?(:empty?) && results.empty? | ||
|
||
# Handle different result formats | ||
if results.respond_to?(:map) | ||
results.map do |solution| | ||
if solution.respond_to?(:to_h) | ||
solution.to_h.transform_values { |v| v.respond_to?(:to_s) ? v.to_s : v } | ||
elsif solution.respond_to?(:bindings) | ||
solution.bindings.transform_values { |v| v.respond_to?(:to_s) ? v.to_s : v } | ||
else | ||
solution.to_s | ||
end | ||
end | ||
else | ||
# This handles ASK queries (boolean) and any other single values | ||
[{ 'result' => results.to_s }] | ||
end | ||
end | ||
|
||
def rdf_repository_available? | ||
Seek::Rdf::RdfRepository.instance.available? | ||
end | ||
|
||
def rdf_repository_configured? | ||
unless Seek::Rdf::RdfRepository.instance&.configured? | ||
flash[:error] = 'SPARQL endpoint is not configured.' | ||
redirect_to main_app.root_path | ||
end | ||
end | ||
|
||
def load_example_queries | ||
queries_file = Rails.root.join('config', 'sparql_queries.yml') | ||
if File.exist?(queries_file) | ||
YAML.safe_load(File.read(queries_file)) || {} | ||
else | ||
{} | ||
end | ||
rescue StandardError => e | ||
Rails.logger.error("Failed to load SPARQL queries: #{e.message}") | ||
{} | ||
end | ||
end |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.