diff options
-rw-r--r-- | lib/algorithm.rb | 19 | ||||
-rw-r--r-- | lib/authorization.rb | 43 | ||||
-rw-r--r-- | lib/compound.rb | 6 | ||||
-rw-r--r-- | lib/dataset.rb | 34 | ||||
-rw-r--r-- | lib/error.rb | 75 | ||||
-rw-r--r-- | lib/feature.rb | 23 | ||||
-rw-r--r-- | lib/helper.rb | 10 | ||||
-rw-r--r-- | lib/model.rb | 59 | ||||
-rw-r--r-- | lib/opentox-ruby.rb | 5 | ||||
-rw-r--r-- | lib/overwrite.rb | 91 | ||||
-rw-r--r-- | lib/policy.rb | 10 | ||||
-rw-r--r-- | lib/rest_client_wrapper.rb | 175 | ||||
-rw-r--r-- | lib/serializer.rb | 131 | ||||
-rw-r--r-- | lib/task.rb | 164 | ||||
-rwxr-xr-x | lib/to-html.rb | 81 |
15 files changed, 753 insertions, 173 deletions
diff --git a/lib/algorithm.rb b/lib/algorithm.rb index a2f7786..58a2640 100644 --- a/lib/algorithm.rb +++ b/lib/algorithm.rb @@ -13,9 +13,10 @@ module OpenTox # Execute algorithm with parameters, please consult the OpenTox API and the webservice documentation for acceptable parameters # @param [optional,Hash] params Algorithm parameters + # @param [optional,OpenTox::Task] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly # @return [String] URI of new resource (dataset, model, ...) - def run(params=nil) - RestClientWrapper.post(@uri, {:accept => 'text/uri-list'}, params).to_s + def run(params=nil, waiting_task=nil) + RestClientWrapper.post(@uri, {:accept => 'text/uri-list'}, params, waiting_task).to_s end # Get OWL-DL representation in RDF/XML format @@ -29,6 +30,20 @@ module OpenTox # Generic Algorithm class, should work with all OpenTox webservices class Generic include Algorithm + + # Find Generic Opentox Algorithm via URI, and loads metadata + # @param [String] uri Algorithm URI + # @return [OpenTox::Algorithm::Generic] Algorithm instance, nil if alogrithm was not found + def self.find(uri) + alg = Generic.new(uri) + alg.load_metadata + if alg.metadata==nil or alg.metadata.size==0 + nil + else + alg + end + end + end # Fminer algorithms (https://github.com/amaunz/fminer2) diff --git a/lib/authorization.rb b/lib/authorization.rb index 7e898cc..5bc690a 100644 --- a/lib/authorization.rb +++ b/lib/authorization.rb @@ -134,6 +134,20 @@ module OpenTox end end + # Lists policies alongside with affected uris + # @param [String] subjectid + # @return [Hash] keys: all policies of the subjectid owner, values: uris affected by those policies + def self.list_policy_uris( subjectid ) + names = list_policies(subjectid) + policies = {} + names.each do |n| + p = OpenTox::Policies.new + p.load_xml( list_policy(n, subjectid) ) + policies[n] = p.uris + end + policies + end + #Returns the owner (who created the first policy) of an URI # @param [String, String]uri,subjectid # return [String, nil]owner,nil returns owner of the URI @@ -273,15 +287,32 @@ module OpenTox return true end - #Checks (if subjectid is valid) if a policy exist and create default policy if not + # Checks (if subjectid is valid) if a policy exist and create default policy if not + # @param [String] uri + # @param [String] subjectid + # @return [Boolean] true if policy checked/created successfully (or no uri/subjectid given), false else def self.check_policy(uri, subjectid) + return true unless uri and subjectid token_valid = OpenTox::Authorization.is_token_valid(subjectid) LOGGER.debug "OpenTox::Authorization.check_policy with uri: #{uri}, subjectid: #{subjectid} is valid: #{token_valid}" - if uri and token_valid - if !uri_has_policy(uri, subjectid) - return send_policy(uri, subjectid) - else - LOGGER.debug "OpenTox::Authorization.check_policy URI: #{uri} has already a Policy." + # check if subjectid is valid + unless token_valid + # abort if invalid + LOGGER.error "OpenTox::Authorization.check_policy, subjectid NOT valid: #{subjectid}" + return false + end + + if !uri_has_policy(uri, subjectid) + # if no policy exists, create a policy, return result of send policy + send_policy(uri, subjectid) + else + LOGGER.debug "OpenTox::Authorization.check_policy URI: #{uri} has already a Policy." + # if policy exists check for POST rights + if authorize(uri, "POST", subjectid) + true + else + LOGGER.error "OpenTox::Authorization.check_policy, already exists, but no POST-authorization with subjectid: #{subjectid}" + false end end true diff --git a/lib/compound.rb b/lib/compound.rb index 6834860..a85507b 100644 --- a/lib/compound.rb +++ b/lib/compound.rb @@ -68,6 +68,12 @@ module OpenTox c end + # Get InChI + # @return [String] InChI string + def to_inchi + @inchi + end + # Get (canonical) smiles # @return [String] Smiles string def to_smiles diff --git a/lib/dataset.rb b/lib/dataset.rb index a85c2b5..640e3da 100644 --- a/lib/dataset.rb +++ b/lib/dataset.rb @@ -46,7 +46,7 @@ module OpenTox dataset.save(subjectid) dataset end - + # Find a dataset and load all data. This can be time consuming, use Dataset.new together with one of the load_* methods for a fine grained control over data loading. # @param [String] uri Dataset URI # @return [OpenTox::Dataset] Dataset object with all data @@ -242,6 +242,38 @@ module OpenTox def add_feature_metadata(feature,metadata) metadata.each { |k,v| @features[feature][k] = v } end + + # Add a new compound + # @param [String] compound Compound URI + def add_compound (compound) + @compounds << compound unless @compounds.include? compound + end + + # Creates a new dataset, by splitting the current dataset, i.e. using only a subset of compounds and features + # @param [Array] compounds List of compound URIs + # @param [Array] features List of feature URIs + # @param [Hash] metadata Hash containing the metadata for the new dataset + # @param [String] subjectid + # @return [OpenTox::Dataset] newly created dataset, already saved + def split( compounds, features, metadata, subjectid=nil) + LOGGER.debug "split dataset using "+compounds.size.to_s+"/"+@compounds.size.to_s+" compounds" + raise "no new compounds selected" unless compounds and compounds.size>0 + dataset = OpenTox::Dataset.create(CONFIG[:services]["opentox-dataset"],subjectid) + if features.size==0 + compounds.each{ |c| dataset.add_compound(c) } + else + compounds.each do |c| + features.each do |f| + @data_entries[c][f].each do |v| + dataset.add(c,f,v) + end + end + end + end + dataset.add_metadata(metadata) + dataset.save(subjectid) + dataset + end # Save dataset at the dataset service # - creates a new dataset if uri is not set diff --git a/lib/error.rb b/lib/error.rb new file mode 100644 index 0000000..e5c460d --- /dev/null +++ b/lib/error.rb @@ -0,0 +1,75 @@ + +# adding additional fields to Exception class to format errors according to OT-API +class Exception + attr_accessor :errorCause + def http_code; 500; end +end + +module OpenTox + + class BadRequestError < RuntimeError + def http_code; 400; end + end + + class NotAuthorizedError < RuntimeError + def http_code; 401; end + end + + class NotFoundError < RuntimeError + def http_code; 404; end + end + + class RestCallError < RuntimeError + attr_accessor :rest_params + def http_code; 502; end + end + + class ErrorReport + + # TODO replace params with URIs (errorCause -> OT.errorCause) + attr_reader :message, :actor, :errorCause, :http_code, :errorDetails, :errorType + + # creates a error report object, from an ruby-exception object + # @param [Exception] error + # @param [String] actor, URI of the call that cause the error + def initialize( error, actor ) + @http_code = error.http_code + @errorType = error.class.to_s + @message = error.message + @actor = actor + @errorCause = error.errorCause if error.errorCause + @rest_params = error.rest_params if error.is_a?(OpenTox::RestCallError) and error.rest_params + @backtrace = error.backtrace.join("\n") if CONFIG[:backtrace] + end + + # overwrite sorting to make easier readable + def to_yaml_properties + p = super + p = ( p - ["@backtrace"]) + ["@backtrace"] if @backtrace + p = ( p - ["@errorCause"]) + ["@errorCause"] if @errorCause + p + end + + def rdf_content() + c = { + RDF.type => OT.ErrorReport, + OT.statusCode => @http_code, + OT.message => @message, + OT.actor => @actor, + OT.errorCode => @errorType, + } + c[OT.errorCause] = @errorCause.rdf_content if @errorCause + c + end + + def self.from_rdf(rdf) + raise "not yet implemented" + end + + def to_rdfxml + s = Serializer::Owl.new + s.add_resource(CONFIG[:services]["opentox-task"]+"/tmpId/ErrorReport/tmpId", OT.errorReport, rdf_content) + s.to_rdfxml + end + end +end
\ No newline at end of file diff --git a/lib/feature.rb b/lib/feature.rb index 349f8ae..28ac0c5 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -3,7 +3,7 @@ module OpenTox include OpenTox def self.find(uri, subjectid=nil) - feature = Feature.new uri + feature = Feature.new uri if (CONFIG[:yaml_hosts].include?(URI.parse(uri).host)) feature.add_metadata YAML.load(RestClientWrapper.get(uri,{:accept => "application/x-yaml", :subjectid => subjectid})) else @@ -11,5 +11,26 @@ module OpenTox end feature end + + # provides domain (possible target values) of classification feature + # @return [Array] list with possible target values + def domain + #TODO derieve from metadata / ontology + return [true, false] + end + + # provides feature type, possible types are "regression" or "classification" + # @return [String] feature type, unknown if OT.isA property is unknown/ not set + def feature_type + case metadata[OT.isA] + when /NominalFeature/ + "classification" + when /NumericFeature/ + "regression" + else + "unknown" + end + end + end end diff --git a/lib/helper.rb b/lib/helper.rb index 5fe1857..e82c8fb 100644 --- a/lib/helper.rb +++ b/lib/helper.rb @@ -9,11 +9,10 @@ helpers do end elsif !env["session"] && subjectid unless authorized?(subjectid) - throw(:halt, [401, "Not authorized.\n"]) - redirect back + raise OpenTox::NotAuthorizedError.new "Not authorized" end else - throw(:halt, [401, "Not authorized.\n"]) unless authorized?(subjectid) + raise OpenTox::NotAuthorizedError.new "Not authorized" unless authorized?(subjectid) end end @@ -60,7 +59,7 @@ helpers do end before do - unless unprotected_requests or CONFIG[:authorization][:free_request].include?(env['REQUEST_METHOD']) + unless !AA_SERVER or unprotected_requests or CONFIG[:authorization][:free_request].include?(env['REQUEST_METHOD']) begin subjectid = nil subjectid = session[:subjectid] if session[:subjectid] @@ -73,7 +72,8 @@ before do LOGGER.debug "OpenTox ruby api wrapper: helper before filter: NO subjectid for URI: #{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}#{request.env['REQUEST_URI']}" subjectid = "" end - protected!(subjectid) if AA_SERVER + @subjectid = subjectid + protected!(subjectid) end end diff --git a/lib/model.rb b/lib/model.rb index 6ef4af2..85be1b5 100644 --- a/lib/model.rb +++ b/lib/model.rb @@ -6,26 +6,54 @@ module OpenTox # Run a model with parameters # @param [Hash] params Parameters for OpenTox model + # @param [optional,OpenTox::Task] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly # @return [text/uri-list] Task or resource URI - def run(params) + def run( params, waiting_task=nil ) if CONFIG[:yaml_hosts].include?(URI.parse(@uri).host) accept = 'application/x-yaml' else accept = 'application/rdf+xml' end - begin - RestClientWrapper.post(@uri,{:accept => accept},params).to_s - rescue => e - LOGGER.error "Failed to run #{@uri} with #{params.inspect} (#{e.inspect})" - raise "Failed to run #{@uri} with #{params.inspect}" - end + RestClientWrapper.post(@uri,{:accept => accept},params,waiting_task).to_s end # Generic OpenTox model class for all API compliant services class Generic include Model + + # Find Generic Opentox Model via URI, and loads metadata + # @param [String] uri Model URI + # @return [OpenTox::Model::Generic] Model instance, nil if model was not found + def self.find(uri) + model = Generic.new(uri) + model.load_metadata + if model.metadata==nil or model.metadata.size==0 + nil + else + model + end + end + + # provides feature type, possible types are "regression" or "classification" + # @return [String] feature type, "unknown" if type could not be estimated + def feature_type + # dynamically perform restcalls if necessary + load_metadata if @metadata==nil or @metadata.size==0 or (@metadata.size==1 && @metadata.values[0]==@uri) + @dependentVariable = OpenTox::Feature.find( @metadata[OT.dependentVariables] ) unless @dependentVariable + + [@dependentVariable.feature_type, @metadata[OT.isA], @metadata[DC.title], @uri].each do |type| + case type + when /(?i)classification/ + return "classification" + when /(?i)regression/ + return "regression" + end + end + raise "unknown model "+[@dependentVariable.feature_type, @metadata[OT.isA], @metadata[DC.title], @uri].inspect + end + end - + # Lazy Structure Activity Relationship class class Lazar @@ -89,8 +117,10 @@ module OpenTox # Predict a dataset # @param [String] dataset_uri Dataset URI + # @param [optional,subjectid] + # @param [optional,OpenTox::Task] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly # @return [OpenTox::Dataset] Dataset with predictions - def predict_dataset(dataset_uri, subjectid=nil) + def predict_dataset(dataset_uri, subjectid=nil, waiting_task=nil) @prediction_dataset = Dataset.create(CONFIG[:services]["opentox-dataset"], subjectid) @prediction_dataset.add_metadata({ OT.hasSource => @uri, @@ -99,9 +129,16 @@ module OpenTox OT.parameters => [{DC.title => "dataset_uri", OT.paramValue => dataset_uri}] }) d = Dataset.new(dataset_uri,subjectid) - d.load_compounds(subjectid) + d.load_compounds + count = 0 d.compounds.each do |compound_uri| - predict(compound_uri,false,subjectid) + begin + predict(compound_uri,false,subjectid) + count += 1 + waiting_task.progress( count/d.compounds.size.to_f*100.0 ) if waiting_task + rescue => ex + LOGGER.warn "prediction for compound "+compound_uri.to_s+" failed: "+ex.message + end end @prediction_dataset.save(subjectid) @prediction_dataset diff --git a/lib/opentox-ruby.rb b/lib/opentox-ruby.rb index c0bff95..735b845 100644 --- a/lib/opentox-ruby.rb +++ b/lib/opentox-ruby.rb @@ -1,4 +1,4 @@ -['rubygems', 'sinatra', 'sinatra/url_for', 'rest_client', 'yaml', 'cgi', 'spork', 'overwrite', 'environment'].each do |lib| +['rubygems', 'sinatra', 'sinatra/url_for', 'rest_client', 'yaml', 'cgi', 'spork', 'error', 'overwrite', 'environment'].each do |lib| require lib end @@ -8,6 +8,7 @@ rescue LoadError puts "Please install Openbabel with 'rake openbabel:install' in the compound component" end -['opentox', 'compound','dataset', 'parser','serializer', 'algorithm','model','task','validation','feature', 'rest_client_wrapper', 'authorization', 'policy', 'helper'].each do |lib| +['opentox', 'compound','dataset', 'parser','serializer', 'algorithm','model','task','validation','feature', + 'rest_client_wrapper', 'authorization', 'policy', 'helper', 'to-html' ].each do |lib| require lib end diff --git a/lib/overwrite.rb b/lib/overwrite.rb index f39fec3..e52618c 100644 --- a/lib/overwrite.rb +++ b/lib/overwrite.rb @@ -1,38 +1,71 @@ # class overwrites aka monkey patches -# hack: store sinatra in global var to make url_for and halt methods accessible -before{ $sinatra = self unless $sinatra } +# hack: store sinatra instance in global var $url_provider to make url_for and halt methods accessible +before { + raise "should not happen, url provider already differently initialized "+ + $url_provider.request.host.to_s+" != "+self.request.host.to_s if + $url_provider and $url_provider.request.host!=self.request.host and + $url_provider.request.script_name!=self.request.script_name + $url_provider = self + # stupid internet explorer does not ask for text/html, add this manually + request.env['HTTP_ACCEPT'] += ";text/html" if request.env["HTTP_USER_AGENT"]=~/MSIE/ +} -class Sinatra::Base - # overwriting halt to log halts (!= 202) - def halt(*response) - LOGGER.error "halt "+response.first.to_s+" "+(response.size>1 ? response[1].to_s : "") if response and response.first and response.first >= 300 - # orig sinatra code: - response = response.first if response.length == 1 - throw :halt, response +# Error handling +# Errors are logged as error and formated according to acccept-header +# Non OpenTox::Errors (defined in error.rb) are handled as internal error (500), stacktrace is logged +# IMPT: set sinatra settings :show_exceptions + :raise_errors to false in config.ru, otherwise Rack::Showexceptions takes over +error Exception do + error = request.env['sinatra.error'] + # log error to logfile + LOGGER.error error.class.to_s+": "+error.message + # log backtrace only if code is 500 -> unwanted (Runtime)Exceptions and internal errors (see error.rb) + LOGGER.error ":\n"+error.backtrace.join("\n") if error.http_code==500 + + actor = "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}#{request.env['REQUEST_URI']}" + rep = OpenTox::ErrorReport.new(error, actor) + + case request.env['HTTP_ACCEPT'] + when /rdf/ + content_type 'application/rdf+xml' + halt error.http_code,rep.to_rdfxml + when /html/ + content_type 'text/html' + halt error.http_code,(OpenTox.text_to_html rep.to_yaml) + else + content_type 'application/x-yaml' + halt error.http_code,rep.to_yaml end end class String - def task_uri? - self.uri? && !self.match(/task/).nil? - end - - def dataset_uri? - self.uri? && !self.match(/dataset/).nil? - end - - def self.model_uri? - self.uri? && !self.match(/model/).nil? - end + def task_uri? + self.uri? && !self.match(/task/).nil? + end + + def dataset_uri? + self.uri? && !self.match(/dataset/).nil? + end + + def self.model_uri? + self.uri? && !self.match(/model/).nil? + end - def uri? - begin - u = URI::parse(self) - return (u.scheme!=nil and u.host!=nil) - rescue URI::InvalidURIError - return false - end + def uri? + begin + u = URI::parse(self) + return (u.scheme!=nil and u.host!=nil) + rescue URI::InvalidURIError + return false end + end + + def underscore + self.gsub(/::/, '/'). + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + tr("-", "_"). + downcase + end end require 'logger' @@ -52,7 +85,7 @@ class OTLogger < Logger n = 2 line = lines[n] - while (line =~ /spork.rb/ or line =~ /create/ or line =~ /ot-logger.rb/) + while (line =~ /spork.rb/ or line =~ /create/ or line =~ /overwrite.rb/) n += 1 line = lines[n] end @@ -63,7 +96,7 @@ class OTLogger < Logger end def format(msg) - pwd.ljust(18)+" :: "+msg.to_s+" :: "+trace+" :: "+($sinatra ? $sinatra.request.env['REMOTE_ADDR'] : nil).to_s + pwd.ljust(18)+" :: "+msg.to_s+" :: "+trace end def debug(msg) diff --git a/lib/policy.rb b/lib/policy.rb index 9c81fbd..8591d52 100644 --- a/lib/policy.rb +++ b/lib/policy.rb @@ -32,6 +32,11 @@ module OpenTox end return true end + + # @return [Array] set of arrays affected by policies + def uris + @policies.collect{ |k,v| v.uris }.flatten.uniq + end #drop all policies in a policies instance def names @@ -199,6 +204,11 @@ module OpenTox @subjects[name] = Subject.new(name, type, value) end + # @return [Array] set of uris affected by policy + def uris + @rules.collect{ |k,v| v.uri }.uniq + end + #rule inside a policy class Rule diff --git a/lib/rest_client_wrapper.rb b/lib/rest_client_wrapper.rb index 5f5273b..7c2d719 100644 --- a/lib/rest_client_wrapper.rb +++ b/lib/rest_client_wrapper.rb @@ -1,32 +1,4 @@ module OpenTox - - #PENDING: implement ot error api, move to own file - class Error - - attr_accessor :code, :body, :uri, :payload, :headers - - def initialize(code, body, uri, payload, headers) - self.code = code - self.body = body.to_s[0..1000] - self.uri = uri - self.payload = payload - self.headers = headers - end - - def self.parse(error_array_string) - begin - err = YAML.load(error_array_string) - if err and err.is_a?(Array) and err.size>0 and err[0].is_a?(Error) - return err - else - return nil - end - rescue - return nil - end - end - - end class WrapperResult < String attr_accessor :content_type, :code @@ -34,37 +6,61 @@ module OpenTox class RestClientWrapper - def self.get(uri, headers=nil, wait=true) - execute( "get", uri, headers, nil, wait) + # performs a GET REST call + # raises OpenTox::Error if call fails (rescued in overwrite.rb -> halt 502) + # per default: waits for Task to finish and returns result URI of Task + # @param [String] uri destination URI + # @param [optional,Hash] headers contains params like accept-header + # @param [optional,OpenTox::Task] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly + # @param [wait,Boolean] wait set to false to NOT wait for task if result is task + # @return [OpenTox::WrapperResult] a String containing the result-body of the REST call + def self.get(uri, headers=nil, waiting_task=nil, wait=true ) + execute( "get", uri, headers, nil, waiting_task, wait) end - def self.post(uri, headers, payload=nil, wait=true) - execute( "post", uri, headers, payload, wait ) + # performs a POST REST call + # raises OpenTox::Error if call fails (rescued in overwrite.rb -> halt 502) + # per default: waits for Task to finish and returns result URI of Task + # @param [String] uri destination URI + # @param [optional,Hash] headers contains params like accept-header + # @param [optional,String] payload data posted to the service + # @param [optional,OpenTox::Task] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly + # @param [wait,Boolean] wait set to false to NOT wait for task if result is task + # @return [OpenTox::WrapperResult] a String containing the result-body of the REST call + def self.post(uri, headers, payload=nil, waiting_task=nil, wait=true ) + execute( "post", uri, headers, payload, waiting_task, wait ) end + # performs a PUT REST call + # raises OpenTox::Error if call fails (rescued in overwrite.rb -> halt 502) + # @param [String] uri destination URI + # @param [optional,Hash] headers contains params like accept-header + # @param [optional,String] payload data put to the service + # @return [OpenTox::WrapperResult] a String containing the result-body of the REST call def self.put(uri, headers, payload=nil ) execute( "put", uri, headers, payload ) end - def self.delete(uri, headers=nil) + # performs a DELETE REST call + # raises OpenTox::Error if call fails (rescued in overwrite.rb -> halt 502) + # @param [String] uri destination URI + # @param [optional,Hash] headers contains params like accept-header + # @return [OpenTox::WrapperResult] a String containing the result-body of the REST call + def self.delete(uri, headers=nil ) execute( "delete", uri, headers, nil) end - def self.raise_uri_error(error_msg, uri, headers=nil, payload=nil) - do_halt( "-", error_msg, uri, headers, payload ) - end - private - def self.execute( rest_call, uri, headers, payload=nil, wait=true ) + def self.execute( rest_call, uri, headers, payload=nil, waiting_task=nil, wait=true ) - do_halt 400,"uri is null",uri,headers,payload unless uri - do_halt 400,"not a uri",uri,headers,payload unless uri.to_s.uri? - do_halt 400,"headers are no hash",uri,headers,payload unless headers==nil or headers.is_a?(Hash) - do_halt 400,"nil headers for post not allowed, use {}",uri,headers,payload if rest_call=="post" and headers==nil + raise OpenTox::BadRequestError.new "uri is null" unless uri + raise OpenTox::BadRequestError.new "not a uri: "+uri.to_s unless uri.to_s.uri? + raise OpenTox::BadRequestError.new "headers are no hash: "+headers.inspect unless headers==nil or headers.is_a?(Hash) + raise OpenTox::BadRequestError.new "nil headers for post not allowed, use {}" if rest_call=="post" and headers==nil headers.each{ |k,v| headers.delete(k) if v==nil } if headers #remove keys with empty values, as this can cause problems begin - #LOGGER.debug "RestCall: "+rest_call.to_s+" "+uri.to_s+" "+headers.inspect + #LOGGER.debug "RestCall: "+rest_call.to_s+" "+uri.to_s+" "+headers.inspect+" "+payload.inspect resource = RestClient::Resource.new(uri,{:timeout => 60}) if payload result = resource.send(rest_call, payload, headers) @@ -80,32 +76,31 @@ module OpenTox raise "content-type not set" unless res.content_type res.code = result.code + #LOGGER.debug "RestCall result: "+res.to_s+" "+res.code.to_s+" "+res.content_type.to_s # TODO: Ambit returns task representation with 200 instead of result URI return res if res.code==200 || !wait while (res.code==201 || res.code==202) - res = wait_for_task(res, uri) + res = wait_for_task(res, uri, waiting_task) end raise "illegal status code: '"+res.code.to_s+"'" unless res.code==200 return res rescue RestClient::RequestTimeout => ex - do_halt 408,ex.message,uri,headers,payload + received_error ex.message, 408, nil, {:rest_uri => uri, :headers => headers} + rescue RestClient::ExceptionWithResponse => ex + # error comming from a different webservice, + received_error ex.http_body, ex.http_code, ex.response.net_http_res.content_type, {:rest_uri => uri, :headers => headers} + rescue OpenTox::RestCallError => ex + # already a rest-error, probably comes from wait_for_task, just pass through + raise ex rescue => ex - #raise ex - #raise "'"+ex.message+"' uri: "+uri.to_s - begin - code = ex.http_code - msg = ex.http_body - rescue - code = 500 - msg = ex.to_s - end - do_halt code,msg,uri,headers,payload + # some internal error occuring in rest_client_wrapper, just pass through + raise ex end end - def self.wait_for_task( res, base_uri ) + def self.wait_for_task( res, base_uri, waiting_task=nil ) task = nil case res.content_type @@ -115,53 +110,55 @@ module OpenTox task = OpenTox::Task.from_yaml(res) when /text\// raise "uri list has more than one entry, should be a task" if res.content_type=~/text\/uri-list/ and res.split("\n").size > 1 #if uri list contains more then one uri, its not a task - task = OpenTox::Task.find(res.to_s) if res.to_s.uri? + task = OpenTox::Task.find(res.to_s.chomp) if res.to_s.uri? else raise "unknown content-type for task: '"+res.content_type.to_s+"'" #+"' content: "+res[0..200].to_s end LOGGER.debug "result is a task '"+task.uri.to_s+"', wait for completion" - task.wait_for_completion - raise task.description unless task.completed? # maybe task was cancelled / error - + task.wait_for_completion waiting_task + unless task.completed? # maybe task was cancelled / error + if task.errorReport + received_error task.errorReport, task.http_code, nil, {:rest_uri => task.uri, :rest_code => task.http_code} + else + raise "task status: '"+task.status.to_s+"' but errorReport nil" + end + end + res = WrapperResult.new task.result_uri res.code = task.http_code res.content_type = "text/uri-list" return res end - def self.do_halt( code, body, uri, headers, payload=nil ) - - #build error - causing_errors = Error.parse(body) - if causing_errors - error = causing_errors + [Error.new(code, "subsequent error", uri, payload, headers)] + def self.received_error( body, code, content_type=nil, params=nil ) + + # try to parse body + report = nil + if body.is_a?(OpenTox::ErrorReport) + report = body else - error = [Error.new(code, body, uri, payload, headers)] + case content_type + when /yaml/ + report = YAML.load(body) + when /rdf/ + report = OpenTox::ErrorReport.from_rdf(body) + end end - #debug utility: write error to file - error_dir = "/tmp/ot_errors" - FileUtils.mkdir(error_dir) unless File.exist?(error_dir) - raise "could not create error dir" unless File.exist?(error_dir) and File.directory?(error_dir) - file_name = "error" - time=Time.now.strftime("%m.%d.%Y-%H:%M:%S") - count = 1 - count+=1 while File.exist?(File.join(error_dir,file_name+"_"+time+"_"+count.to_s)) - File.new(File.join(error_dir,file_name+"_"+time+"_"+count.to_s),"w").puts(body) - - # handle error - # we are either in a task, or in sinatra - # PENDING: always return yaml for now - - if $self_task #this global var in Task.create to mark that the current process is running in a task - raise error.to_yaml # the error is caught, logged, and task state is set to error in Task.create - #elsif $sinatra #else halt sinatra - #$sinatra.halt(502,error.to_yaml) - elsif defined?(halt) - halt(502,error.to_yaml) - else #for testing purposes (if classes used directly) - raise error.to_yaml + unless report + # parsing was not successfull + # raise 'plain' RestCallError + err = OpenTox::RestCallError.new("REST call returned error: '"+body.to_s+"'") + err.rest_params = params + raise err + else + # parsing sucessfull + # raise RestCallError with parsed report as error cause + err = OpenTox::RestCallError.new("REST call subsequent error") + err.errorCause = report + err.rest_params = params + raise err end end end diff --git a/lib/serializer.rb b/lib/serializer.rb index 495702a..44b4414 100644 --- a/lib/serializer.rb +++ b/lib/serializer.rb @@ -14,7 +14,7 @@ module OpenTox def initialize @object = { - # this should come from opntox.owl + # this should come from opentox.owl OT.Compound => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , OT.Feature => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , OT.NominalFeature => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , @@ -26,6 +26,16 @@ module OpenTox OT.Algorithm => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , OT.Parameter => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , OT.Task => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + #classes for validation + OT.Validation => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.ClassificationStatistics => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.ConfusionMatrix => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.ConfusionMatrixCell => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.ClassValueStatistics => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.RegressionStatistics => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.Crossvalidation => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.CrossvalidationInfo => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , + OT.ErrorReport => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } , OT.compound => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , OT.feature => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , @@ -34,6 +44,22 @@ module OpenTox OT.values => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , OT.algorithm => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , OT.parameters => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + #object props for validation# + OT.model => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.trainingDataset => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.predictionFeature => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.predictionDataset => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.crossvalidation => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.testTargetDataset => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.testDataset => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.classificationStatistics => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.confusionMatrix => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.confusionMatrixCell => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.classValueStatistics => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.regressionStatistics => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.validation => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.crossvalidationInfo => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , + OT.dataset => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } , DC.title => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , DC.identifier => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , @@ -47,6 +73,51 @@ module OpenTox OT.hasStatus => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , OT.resultURI => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , OT.percentageCompleted => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + # annotation props for validation + OT.numUnpredicted => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.crossvalidationFold => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numInstances => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numWithoutClass => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.percentWithoutClass => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.percentUnpredicted => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.confusionMatrixActual => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.confusionMatrixPredicted => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.confusionMatrixValue => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numIncorrect => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.percentCorrect => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numCorrect => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.accuracy => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.trueNegativeRate => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.truePositiveRate => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.falseNegativeRate => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.falsePositiveRate => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numTrueNegatives => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numTruePositives => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numFalseNegatives => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numFalsePositives => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.classValue => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.precision => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.areaUnderRoc => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.weightedAreaUnderRoc => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.fMeasure => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.percentIncorrect => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.validationType => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.realRuntime => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.sampleCorrelationCoefficient => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.targetVarianceActual => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.targetVariancePredicted => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.meanAbsoluteError => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.sumSquaredError => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.rootMeanSquaredError => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.rSquare => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.stratified => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.numFolds => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.randomSeed => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.reportType => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.message => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.statusCode => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.actor => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , + OT.errorCode => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } , OT.hasSource => { RDF["type"] => [{ "type" => "uri", "value" => OWL.DatatypeProperty }] } , OT.value => { RDF["type"] => [{ "type" => "uri", "value" => OWL.DatatypeProperty }] } , @@ -121,6 +192,64 @@ module OpenTox @object[uri] = { RDF["type"] => [{ "type" => "uri", "value" => OT.Task }] } add_metadata uri, metadata end + + # Add a resource defined by resource_class and content + # (see documentation of add_content for example) + # @param [String] uri of resource + # @param [String] resource class, e.g. OT.Validation + # @param [Hash] content as hash + def add_resource(uri, resource_class, content) + @object[uri] = { RDF["type"] => [{ "type" => "uri", "value" => resource_class }] } + @@content_id = 1 + add_content uri, content + end + + private + @@content_id = 1 + + # Recursiv function to add content + # @example + # { DC.description => "bla", + # OT.similar_resources => [ "http://uri1", "http://uri2" ], + # OT.matrixCells => + # [ { RDF.type => OT.MatrixCell, OT.cellIndex=1 OT.cellValue => "xy" }, + # { RDF.type => OT.MatrixCell, OT.cellIndex=2 OT.cellValue => "z" } ], + # OT.info => { RDF.type => OT.ImportantInfo, + # DC.description => "blub" } + # } + # @param [String] uri + # @param [Hash] content as hash, uri must already have been added to @object + def add_content(uri, hash) + raise "content is no hash: "+hash.class.to_s unless hash.is_a?(Hash) + hash.each do |u,v| + if v.is_a? Hash + # value is again a hash, i.e. a new owl class is added + # first make sure type (==class) is set + type = v[RDF.type] + raise "type missing for "+u.to_s+" content:\n"+v.inspect unless type + raise "class unknown "+type.to_s+" (for "+u.to_s+")" unless @object.has_key?(type) + # create new node and add to current uri + genid = "_:#{type.split('#')[-1]}#{@@content_id}" + @@content_id += 1 + @object[uri] = {} unless @object[uri] + @object[uri][u] = [{ "type" => "bnode", "value" => genid }] + # add content to new class + add_content(genid,v) + elsif v.is_a? Array + # value is an array, i.e. a list of values with property is added + v.each{ |vv| add_content( uri, { u => vv } ) } + else # v.is_a? String + # simple string value + @object[uri] = {} unless @object[uri] + @object[uri][u] = [] unless @object[uri][u] + raise "property unknown "+u.to_s if !@object.has_key?(u) and u!=RDF.type + # use << to allow different values for one property + @object[uri][u] << {"type" => type(v), "value" => v } + end + end + end + + public # Add metadata # @param [Hash] metadata diff --git a/lib/task.rb b/lib/task.rb index 9cf909f..3c6aba5 100644 --- a/lib/task.rb +++ b/lib/task.rb @@ -1,4 +1,3 @@ -$self_task=nil module OpenTox @@ -13,7 +12,7 @@ module OpenTox DC.title => "", DC.date => "", OT.hasStatus => "Running", - OT.percentageCompleted => "0", + OT.percentageCompleted => 0.0, OT.resultURI => "", DC.creator => "", # not mandatory according to API DC.description => "", # not mandatory according to API @@ -57,27 +56,17 @@ module OpenTox # #raise "Server too busy to start a new task" #end - task_pid = Spork.spork(:logger => LOGGER) do LOGGER.debug "Task #{task.uri} started #{Time.now}" - $self_task = task - begin - result = catch(:halt) do - yield task - end - # catching halt, set task state to error - if result && result.is_a?(Array) && result.size==2 && result[0]>202 - LOGGER.error "task was halted: "+result.inspect - task.error(result[1]) - return - end + result = yield task LOGGER.debug "Task #{task.uri} done #{Time.now} -> "+result.to_s task.completed(result) - rescue => ex - LOGGER.error "task failed: "+ex.message - LOGGER.error ": "+ex.backtrace.join("\n") - task.error(ex.message) + rescue => error + LOGGER.error "task failed: "+error.class.to_s+": "+error.message + # log backtrace only if code is 500 -> unwanted (Runtime)Exceptions and internal errors (see error.rb) + LOGGER.error ":\n"+error.backtrace.join("\n") if error.http_code==500 + task.error(OpenTox::ErrorReport.new(error, creator)) end end task.pid = task_pid @@ -113,7 +102,9 @@ module OpenTox def to_rdfxml s = Serializer::Owl.new + @metadata[OT.errorReport] = @uri+"/ErrorReport/tmpId" if @error_report s.add_task(@uri,@metadata) + s.add_resource(@uri+"/ErrorReport/tmpId", OT.errorReport, @error_report.rdf_content) if @error_report s.to_rdfxml end @@ -129,6 +120,10 @@ module OpenTox @metadata[DC.description] end + def errorReport + @metadata[OT.errorReport] + end + def cancel RestClientWrapper.put(File.join(@uri,'Cancelled')) load_metadata @@ -139,11 +134,17 @@ module OpenTox load_metadata end - def error(description) - RestClientWrapper.put(File.join(@uri,'Error'),{:description => description.to_s[0..2000]}) + def error(error_report) + raise "no error report" unless error_report.is_a?(OpenTox::ErrorReport) + RestClientWrapper.put(File.join(@uri,'Error'),{:errorReport => error_report.to_yaml}) load_metadata end + # not stored just for to_rdf + def add_error_report( error_report ) + @error_report = error_report + end + def pid=(pid) RestClientWrapper.put(File.join(@uri,'pid'), {:pid => pid}) end @@ -162,12 +163,12 @@ module OpenTox def load_metadata if (CONFIG[:yaml_hosts].include?(URI.parse(uri).host)) - result = RestClientWrapper.get(@uri, {:accept => 'application/x-yaml'}, false) + result = RestClientWrapper.get(@uri, {:accept => 'application/x-yaml'}, nil, false) @metadata = YAML.load result.to_s @http_code = result.code else @metadata = Parser::Owl::Generic.new(@uri).load_metadata - @http_code = RestClientWrapper.get(uri, {:accept => 'application/rdf+xml'}, false).code + @http_code = RestClientWrapper.get(uri, {:accept => 'application/rdf+xml'}, nil, false).code end end @@ -218,7 +219,9 @@ module OpenTox =end # waits for a task, unless time exceeds or state is no longer running - def wait_for_completion(dur=0.3) + # @param [optional,OpenTox::Task] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly + # @param [optional,Numeric] dur seconds pausing before cheking again for completion + def wait_for_completion( waiting_task=nil, dur=0.3) due_to_time = Time.new + DEFAULT_TASK_MAX_DURATION LOGGER.debug "start waiting for task "+@uri.to_s+" at: "+Time.new.to_s+", waiting at least until "+due_to_time.to_s @@ -228,15 +231,29 @@ module OpenTox while self.running? sleep dur load_metadata + # if another (sub)task is waiting for self, set progress accordingly + waiting_task.progress(@metadata[OT.percentageCompleted]) if waiting_task check_state if (Time.new > due_to_time) raise "max wait time exceeded ("+DEFAULT_TASK_MAX_DURATION.to_s+"sec), task: '"+@uri.to_s+"'" end end - LOGGER.debug "Task '"+@metadata[OT.hasStatus]+"': "+@uri.to_s+", Result: "+@metadata[OT.resultURI].to_s + LOGGER.debug "Task '"+@metadata[OT.hasStatus].to_s+"': "+@uri.to_s+", Result: "+@metadata[OT.resultURI].to_s end - + + # updates percentageCompleted value (can only be increased) + # task has to be running + # @param [Numeric] pct value between 0 and 100 + def progress(pct) + #puts "task := "+pct.to_s + raise "no numeric >= 0 and <= 100 : '"+pct.to_s+"'" unless pct.is_a?(Numeric) and pct>=0 and pct<=100 + if (pct > @metadata[OT.percentageCompleted] + 0.0001) + RestClientWrapper.put(File.join(@uri,'Running'),{:percentageCompleted => pct}) + load_metadata + end + end + private def check_state begin @@ -251,10 +268,105 @@ module OpenTox "'" unless @metadata[OT.resultURI] and @metadata[OT.resultURI].to_s.uri? end rescue => ex - RestClientWrapper.raise_uri_error(ex.message, @uri) + raise OpenTox::BadRequestError.new ex.message+" (task-uri:"+@uri+")" + end + end + + end + + # Convenience class to split a (sub)task into subtasks + # + # example: + # a crossvalidation is split into creating datasets and performing the validations + # creating the dataset is 1/3 of the work, perform the validations is 2/3: + # Task.as_task do |task| + # create_datasets( SubTask.new(task, 0, 33) ) + # perfom_validations( SubTask.new(task, 33, 100) ) + # end + # inside the create_datasets / perform_validations you can use subtask.progress(<val>) + # with vals from 0-100 + # + # note that you can split a subtask into further subtasks + class SubTask + + def initialize(task, min, max) + raise "not a task or subtask" unless task.is_a?(Task) or task.is_a?(SubTask) + raise "invalid max ("+max.to_s+"), min ("+min.to_s+") params" unless + min.is_a?(Numeric) and max.is_a?(Numeric) and min >= 0 and max <= 100 and max > min + @task = task + @min = min + @max = max + @delta = max - min + end + + # convenience method to handle null tasks + def self.create(task, min, max) + if task + SubTask.new(task, min, max) + else + nil end end + + def progress(pct) + raise "no numeric >= 0 and <= 100 : '"+pct.to_s+"'" unless pct.is_a?(Numeric) and pct>=0 and pct<=100 + #puts "subtask := "+pct.to_s+" -> task := "+(@min + @delta * pct.to_f * 0.01).to_s + @task.progress( @min + @delta * pct.to_f * 0.01 ) + end + + def running?() + @task.running? + end + end + + # The David Gallagher feature: + # a fake sub task to keep the progress bar movin for external jobs + # note: param could be a subtask + # + # usage (for a call that is normally finished in under 60 seconds): + # fsk = FakeSubTask.new(task, 60) + # external_lib_call.start + # external_lib_call.wait_until_finished + # fsk.finished + # + # what happens: + # the FakeSubTask updates the task.progress each second until + # runtime is up or the finished mehtod is called + # + # example if the param runtime is too low: + # 25% .. 50% .. 75% .. 100% .. 100% .. 100% .. 100% .. 100% + # example if the param runtime is too high: + # 5% .. 10% .. 15% .. 20% .. 25% .. 30% .. 35% .. 100% + # the latter example is better (keep the bar movin!) + # -> better make a conservative runtime estimate + class FakeSubTask + + def initialize(task, runtime) + @task = task + @thread = Thread.new do + timeleft = runtime + while (timeleft > 0 and @task.running?) + sleep 1 + timeleft -= 1 + @task.progress( (runtime - timeleft) / runtime.to_f * 100 ) + end + end + end + + # convenience method to handle null tasks + def self.create(task, runtime) + if task + FakeSubTask.new(task, runtime) + else + nil + end + end + + def finished + @thread.exit + @task.progress(100) if @task.running? + end end end diff --git a/lib/to-html.rb b/lib/to-html.rb new file mode 100755 index 0000000..e9764ef --- /dev/null +++ b/lib/to-html.rb @@ -0,0 +1,81 @@ + +OT_LOGO = "http://opentox.informatik.uni-freiburg.de/ot-logo.png" + + +class String + + # encloses URI in text with with link tag + # @return [String] new text with marked links + def link_urls + self.gsub(/(?i)http:\/\/[^\r\n\s']*/, '<a href=\0>\0</a>') + end +end + +module OpenTox + + # produces a html page for making web services browser friendly + # format of text (=string params) is preserved (e.g. line breaks) + # urls are marked as links + # @example post params: + # [ [ [:mandatory_param_1], [:mandatory_param_2], [:optional_param,"default_value"] ], + # [ [:alteranative_mandatory_param_1], [:alteranative_mandatory_param_2] ] + # ] + # @param [String] text this is the actual content, + # @param [optional,String] related_links info on related resources + # @param [optional,String] description general info + # @param [optional,Array] post_params, array of arrays containing info on POST operation, see example + # @return [String] html page + def self.text_to_html( text, related_links=nil, description=nil, post_params=nil ) + + # TODO add title as parameter + title = nil #$sinatra.url_for($sinatra.request.env['PATH_INFO'], :full) if $sinatra + + html = <<EOF +<html> +EOF + html.chomp! + html += "<title>"+title+"</title>" if title + html += <<EOF +<img src=" +EOF + html.chomp! + html += OT_LOGO + html += <<EOF +"> +<body> +EOF + html.chomp! + html += "<h3>Description</h3><pre><p>"+description.link_urls+"</p></pre>" if description + html += "<h3>Related links</h3><pre><p>"+related_links.link_urls+"</p></pre>" if related_links + if post_params + html += "<h3>POST parameters</h3>" + count = 0 + post_params.each do |p| + html += "<pre><p>alternatively:</p></pre>" if count > 0 + html += "<pre><p><table><thead><tr><th>param</th><th>default_value</th></tr></thead>" + p.each do |k,v| + html += "<tr><th>"+k.to_s+"</th><th>"+(v!=nil ? v.to_s : "<i>mandatory</i>")+"</th></tr>" + end + html += "</table></p></pre>" + count += 1 + end + end + html += "<h3>Content</h3>" if description || related_links + html += <<EOF +<pre> +<p style="padding:15px; border:10px solid #5D308A"> +EOF + html.chomp! + html += text.link_urls + html += <<EOF +</p> +</pre> +</body> +<html> +EOF + html + end + +end + +#puts OpenTox.text_to_html("bla")
\ No newline at end of file |