#!/usr/bin/env ruby # -*- coding: utf-8 -*- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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. # SPARQL HTTP Update, client. require 'optparse' require 'net/http' require 'uri' require 'cgi' require 'pp' require 'ostruct' # ToDo # Allow a choice of media type for GET # --accept "content-type" (and abbreviations) # --header "Add:this" # --user, --password # Basic authentication: request.basic_auth("username", "password") # Follow redirects => 301: puts response["location"] # All headers are lowercase? SOH_NAME="SOH" SOH_VERSION="0.0.0" $proxy = ENV['http_proxy'] ? URI.parse(ENV['http_proxy']) : OpenStruct.new # What about direct naming? # Names $mtTurtle = 'text/turtle;charset=utf-8' $mtRDF = 'application/rdf+xml' $mtText = 'text/plain' $mtNQuads = 'text/n-quads' $mtTriG = 'application/trig' $mtSparqlResultsX = 'application/sparql-results+xml' $mtSparqlResultsJ = 'application/sparql-results+json' $mtAppJSON = 'application/json' $mtAppXML = 'application/xml' $mtSparqlResultsTSV = 'application/sparql-results+tsv' $mtSparqlResultsCSV = 'application/sparql-results+csv' $mtSparqlUpdate = 'application/sparql-update' $mtWWWForm = 'application/x-www-form-urlencoded' $mtSparqlQuery = "application/sparql-query" ; # Global media type table. $fileMediaTypes = {} $fileMediaTypes['ttl'] = $mtTurtle $fileMediaTypes['n3'] = 'text/n3; charset=utf-8' $fileMediaTypes['nt'] = $mtText $fileMediaTypes['rdf'] = $mtRDF $fileMediaTypes['owl'] = $mtRDF $fileMediaTypes['nq'] = $mtNQuads $fileMediaTypes['trig'] = $mtTriG # Global charset : no entry means "don't set" $charsetUTF8 = 'utf-8' $charset = {} $charset[$mtTurtle] = 'utf-8' $charset[$mtText] = 'ascii' $charset[$mtTriG] = 'utf-8' $charset[$mtNQuads] = 'ascii' # Headers $hContentType = 'Content-Type' # $hContentEncoding = 'Content-Encoding' $hContentLength = 'Content-Length' # $hContentLocation = 'Content-Location' # $hContentRange = 'Content-Range' $hAccept = 'Accept' $hAcceptCharset = 'Accept-Charset' $hAcceptEncoding = 'Accept-Encoding' $hAcceptRanges = 'Accept-Ranges' $headers = { "User-Agent" => "#{SOH_NAME}/Fuseki #{SOH_VERSION}"} $print_http = false # Default for GET # At least allow anythign (and hope!) $accept_rdf="#{$mtRDF};q=0.9 , #{$mtTurtle}" # For SPARQL query $accept_results="#{$mtSparqlResultsJ} , #{$mtSparqlResultsX};q=0.9 , #{$accept_rdf}" # Accept any in case of trouble. $accept_rdf="#{$accept_rdf} , */*;q=0.1" $accept_results="#{$accept_results} , */*;q=0.1" # The media type usually forces the charset. $accept_charset=nil ## Who we are. ## Two styles: ## s-query ..... ## soh query ..... $cmd = File.basename($0) if $cmd == 'soh' then $cmd = (ARGV.size == 0) ? 'soh' : ARGV.shift end if ! $cmd.start_with?('s-') && $cmd != 'soh' $cmd = 's-'+$cmd end ## -------- def GET(dataset, graph) print "GET #{dataset} #{graph}\n" if $verbose requestURI = target(dataset, graph) headers = {} headers.merge!($headers) headers[$hAccept] = $accept_rdf headers[$hAcceptCharset] = $accept_charset unless $accept_charset.nil? get_worker(requestURI, headers) end def get_worker(requestURI, headers) uri = URI.parse(requestURI) request = Net::HTTP::Get.new(uri.request_uri) request.initialize_http_header(headers) print_http_request(uri, request) response_print_body(uri, request) end def HEAD(dataset, graph) print "HEAD #{dataset} #{graph}\n" if $verbose requestURI = target(dataset, graph) headers = {} headers.merge!($headers) headers[$hAccept] = $accept_rdf headers[$hAcceptCharset] = $accept_charset unless $accept_charset.nil? uri = URI.parse(requestURI) request = Net::HTTP::Head.new(uri.request_uri) request.initialize_http_header(headers) print_http_request(uri, request) response_no_body(uri, request) end def PUT(dataset, graph, file) print "PUT #{dataset} #{graph} #{file}\n" if $verbose send_body(dataset, graph, file, Net::HTTP::Put) end def POST(dataset, graph, file) print "POST #{dataset} #{graph} #{file}\n" if $verbose send_body(dataset, graph, file, Net::HTTP::Post) end def DELETE(dataset, graph) print "DELETE #{dataset} #{graph}\n" if $verbose requestURI = target(dataset, graph) uri = URI.parse(requestURI) request = Net::HTTP::Delete.new(uri.request_uri) headers = {} headers.merge!($headers) request.initialize_http_header(headers) print_http_request(uri, request) response_no_body(uri, request) end def uri_escape(string) CGI.escape(string) end def target(dataset, graph) return dataset+"?default" if graph == "default" return dataset+"?graph="+uri_escape(graph) end def send_body(dataset, graph, file, method) mt = content_type(file) headers = {} headers.merge!($headers) headers[$hContentType] = mt headers[$hContentLength] = File.size(file).to_s ## p headers requestURI = target(dataset, graph) uri = URI.parse(requestURI) request = method.new(uri.request_uri) request.initialize_http_header(headers) print_http_request(uri, request) request.body_stream = File.open(file) response_no_body(uri, request) end def response_no_body(uri, request) http = Net::HTTP::Proxy($proxy.host,$proxy.port).new(uri.host, uri.port) http.read_timeout = nil # check we can connect. begin http.start rescue Exception => e # puts e.message #puts e.backtrace.inspect warn_exit "Failed to connect: #{uri.host}:#{uri.port}: #{e.message}", 3 end response = http.request(request) print_http_response(response) case response when Net::HTTPSuccess, Net::HTTPRedirection # OK when Net::HTTPNotFound warn_exit "404 Not found: #{uri}", 9 #print response.body else warn_exit "#{response.code} #{response.message} #{uri}", 9 # Unreachable response.error! end # NO BODY IN RESPONSE end def response_print_body(uri, request) http = Net::HTTP::Proxy($proxy.host,$proxy.port).new(uri.host, uri.port) http.read_timeout = nil # check we can connect. begin http.start rescue => e #puts e.backtrace.inspect #print e.class warn_exit "Failed to connect: #{uri.host}:#{uri.port}: #{e.message}", 3 end # Add a blank line if headers were output. print "\n" if $http_print ; begin response = http.request(request) { |res| print_http_response(res) #puts res.code res.read_body do |segment| print segment end } case response when Net::HTTPSuccess, Net::HTTPRedirection # OK when Net::HTTPNotFound warn_exit "404 Not found: #{uri}", 9 #print response.body else warn_exit "#{response.code}: #{uri}", 9 # Unreachable response.error! end rescue EOFError => e warn_exit "IO Error: "+e.message, 3 end end def print_http_request(uri, request) return unless $print_http #print "Request\n" print request.method," ",uri, "\n" print_headers(" ",request) end def print_http_response(response) return unless $print_http #print "Response\n" print response.code, " ", response.message, "\n" print_headers(" ",response) end def print_headers(marker, headers) headers.each do |k,v| k = k.split('-').map{|w| w.capitalize}.join('-')+':' printf "%s%-20s %s\n",marker,k,v end end def content_type(file) file =~ /\.([^.]*)$/ ext = $1 mt = $fileMediaTypes[ext] cs = $charset[mt] mt = mt+';charset='+cs if ! cs.nil? return mt end def charset(content_type) return $charset[content_type] end def warn_exit(msg, rc) warn msg exit rc ; end def parseURI(uri_string) begin return URI.parse(uri_string).to_s rescue URI::InvalidURIError => err warn_exit "Bad URI: <#{uri_string}>", 2 end end ## ---- Command def cmd_soh(command=nil) ## Command line options = {} optparse = OptionParser.new do |opts| # Set a banner, displayed at the top # of the help screen. case $cmd when "s-http", "sparql-http", "soh" banner="$cmd [get|post|put|delete] datasetURI graph [file]" when "s-get", "s-head", "s-delete" banner="$cmd datasetURI graph" end opts.banner = $banner # Define the options, and what they do options[:verbose] = false opts.on( '-v', '--verbose', 'Verbose' ) do options[:verbose] = true end options[:version] = false opts.on( '--version', 'Print version and exit' ) do print "#{SOH_NAME} #{SOH_VERSION}\n" exit end # This displays the help screen, all programs are # assumed to have this option. opts.on( '-h', '--help', 'Display this screen and exit' ) do puts opts exit end end begin optparse.parse! rescue OptionParser::InvalidArgument => e warn e exit end $verbose = options[:verbose] $print_http = $verbose if command.nil? if ARGV.size == 0 warn "No command given: expected one of 'get', 'put', 'post', 'delete', 'query' or 'update'" exit 1 end cmdPrint=ARGV.shift command=cmdPrint.upcase else cmdPrint=command end case command when "HEAD", "GET", "DELETE" requiredFile=false when "PUT", "POST" requiredFile=true when "QUERY" cmd_sparql_query when "UPDATE" cmd_sparql_update else warn_exit "Unknown command: #{command}", 2 end if requiredFile then if ARGV.size != 3 warn_exit "Required: dataset URI, graph URI (or 'default') and file", 1 end else if ARGV.size != 2 warn_exit "Required: dataset URI and graph URI (or 'default')", 1 end end dataset=parseURI(ARGV.shift) # Relative URI? graph=parseURI(ARGV.shift) file="" if requiredFile then file = ARGV.shift if requiredFile if ! File.exist?(file) warn_exit "No such file: "+file, 3 end if File.directory?(file) warn_exit "File is a directory: "+file, 3 end end case command when "GET" GET(dataset, graph) when "HEAD" HEAD(dataset, graph) when "PUT" PUT(dataset, graph, file) when "DELETE" DELETE(dataset, graph) when "POST" POST(dataset, graph, file) else warn_exit "Internal error: Unknown command: #{cmd}", 2 end exit 0 end ## -------- def string_or_file(arg) return arg if ! arg.match(/^@/) a=(arg[1..-1]) open(a, 'rb'){|f| f.read} end ## -------- SPARQL Query ## Choose method def SPARQL_query(service, query, query_file, forcePOST=false, args2={}) if ! query_file.nil? query = open(query_file, 'rb'){|f| f.read} end if forcePOST || query.length >= 2*1024 SPARQL_query_POST(service, query, args2) else SPARQL_query_GET(service, query, args2) end end ## By GET def SPARQL_query_GET(service, query, args2) args = { "query" => query } args.merge!(args2) qs=args.collect { |k,v| "#{k}=#{uri_escape(v)}" }.join('&') action="#{service}?#{qs}" headers={} headers.merge!($headers) headers[$hAccept]=$accept_results get_worker(action, headers) end ## By POST def SPARQL_query_POST(service, query, args2) # DRY - body/no body for each of request and response. post_params={ "query" => query } post_params.merge!(args2) uri = URI.parse(service) headers={} headers.merge!($headers) headers[$hAccept]=$accept_results execute_post_form_body(uri, headers, post_params) end def execute_post_form_body(uri, headers, post_params) request = Net::HTTP::Post.new(uri.request_uri) qs=post_params.collect { |k,v| "#{k}=#{uri_escape(v)}" }.join('&') headers[$hContentType] = $mtWWWForm headers[$hContentLength] = qs.length.to_s request.initialize_http_header(headers) request.body = qs print_http_request(uri, request) response_print_body(uri, request) end # Usage: -v --help --file= --query= def cmd_sparql_query options={} optparse = OptionParser.new do |opts| opts.banner = "Usage: #{$cmd} [--query QUERY] [--service URI] [--post] 'query' | @file" opts.on('--service=URI', '--server=URI', 'SPARQL endpoint') do |uri| options[:service]=uri end opts.on('--query=FILE','--file=FILE', 'Take query from a file') do |file| options[:file]=file end opts.on('--output=TYPE', [:json,:xml,:text,:csv,:tsv], 'Set the output argument') do |type| options[:output]=type end opts.on('--accept=TYPE', [:json,:xml,:text,:csv,:tsv], 'Set the accept header type') do |type| options[:accept]=type end options[:verbose] = false opts.on( '--post', 'Force use of POST' ) do options[:post] = true end opts.on( '-v', '--verbose', 'Verbose' ) do options[:verbose] = true end opts.on( '--version', 'Print version and exit' ) do print "#{SOH_NAME} #{SOH_VERSION}\n" exit end opts.on( '-h', '--help', 'Display this screen and exit' ) do puts opts exit end end begin optparse.parse! rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e warn e exit 1 end $verbose = options[:verbose] $print_http = $verbose usePOST = options[:post] service = options[:service] warn_exit 'No service specified. Required --service=URI',1 if service.nil? # Query query=nil query_file=options[:file] if query_file.nil? && ARGV.size == 0 then warn_exit 'No query specified.',1 end if query_file.nil? query = ARGV.shift if query.match(/^@/) query_file = query[1..-1] query = nil end end # --output ==> output= (non-standard) args={} case options[:output] when nil when "json","xml","text","csv","tsv" args['output'] = options[:output] when :json,:xml,:text,:csv,:tsv args['output'] = options[:output].to_s else warn_exit "Unrecognized output type: "+options[:output],2 end # --accept # options[:accept] print "SPARQL #{service}\n" if $verbose #args={"output"=>"text"} SPARQL_query(service, query, query_file, usePOST, args) exit(0) end ## -------- SPARQL Update # Update sent as a WWW form. def SPARQL_update_by_form(service, update, args2={}) args = {} args.merge!(args2) headers={} headers.merge!($headers) # args? encode? body="update="+uri_escape(update) headers[$hContentType] = $mtWWWForm headers[$hContentLength] = body.length.to_s uri = URI.parse(service) execute_post_form(uri, headers, body) end # DRY - query form. def execute_post_form(uri, headers, body) request = Net::HTTP::Post.new(uri.request_uri) request.initialize_http_header(headers) request.body = body print_http_request(uri, request) response_no_body(uri, request) end def SPARQL_update(service, update, args2={}) args = {} args.merge!(args2) headers={} headers.merge!($headers) headers[$hContentType] = $mtSparqlUpdate uri = URI.parse(service) request = Net::HTTP::Post.new(uri.request_uri) request.initialize_http_header(headers) request.body = update print_http_request(uri, request) response_no_body(uri, request) end def cmd_sparql_update(by_raw_post=true) # Share with cmd_sparql_query options={} optparse = OptionParser.new do |opts| opts.banner = "Usage: #{$cmd} [--file REQUEST] [--service URI] 'request' | @file" opts.on('--service=URI', '--server=URI', 'SPARQL endpoint') do |uri| options[:service]=uri end opts.on('--update=FILE', '--file=FILE', 'Take update from a file') do |file| options[:file]=file end options[:verbose] = false opts.on( '-v', '--verbose', 'Verbose' ) do options[:verbose] = true end opts.on( '--version', 'Print version and exit' ) do print "#{SOH_NAME} #{SOH_VERSION}\n" exit end opts.on( '-h', '--help', 'Display this screen and exit' ) do puts opts exit end end begin optparse.parse! rescue OptionParser::InvalidArgument => e warn e exit end $verbose = options[:verbose] $print_http = $verbose service = options[:service] warn_exit 'No service specified. Required --service=URI',1 if service.nil? update=nil update_file=options[:file] if update_file.nil? && ARGV.size == 0 then warn_exit 'No update specified.',1 end if update_file.nil? update = ARGV.shift if update.match(/^@/) update_file = update[1..-1] update = nil end end print "SPARQL-Update #{service}\n" if $verbose args={} # Reads in the file :-( if update.nil? then update = open(update_file, 'rb'){|f| f.read} else update = string_or_file(update) end if by_raw_post SPARQL_update(service, update, args) else SPARQL_update_by_form(service, update, args) end exit(0) end ## ------- case $cmd when "s-http", "sparql-http", "soh" $banner="#{$cmd} [get|post|put|delete] datasetURI graph [file]" cmd_soh when "s-get", "s-head", "s-put", "s-delete", "s-post" case $cmd when "s-get", "s-head", "s-delete" $banner="#{$cmd} datasetURI graph" when "s-put", "s-post" $banner="#{$cmd} datasetURI graph file" end cmd2 = $cmd.sub(/^s-/, '').upcase cmd_soh cmd2 when "s-query", "sparql-query" cmd_sparql_query when "s-update", "sparql-update" cmd_sparql_update true when "s-update-form", "sparql-update-form" cmd_sparql_update false else warn_exit "Unknown: "+$cmd, 1 end