summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Helma <helma@in-silico.ch>2011-03-09 12:31:05 +0100
committerChristoph Helma <helma@in-silico.ch>2011-03-09 12:31:05 +0100
commitc651fe10fe5e09dbfdf2b3abd420fc2fc0051937 (patch)
tree30029f9c3246de739aaf7b66a490c01b7b147f04
parent5233ab341757557c536ed8cc8eefd79a936b4295 (diff)
parent66f2ee967317954568562510111b0d832881547d (diff)
Merge branch 'release/v1.0.0'v1.0.0
Conflicts: lib/dataset.rb
-rw-r--r--README.markdown41
-rw-r--r--README.rdoc23
-rw-r--r--Rakefile74
-rw-r--r--VERSION2
-rwxr-xr-xbin/yaml2owl.rb18
-rw-r--r--lib/algorithm.rb277
-rw-r--r--lib/authorization.rb401
-rw-r--r--lib/compound.rb215
-rw-r--r--lib/config/config_ru.rb1
-rw-r--r--lib/dataset.rb532
-rw-r--r--lib/environment.rb84
-rw-r--r--lib/error.rb99
-rw-r--r--lib/feature.rb43
-rw-r--r--lib/features.rb19
-rw-r--r--lib/helper.rb107
-rw-r--r--lib/model.rb391
-rw-r--r--lib/ontology_service.rb43
-rw-r--r--lib/opentox-ruby-api-wrapper.rb13
-rw-r--r--lib/opentox-ruby.rb14
-rw-r--r--lib/opentox.rb52
-rw-r--r--lib/ot-logger.rb48
-rw-r--r--lib/overwrite.rb147
-rw-r--r--lib/owl.rb552
-rw-r--r--lib/parser.rb381
-rw-r--r--lib/policy.rb261
-rw-r--r--lib/rest_client_wrapper.rb223
-rw-r--r--lib/serializer.rb469
-rw-r--r--lib/task.rb430
-rw-r--r--lib/templates/config.yaml45
-rw-r--r--lib/templates/default_guest_policy.xml53
-rw-r--r--lib/templates/default_policy.xml53
-rw-r--r--lib/templates/users.yaml5
-rw-r--r--lib/to-html.rb112
-rw-r--r--lib/utils.rb50
-rw-r--r--lib/validation.rb204
-rw-r--r--opentox-ruby.gemspec (renamed from opentox-ruby-api-wrapper.gemspec)43
36 files changed, 4016 insertions, 1509 deletions
diff --git a/README.markdown b/README.markdown
new file mode 100644
index 0000000..79bdab2
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,41 @@
+opentox-ruby
+============
+
+Ruby wrapper for the [OpenTox](http://www.opentox.org) REST API
+
+Installation
+------------
+
+opentox-ruby depends on many third party programs and libraries, which makes the setup complicated and error prone. For this reason we recommend to use the installer from [opentox-install](http://github.com/opentox/opentox-install). If you want to install manually you can find the necessary steps in the installation scripts.
+
+Quickstart
+----------
+
+This example shows how to create a lazar model and predict a compound, it assumes that you have access to a working installation of OpenTox services with corresponding settings in $HOME/.opentox/config. Run the following code in irb or from a ruby script:
+
+ require 'rubygems'
+ require 'opentox-ruby'
+
+ # Authenticate
+ subjectid = OpenTox::Authorization.authenticate(USER,PASSWORD)
+
+ # Upload a dataset
+ training_dataset = OpenTox::Dataset.create_from_csv_file(TRAINING_DATASET, subjectid)
+
+ # Create a prediction model
+ model_uri = OpenTox::Algorithm::Lazar.new.run({:dataset_uri => training_dataset.uri, :subjectid => subjectid}).to_s
+ lazar = OpenTox::Model::Lazar.find model_uri, subjectid
+
+ # Predict a compound
+ compound = OpenTox::Compound.from_smiles("c1ccccc1NN")
+ prediction_uri = lazar.run(:compound_uri => compound.uri, :subjectid => subjectid)
+ prediction = OpenTox::LazarPrediction.find(prediction_uri, subjectid)
+ puts prediction.to_yaml
+
+[API documentation](http://rdoc.info/gems/opentox-ruby/1.0.0/frames)
+-------------------------------------------------------------------
+
+Copyright
+---------
+
+Copyright (c) 2009-2011 Christoph Helma, Martin Guetlein, Micha Rautenberg, Andreas Maunz, David Vorgrimmler, Denis Gebele. See LICENSE for details.
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index e337907..0000000
--- a/README.rdoc
+++ /dev/null
@@ -1,23 +0,0 @@
-= opentox-ruby-api-wrapper
-
-Ruby wrapper for the OpenTox REST API (http://www.opentox.org)
-
-== Installation
-
-Run the following if you haven't already:
-
- gem sources -a http://gems.github.com
-
-Install the gem:
-
- sudo gem install helma-opentox-ruby-api-wrapper
-
-== Usage
-
-- adjust the settings in $HOME/.opentox/config
-- require 'opentox-ruby-api-wrapper' in your ruby application
-- consult the rdoc API documentation for details
-
-== Copyright
-
-Copyright (c) 2009 Christoph Helma. See LICENSE for details.
diff --git a/Rakefile b/Rakefile
index 3846bd1..b496cc2 100644
--- a/Rakefile
+++ b/Rakefile
@@ -4,46 +4,50 @@ require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
- gem.name = "opentox-ruby-api-wrapper"
+ gem.name = "opentox-ruby"
gem.summary = %Q{Ruby wrapper for the OpenTox REST API}
gem.description = %Q{Ruby wrapper for the OpenTox REST API (http://www.opentox.org)}
gem.email = "helma@in-silico.ch"
- gem.homepage = "http://github.com/helma/opentox-ruby-api-wrapper"
- gem.authors = ["Christoph Helma, Martin Guetlein"]
- # dependencies
- [ "sinatra",
- "emk-sinatra-url-for",
- "sinatra-respond_to",
- "sinatra-static-assets",
- "rest-client",
- "rack",
- "rack-contrib",
- "rack-flash",
- "nokogiri",
- "rubyzip",
- "builder",
- "roo",
- "spreadsheet",
- "google-spreadsheet-ruby",
- "tmail",
- "rinruby",
- "rjb"
- ].each { |dep| gem.add_dependency dep }
- [ "dm-core",
- 'dm-serializer',
- 'dm-timestamps',
- 'dm-types',
- 'dm-migrations',
- "dm-mysql-adapter",
+ gem.homepage = "http://github.com/helma/opentox-ruby"
+ gem.authors = ["Christoph Helma, Martin Guetlein, Andreas Maunz, Micha Rautenberg, David Vorgrimmler"]
+ # dependencies
+ [ "sinatra",
+ "emk-sinatra-url-for",
+ "sinatra-respond_to",
+ "sinatra-static-assets",
+ "rest-client",
+ "rack",
+ "rack-contrib",
+ "rack-flash",
+ "nokogiri",
+ "rubyzip",
+ "roo",
+ "spreadsheet",
+ "google-spreadsheet-ruby",
+ "yajl-ruby",
+ "tmail",
+ "rinruby",
+ "ohm",
+ "SystemTimer",
+ "rjb"
+ ].each { |dep| gem.add_dependency dep }
+=begin
+ [ "dm-core",
+ 'dm-serializer',
+ 'dm-timestamps',
+ 'dm-types',
+ 'dm-migrations',
+ "dm-mysql-adapter",
"dm-validations",
- ].each {|dep| gem.add_dependency dep, ">= 1" }
- gem.add_dependency "haml", ">=3"
- ['cucumber','jeweler'].each { |dep| gem.add_development_dependency dep }
- gem.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
- gem.files.include %w(lib/tasks/owl.rb, lib/environment.rb, lib/algorithm.rb, lib/compound.rb, lib/dataset.rb, lib/model.rb, lib/utils.rb, lib/validation.rb, lib/templates/*)
+ ].each {|dep| gem.add_dependency dep, ">= 1" }
+=end
+ gem.add_dependency "haml", ">=3"
+ ['jeweler'].each { |dep| gem.add_development_dependency dep }
+ gem.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
+ #gem.files.include %w(lib/environment.rb, lib/algorithm.rb, lib/compound.rb, lib/dataset.rb, lib/model.rb, lib/validation.rb, lib/templates/*)
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
- Jeweler::GemcutterTasks.new
+ Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
end
@@ -81,7 +85,7 @@ Rake::RDocTask.new do |rdoc|
end
rdoc.rdoc_dir = 'rdoc'
- rdoc.title = "opentox-ruby-api-wrapper #{version}"
+ rdoc.title = "opentox-ruby #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
diff --git a/VERSION b/VERSION
index 9f05f9f..afaf360 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.6.5
+1.0.0 \ No newline at end of file
diff --git a/bin/yaml2owl.rb b/bin/yaml2owl.rb
deleted file mode 100755
index 1002912..0000000
--- a/bin/yaml2owl.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env ruby
-require 'rubygems'
-require 'opentox-ruby-api-wrapper'
-
-input = YAML.load_file(ARGV[0])
-dataset = OpenTox::Dataset.new
-dataset.title = input[:title]
-dataset.creator = input[:source]
-input[:data].each do |c,f|
- f.each do |k,v|
- v.each do |value|
- dataset.add c,k,value
- end
- end
-end
-outfile = File.expand_path(File.join(File.dirname(__FILE__),ARGV[0].sub(/yaml/,'owl')))
-dataset.uri = outfile
-File.open(outfile,'w+'){|f| f.puts dataset.rdf}
diff --git a/lib/algorithm.rb b/lib/algorithm.rb
index d7b57af..af8dfaf 100644
--- a/lib/algorithm.rb
+++ b/lib/algorithm.rb
@@ -1,82 +1,265 @@
+# R integration
+# workaround to initialize R non-interactively (former rinruby versions did this by default)
+# avoids compiling R with X
+R = nil
+require "rinruby"
module OpenTox
+
+ # Wrapper for OpenTox Algorithms
module Algorithm
+
+ include 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, waiting_task=nil)
+ RestClientWrapper.post(@uri, params, {:accept => 'text/uri-list'}, waiting_task).to_s
+ end
-
- class Generic
-
- attr_accessor :uri, :title, :date
-
- def self.find(uri)
- owl = OpenTox::Owl.from_uri(uri, "Algorithm")
- return self.new(owl)
- end
+ # Get OWL-DL representation in RDF/XML format
+ # @return [application/rdf+xml] RDF/XML representation
+ def to_rdfxml
+ s = Serializer::Owl.new
+ s.add_algorithm(@uri,@metadata)
+ s.to_rdfxml
+ end
+
+ # Generic Algorithm class, should work with all OpenTox webservices
+ class Generic
+ include Algorithm
- protected
- def initialize(owl)
- @title = owl.get("title")
- @date = owl.get("date")
- @uri = owl.uri
+ # Find Generic Opentox Algorithm via URI, and loads metadata, could raise NotFound/NotAuthorized error
+ # @param [String] uri Algorithm URI
+ # @return [OpenTox::Algorithm::Generic] Algorithm instance
+ def self.find(uri, subjectid=nil)
+ return nil unless uri
+ alg = Generic.new(uri)
+ alg.load_metadata( subjectid )
+ raise "cannot load algorithm metadata" if alg.metadata==nil or alg.metadata.size==0
+ alg
end
end
- class Fminer
+ # Fminer algorithms (https://github.com/amaunz/fminer2)
+ module Fminer
+ include Algorithm
+
+ # Backbone Refinement Class mining (http://bbrc.maunz.de/)
+ class BBRC
+ include Fminer
+ # Initialize bbrc algorithm
+ def initialize
+ super File.join(CONFIG[:services]["opentox-algorithm"], "fminer/bbrc")
+ load_metadata
+ end
+ end
- def self.create_feature_dataset(params)
- LOGGER.debug File.basename(__FILE__) + ": creating feature dataset"
- resource = RestClient::Resource.new(params[:feature_generation_uri], :user => @@users[:users].keys[0], :password => @@users[:users].values[0])
- resource.post :dataset_uri => params[:dataset_uri], :feature_uri => params[:feature_uri]
+ # LAtent STructure Pattern Mining (http://last-pm.maunz.de)
+ class LAST
+ include Fminer
+ # Initialize last algorithm
+ def initialize
+ super File.join(CONFIG[:services]["opentox-algorithm"], "fminer/last")
+ load_metadata
+ end
end
- def self.uri
- File.join(@@config[:services]["opentox-algorithm"], "fminer")
- end
end
- class Lazar
-
- def self.create_model(params)
- LOGGER.debug params
- LOGGER.debug File.basename(__FILE__) + ": creating model"
- LOGGER.debug File.join(@@config[:services]["opentox-algorithm"], "lazar")
- resource = RestClient::Resource.new(File.join(@@config[:services]["opentox-algorithm"], "lazar"), :user => @@users[:users].keys[0], :password => @@users[:users].values[0], :content_type => "application/x-yaml")
- @uri = resource.post(:dataset_uri => params[:dataset_uri], :prediction_feature => params[:prediction_feature], :feature_generation_uri => File.join(@@config[:services]["opentox-algorithm"], "fminer")).body.chomp
- end
+ # Create lazar prediction model
+ class Lazar
+ include Algorithm
+ # Initialize lazar algorithm
+ def initialize
+ super File.join(CONFIG[:services]["opentox-algorithm"], "lazar")
+ load_metadata
+ end
+ end
- def self.uri
- File.join(@@config[:services]["opentox-algorithm"], "lazar")
- end
+ # Utility methods without dedicated webservices
- end
+ # Similarity calculations
+ module Similarity
+ include Algorithm
- class Similarity
- def self.weighted_tanimoto(fp_a,fp_b,p)
- common_features = fp_a & fp_b
- all_features = (fp_a + fp_b).uniq
+ # Tanimoto similarity
+ # @param [Array] features_a Features of first compound
+ # @param [Array] features_b Features of second compound
+ # @param [optional, Hash] weights Weights for all features
+ # @return [Float] (Weighted) tanimoto similarity
+ def self.tanimoto(features_a,features_b,weights=nil)
+ common_features = features_a & features_b
+ all_features = (features_a + features_b).uniq
common_p_sum = 0.0
if common_features.size > 0
- common_features.each{|f| common_p_sum += OpenTox::Utils.gauss(p[f])}
- all_p_sum = 0.0
- all_features.each{|f| all_p_sum += OpenTox::Utils.gauss(p[f])}
- common_p_sum/all_p_sum
+ if weights
+ common_features.each{|f| common_p_sum += Algorithm.gauss(weights[f])}
+ all_p_sum = 0.0
+ all_features.each{|f| all_p_sum += Algorithm.gauss(weights[f])}
+ common_p_sum/all_p_sum
+ else
+ common_features.to_f/all_features
+ end
else
0.0
end
end
- def self.euclidean(prop_a,prop_b)
- common_properties = prop_a.keys & prop_b.keys
+
+ # Euclidean similarity
+ # @param [Hash] properties_a Properties of first compound
+ # @param [Hash] properties_b Properties of second compound
+ # @param [optional, Hash] weights Weights for all properties
+ # @return [Float] (Weighted) euclidean similarity
+ def self.euclidean(properties_a,properties_b,weights=nil)
+ common_properties = properties_a.keys & properties_b.keys
if common_properties.size > 1
dist_sum = 0
common_properties.each do |p|
- dist_sum += (prop_a[p] - prop_b[p])**2
+ if weights
+ dist_sum += ( (properties_a[p] - properties_b[p]) * Algorithm.gauss(weights[p]) )**2
+ else
+ dist_sum += (properties_a[p] - properties_b[p])**2
+ end
end
1/(1+Math.sqrt(dist_sum))
else
- nil
+ 0.0
+ end
+ end
+ end
+
+ module Neighbors
+
+ # Classification with majority vote from neighbors weighted by similarity
+ # @param [Array] neighbors, each neighbor is a hash with keys `:similarity, :activity`
+ # @param [optional] params Ignored (only for compatibility with local_svm_regression)
+ # @return [Hash] Hash with keys `:prediction, :confidence`
+ def self.weighted_majority_vote(neighbors,params={})
+ conf = 0.0
+ confidence = 0.0
+ neighbors.each do |neighbor|
+ case neighbor[:activity].to_s
+ when 'true'
+ conf += Algorithm.gauss(neighbor[:similarity])
+ when 'false'
+ conf -= Algorithm.gauss(neighbor[:similarity])
+ end
+ end
+ if conf > 0.0
+ prediction = true
+ elsif conf < 0.0
+ prediction = false
+ else
+ prediction = nil
+ end
+ confidence = conf/neighbors.size if neighbors.size > 0
+ {:prediction => prediction, :confidence => confidence.abs}
+ end
+
+ # Local support vector regression from neighbors
+ # @param [Array] neighbors, each neighbor is a hash with keys `:similarity, :activity, :features`
+ # @param [Hash] params Keys `:similarity_algorithm,:p_values` are required
+ # @return [Hash] Hash with keys `:prediction, :confidence`
+ def self.local_svm_regression(neighbors,params )
+ sims = neighbors.collect{ |n| n[:similarity] } # similarity values between query and neighbors
+ conf = sims.inject{|sum,x| sum + x }
+ acts = neighbors.collect do |n|
+ act = n[:activity]
+ Math.log10(act.to_f)
+ end # activities of neighbors for supervised learning
+
+ neighbor_matches = neighbors.collect{ |n| n[:features] } # as in classification: URIs of matches
+ gram_matrix = [] # square matrix of similarities between neighbors; implements weighted tanimoto kernel
+ if neighbor_matches.size == 0
+ raise "No neighbors found"
+ else
+ # gram matrix
+ (0..(neighbor_matches.length-1)).each do |i|
+ gram_matrix[i] = [] unless gram_matrix[i]
+ # upper triangle
+ ((i+1)..(neighbor_matches.length-1)).each do |j|
+ sim = eval("#{params[:similarity_algorithm]}(neighbor_matches[i], neighbor_matches[j], params[:p_values])")
+ gram_matrix[i][j] = Algorithm.gauss(sim)
+ gram_matrix[j] = [] unless gram_matrix[j]
+ gram_matrix[j][i] = gram_matrix[i][j] # lower triangle
+ end
+ gram_matrix[i][i] = 1.0
+ end
+
+ LOGGER.debug gram_matrix.to_yaml
+
+ @r = RinRuby.new(false,false) # global R instance leads to Socket errors after a large number of requests
+ @r.eval "library('kernlab')" # this requires R package "kernlab" to be installed
+ LOGGER.debug "Setting R data ..."
+ # set data
+ @r.gram_matrix = gram_matrix.flatten
+ @r.n = neighbor_matches.size
+ @r.y = acts
+ @r.sims = sims
+
+ LOGGER.debug "Preparing R data ..."
+ # prepare data
+ @r.eval "y<-as.vector(y)"
+ @r.eval "gram_matrix<-as.kernelMatrix(matrix(gram_matrix,n,n))"
+ @r.eval "sims<-as.vector(sims)"
+
+ # model + support vectors
+ LOGGER.debug "Creating SVM model ..."
+ @r.eval "model<-ksvm(gram_matrix, y, kernel=matrix, type=\"nu-svr\", nu=0.8)"
+ @r.eval "sv<-as.vector(SVindex(model))"
+ @r.eval "sims<-sims[sv]"
+ @r.eval "sims<-as.kernelMatrix(matrix(sims,1))"
+ LOGGER.debug "Predicting ..."
+ @r.eval "p<-predict(model,sims)[1,1]"
+ prediction = 10**(@r.p.to_f)
+ LOGGER.debug "Prediction is: '" + @prediction.to_s + "'."
+ @r.quit # free R
end
+ confidence = conf/neighbors.size if neighbors.size > 0
+ {:prediction => prediction, :confidence => confidence}
+
+ end
+
+ end
+
+ module Substructure
+ include Algorithm
+ # Substructure matching
+ # @param [OpenTox::Compound] compound Compound
+ # @param [Array] features Array with Smarts strings
+ # @return [Array] Array with matching Smarts
+ def self.match(compound,features)
+ compound.match(features)
+ end
+ end
+
+ module Dataset
+ include Algorithm
+ # API should match Substructure.match
+ def features(dataset_uri,compound_uri)
end
end
+
+ # Gauss kernel
+ # @return [Float]
+ def self.gauss(x, sigma = 0.3)
+ d = 1.0 - x
+ Math.exp(-(d*d)/(2*sigma*sigma))
+ end
+
+ # Median of an array
+ # @param [Array] Array with values
+ # @return [Float] Median
+ def self.median(array)
+ return nil if array.empty?
+ array.sort!
+ m_pos = array.size / 2
+ return array.size % 2 == 1 ? array[m_pos] : (array[m_pos-1] + array[m_pos])/2
+ end
end
end
diff --git a/lib/authorization.rb b/lib/authorization.rb
index 9a1760a..eab20df 100644
--- a/lib/authorization.rb
+++ b/lib/authorization.rb
@@ -1,24 +1,381 @@
-helpers do
+module OpenTox
+
+ #Module for Authorization and Authentication
+ #@example Authentication
+ # require "opentox-ruby-api-wrapper"
+ # OpenTox::Authorization::AA_SERVER = "https://opensso.in-silico.ch" #if not set in .opentox/conf/[environment].yaml
+ # token = OpenTox::Authorization.authenticate("benutzer", "passwort")
+ #@see http://www.opentox.org/dev/apis/api-1.2/AA OpenTox A&A API 1.2 specification
+
+ module Authorization
+
+ #Helper Class AA to create and send default policies out of xml templates
+ #@example Creating a default policy to a URI
+ # aa=OpenTox::Authorization::AA.new(tok)
+ # xml=aa.get_xml('http://uri....')
+ # OpenTox::Authorization.create_policy(xml,tok)
+
+ class AA
+ attr_accessor :user, :subjectid, :policy
+
+ #Generates AA object - requires subjectid
+ # @param [String] subjectid
+ def initialize(subjectid)
+ @user = Authorization.get_user(subjectid)
+ @subjectid = subjectid
+ @policy = Policies.new()
+ end
+
+ #Cleans AA Policies and loads default xml file into policy attribute
+ #set uri and user, returns Policyfile(XML) for open-sso
+ # @param [String] URI to create a policy for
+ def get_xml(uri)
+ @policy.drop_policies
+ @policy.load_default_policy(@user, uri)
+ return @policy.to_xml
+ end
+
+ #Loads and sends Policyfile(XML) to open-sso server
+ # @param [String] URI to create a policy for
+ def send(uri)
+ xml = get_xml(uri)
+ ret = false
+ ret = Authorization.create_policy(xml, @subjectid)
+ LOGGER.debug "Policy send with subjectid: #{@subjectid}"
+ LOGGER.warn "Not created Policy is: #{xml}" if !ret
+ ret
+ end
+
+ end
+
+ #Returns the open-sso server set in the config file .opentox/config/[environment].yaml
+ # @return [String, nil] the openSSO server URI or nil
+ def self.server
+ return AA_SERVER
+ end
+
+ #Authentication against OpenSSO. Returns token. Requires Username and Password.
+ # @param [String, String]Username,Password
+ # @return [String, nil] gives subjectid or nil
+ def self.authenticate(user, pw)
+ return nil if !AA_SERVER
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/authenticate")
+ out = resource.post(:username=>user, :password => pw).sub("token.id=","").sub("\n","")
+ return out
+ rescue
+ return nil
+ end
+ end
+
+ #Logout on opensso. Make token invalid. Requires token
+ # @param [String]subjectid the subjectid
+ # @return [Boolean] true if logout is OK
+ def self.logout(subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/logout")
+ resource.post(:subjectid => subjectid)
+ return true
+ rescue
+ return false
+ end
+ end
+
+ #Authorization against OpenSSO for a URI with request-method (action) [GET/POST/PUT/DELETE]
+ # @param [String,String,String]uri,action,subjectid
+ # @return [Boolean, nil] returns true, false or nil (if authorization-request fails).
+ def self.authorize(uri, action, subjectid)
+ return true if !AA_SERVER
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/authorize")
+ return true if resource.post(:uri => uri, :action => action, :subjectid => subjectid) == "boolean=true\n"
+ rescue
+ return nil
+ end
+ end
+
+ #Checks if a token is a valid token
+ # @param [String]subjectid subjectid from openSSO session
+ # @return [Boolean] subjectid is valid or not.
+ def self.is_token_valid(subjectid)
+ return true if !AA_SERVER
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/auth/isTokenValid")
+ return true if resource.post(:tokenid => subjectid) == "boolean=true\n"
+ rescue
+ return false
+ end
+ end
- def protected!
- response['WWW-Authenticate'] = %(Basic realm="Opentox Webservice Authentication") and \
- throw(:halt, [401, "Not authorized\n"]) and \
- return unless authorized?
+ #Returns array with all policies of the token owner
+ # @param [String]subjectid requires subjectid
+ # @return [Array, nil] returns an Array of policy names or nil if request fails
+ def self.list_policies(subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ out = resource.get(:subjectid => subjectid)
+ return out.split("\n")
+ rescue RestClient::InternalServerError => e
+ raise e.response
+ rescue
+ return nil
+ end
+ end
+
+ #Returns a policy in xml-format
+ # @param [String, String]policy,subjectid
+ # @return [String] XML of the policy
+ def self.list_policy(policy, subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ return resource.get(:subjectid => subjectid,:id => policy)
+ rescue
+ return nil
+ 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
+ def self.get_uri_owner(uri, subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ return resource.get(:uri => uri, :subjectid => subjectid).sub("\n","")
+ rescue
+ return nil
+ end
+ end
+
+ #Checks if a policy exists to a URI. Requires URI and token.
+ # @param [String, String]uri,subjectid
+ # return [Boolean]
+ def self.uri_has_policy(uri, subjectid)
+ owner = get_uri_owner(uri, subjectid)
+ return true if owner and owner != "null"
+ false
+ end
+
+ #List all policynames for a URI. Requires URI and token.
+ # @param [String, String]uri,subjectid
+ # return [Array, nil] returns an Array of policy names or nil if request fails
+ def self.list_uri_policies(uri, subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ out = resource.get(:uri => uri, :polnames => true, :subjectid => subjectid)
+ policies = []; notfirstline = false
+ out.split("\n").each do |line|
+ policies << line if notfirstline
+ notfirstline = true
+ end
+ return policies
+ rescue
+ return nil
+ end
+ end
+
+ #Sends a policy in xml-format to opensso server. Requires policy-xml and token.
+ # @param [String, String]policyxml,subjectid
+ # return [Boolean] returns true if policy is created
+ def self.create_policy(policy, subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/Pol/opensso-pol")
+ LOGGER.debug "OpenTox::Authorization.create_policy policy: #{policy[168,43]} with token:" + subjectid.to_s + " length: " + subjectid.length.to_s
+ return true if resource.post(policy, :subjectid => subjectid, :content_type => "application/xml")
+ rescue
+ return false
+ end
+ end
+
+ #Deletes a policy
+ # @param [String, String]policyname,subjectid
+ # @return [Boolean,nil]
+ def self.delete_policy(policy, subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/pol")
+ LOGGER.debug "OpenTox::Authorization.delete_policy policy: #{policy} with token: #{subjectid}"
+ return true if resource.delete(:subjectid => subjectid, :id => policy)
+ rescue
+ return nil
+ end
+ end
+
+ #Returns array of all possible LDAP-Groups
+ # @param [String]subjectid
+ # @return [Array]
+ def self.list_groups(subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/opensso/identity/search")
+ grps = resource.post(:admin => subjectid, :attributes_names => "objecttype", :attributes_values_objecttype => "group")
+ grps.split("\n").collect{|x| x.sub("string=","")}
+ rescue
+ []
+ end
+ end
+
+ #Returns array of the LDAP-Groups of an user
+ # @param [String]subjectid
+ # @return [Array] gives array of LDAP groups of a user
+ def self.list_user_groups(user, subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/opensso/identity/read")
+ out = resource.post(:name => user, :admin => subjectid, :attributes_names => "group")
+ grps = []
+ out.split("\n").each do |line|
+ grps << line.sub("identitydetails.group=","") if line.include?("identitydetails.group=")
+ end
+ return grps
+ rescue
+ []
+ end
+ end
+
+ #Returns the owner (user id) of a token
+ # @param [String]subjectid
+ # @return [String]user
+ def self.get_user(subjectid)
+ begin
+ resource = RestClient::Resource.new("#{AA_SERVER}/opensso/identity/attributes")
+ out = resource.post(:subjectid => subjectid, :attributes_names => "uid")
+ user = ""; check = false
+ out.split("\n").each do |line|
+ if check
+ user = line.sub("userdetails.attribute.value=","") if line.include?("userdetails.attribute.value=")
+ check = false
+ end
+ check = true if line.include?("userdetails.attribute.name=uid")
+ end
+ return user
+ rescue
+ nil
+ end
+ end
+
+ #Send default policy with Authorization::AA class
+ # @param [String, String]URI,subjectid
+ def self.send_policy(uri, subjectid)
+ return true if !AA_SERVER
+ aa = Authorization::AA.new(subjectid)
+ ret = aa.send(uri)
+ LOGGER.debug "OpenTox::Authorization send policy for URI: #{uri} | subjectid: #{subjectid} - policy created: #{ret}"
+ ret
+ end
+
+ #Deletes all policies of an URI
+ # @param [String, String]URI,subjectid
+ # @return [Boolean]
+ def self.delete_policies_from_uri(uri, subjectid)
+ policies = list_uri_policies(uri, subjectid)
+ policies.each do |policy|
+ ret = delete_policy(policy, subjectid)
+ LOGGER.debug "OpenTox::Authorization delete policy: #{policy} - with result: #{ret}"
+ end
+ return true
+ end
+
+ # 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}"
+ # 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
+ # 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
+ end
+
+ class << self
+ alias :token_valid? :is_token_valid
+ end
+
+ # Check Authorization for a resource (identified via URI) with method and subjectid.
+ # @param [String] uri
+ # @param [String] request_method, should be GET, POST, PUT, DELETE
+ # @param [String] subjectid
+ # @return [Boolean] true if access granted, else otherwise
+ def self.authorized?(uri, request_method, subjectid)
+ if CONFIG[:authorization][:free_request].include?(request_method)
+ #LOGGER.debug "authorized? >>true<< (request is free), method: #{request_method}, URI: #{uri}, subjectid: #{subjectid}"
+ true
+ elsif OpenTox::Authorization.free_uri?(uri, request_method)
+ #LOGGER.debug "authorized? >>true<< (uris is free_uri), method: #{request_method}, URI: #{uri}, subjectid: #{subjectid}"
+ true
+ elsif CONFIG[:authorization][:authenticate_request].include?(request_method)
+ ret = OpenTox::Authorization.is_token_valid(subjectid)
+ LOGGER.debug "authorized? >>#{ret}<< (token is in/valid), method: #{request_method}, URI: #{uri}, subjectid: #{subjectid}" unless ret
+ ret
+ elsif OpenTox::Authorization.authorize_exception?(uri, request_method)
+ ret = OpenTox::Authorization.is_token_valid(subjectid)
+ LOGGER.debug "authorized? >>#{ret}<< (uris is authorize exception, token is in/valid), method: #{request_method}, URI: #{uri}, subjectid: #{subjectid}" unless ret
+ ret
+ elsif CONFIG[:authorization][:authorize_request].include?(request_method)
+ ret = OpenTox::Authorization.authorize(uri, request_method, subjectid)
+ LOGGER.debug "authorized? >>#{ret}<< (uri (not) authorized), method: #{request_method}, URI: #{uri}, subjectid: #{subjectid}" unless ret
+ ret
+ else
+ LOGGER.error "invalid request/uri method: #{request_method}, URI: #{uri}, subjectid: #{subjectid}"
+ false
+ end
+ end
+
+ private
+ def self.free_uri?(uri, request_method)
+ if CONFIG[:authorization][:free_uris]
+ CONFIG[:authorization][:free_uris].each do |request_methods,uris|
+ if request_methods and uris and request_methods.include?(request_method.to_sym)
+ uris.each do |u|
+ return true if u.match uri
+ end
+ end
+ end
+ end
+ return false
+ end
+
+ def self.authorize_exception?(uri, request_method)
+ if CONFIG[:authorization][:authorize_exceptions]
+ CONFIG[:authorization][:authorize_exceptions].each do |request_methods,uris|
+ if request_methods and uris and request_methods.include?(request_method.to_sym)
+ uris.each do |u|
+ return true if u.match uri
+ end
+ end
+ end
+ end
+ return false
+ end
+
end
-
- def authorized?
- @auth ||= Rack::Auth::Basic::Request.new(request.env)
- @auth.provided? && @auth.basic? && @auth.credentials && valid_user?
- end
-
- def valid_user?
- users = @@users[:users]
- return @auth.credentials == [@auth.username, users.fetch(@auth.username)] if users.has_key?(@auth.username)
- return false
- end
-
-end
-
-before do
- #protected! unless env['REQUEST_METHOD'] == "GET"
-end
+end \ No newline at end of file
diff --git a/lib/compound.rb b/lib/compound.rb
index 49c166f..a85507b 100644
--- a/lib/compound.rb
+++ b/lib/compound.rb
@@ -3,82 +3,126 @@
module OpenTox
- class Compound #< OpenTox
-
- attr_reader :inchi, :uri
-
- # Initialize with <tt>:uri => uri</tt>, <tt>:smiles => smiles</tt> or <tt>:name => name</tt> (name can be also an InChI/InChiKey, CAS number, etc)
- def initialize(params)
- if params[:smiles]
- @inchi = smiles2inchi(params[:smiles])
- @uri = File.join(@@config[:services]["opentox-compound"],URI.escape(@inchi))
- elsif params[:inchi]
- @inchi = params[:inchi]
- @uri = File.join(@@config[:services]["opentox-compound"],URI.escape(@inchi))
- elsif params[:sdf]
- @inchi = sdf2inchi(params[:sdf])
- @uri = File.join(@@config[:services]["opentox-compound"],URI.escape(@inchi))
- elsif params[:name]
- # paranoid URI encoding to keep SMILES charges and brackets
- @inchi = RestClient.get("#{@@cactus_uri}#{URI.encode(params[:name], Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}/stdinchi").body.chomp
- # this was too hard for me to debug and leads to additional errors (ch)
- #@inchi = RestClientWrapper.get("#{@@cactus_uri}#{URI.encode(params[:name], Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}/stdinchi").chomp
- @uri = File.join(@@config[:services]["opentox-compound"],URI.escape(@inchi))
- elsif params[:uri]
- @uri = params[:uri]
- case params[:uri]
- when /ambit/ # Ambit does not deliver InChIs reliably
- smiles = RestClientWrapper.get @uri, :accept => 'chemical/x-daylight-smiles'
- @inchi = obconversion(smiles,'smi','inchi')
- when /InChI/ # shortcut for IST services
- @inchi = params[:uri].sub(/^.*InChI/, 'InChI')
- else
- @inchi = RestClientWrapper.get @uri, :accept => 'chemical/x-inchi'
- end
- end
+ # Ruby wrapper for OpenTox Compound Webservices (http://opentox.org/dev/apis/api-1.2/structure).
+ class Compound
+
+ attr_accessor :inchi, :uri
+
+ # Create compound with optional uri
+ # @example
+ # compound = OpenTox::Compound.new("http://webservices.in-silico.ch/compound/InChI=1S/C6H6/c1-2-4-6-5-3-1/h1-6H"")
+ # @param [optional, String] uri Compound URI
+ # @return [OpenTox::Compound] Compound
+ def initialize(uri=nil)
+ @uri = uri
+ case @uri
+ when /InChI/ # shortcut for IST services
+ @inchi = @uri.sub(/^.*InChI/, 'InChI')
+ else
+ @inchi = RestClientWrapper.get(@uri, :accept => 'chemical/x-inchi').to_s.chomp if @uri
+ end
+ end
+
+ # Create a compound from smiles string
+ # @example
+ # compound = OpenTox::Compound.from_smiles("c1ccccc1")
+ # @param [String] smiles Smiles string
+ # @return [OpenTox::Compound] Compound
+ def self.from_smiles(smiles)
+ c = Compound.new
+ c.inchi = Compound.smiles2inchi(smiles)
+ c.uri = File.join(CONFIG[:services]["opentox-compound"],URI.escape(c.inchi))
+ c
+ end
+
+ # Create a compound from inchi string
+ # @param [String] smiles InChI string
+ # @return [OpenTox::Compound] Compound
+ def self.from_inchi(inchi)
+ c = Compound.new
+ c.inchi = inchi
+ c.uri = File.join(CONFIG[:services]["opentox-compound"],URI.escape(c.inchi))
+ c
+ end
+
+ # Create a compound from sdf string
+ # @param [String] smiles SDF string
+ # @return [OpenTox::Compound] Compound
+ def self.from_sdf(sdf)
+ c = Compound.new
+ c.inchi = Compound.sdf2inchi(sdf)
+ c.uri = File.join(CONFIG[:services]["opentox-compound"],URI.escape(c.inchi))
+ c
+ end
+
+ # Create a compound from name. Relies on an external service for name lookups.
+ # @example
+ # compound = OpenTox::Compound.from_name("Benzene")
+ # @param [String] name name can be also an InChI/InChiKey, CAS number, etc
+ # @return [OpenTox::Compound] Compound
+ def self.from_name(name)
+ c = Compound.new
+ # paranoid URI encoding to keep SMILES charges and brackets
+ c.inchi = RestClientWrapper.get("#{@@cactus_uri}#{URI.encode(name, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}/stdinchi").to_s.chomp
+ c.uri = File.join(CONFIG[:services]["opentox-compound"],URI.escape(c.inchi))
+ c
+ end
+
+ # Get InChI
+ # @return [String] InChI string
+ def to_inchi
+ @inchi
end
- # Get the (canonical) smiles
- def smiles
- obconversion(@inchi,'inchi','can')
+ # Get (canonical) smiles
+ # @return [String] Smiles string
+ def to_smiles
+ Compound.obconversion(@inchi,'inchi','can')
end
- def sdf
- obconversion(@inchi,'inchi','sdf')
+ # Get sdf
+ # @return [String] SDF string
+ def to_sdf
+ Compound.obconversion(@inchi,'inchi','sdf')
end
- def gif
+ # Get gif image
+ # @return [image/gif] Image data
+ def to_gif
RestClientWrapper.get("#{@@cactus_uri}#{@inchi}/image")
end
- def png
+ # Get png image
+ # @example
+ # image = compound.to_png
+ # @return [image/png] Image data
+ def to_png
RestClientWrapper.get(File.join @uri, "image")
end
- def names
+ # Get URI of compound image
+ # @return [String] Compound image URI
+ def to_image_uri
+ File.join @uri, "image"
+ end
+
+ # Get all known compound names. Relies on an external service for name lookups.
+ # @example
+ # names = compound.to_names
+ # @return [String] Compound names
+ def to_names
begin
- RestClientWrapper.get("#{@@cactus_uri}#{@inchi}/names")
+ RestClientWrapper.get("#{@@cactus_uri}#{@inchi}/names").split("\n")
rescue
"not available"
end
end
- def display_smarts_uri(activating, deactivating, highlight = nil)
- LOGGER.debug activating.to_yaml unless activating.nil?
- activating_smarts = URI.encode "\"#{activating.join("\"/\"")}\""
- deactivating_smarts = URI.encode "\"#{deactivating.join("\"/\"")}\""
- if highlight.nil?
- File.join @@config[:services]["opentox-compound"], "smiles", URI.encode(smiles), "smarts/activating", URI.encode(activating_smarts),"deactivating", URI.encode(deactivating_smarts)
- else
- File.join @@config[:services]["opentox-compound"], "smiles", URI.encode(smiles), "smarts/activating", URI.encode(activating_smarts),"deactivating", URI.encode(deactivating_smarts), "highlight", URI.encode(highlight)
- end
- end
-
- def image_uri
- File.join @uri, "image"
- end
-
- # Matchs a smarts string
+ # Match a smarts string
+ # @example
+ # compound = OpenTox::Compound.from_name("Benzene")
+ # compound.match?("cN") # returns false
+ # @param [String] smarts Smarts string
def match?(smarts)
obconversion = OpenBabel::OBConversion.new
obmol = OpenBabel::OBMol.new
@@ -89,30 +133,57 @@ module OpenTox
smarts_pattern.match(obmol)
end
- # Match an array of smarts features, returns matching features
+ # Match an array of smarts strings, returns array with matching smarts
+ # @example
+ # compound = OpenTox::Compound.from_name("Benzene")
+ # compound.match(['cc','cN']) # returns ['cc']
+ # @param [Array] smarts_array Array with Smarts strings
+ # @return [Array] Array with matching Smarts strings
def match(smarts_array)
- smarts_array.collect{|s| s if match?(s)}.compact
+ # avoid recreation of OpenBabel objects
+ obconversion = OpenBabel::OBConversion.new
+ obmol = OpenBabel::OBMol.new
+ obconversion.set_in_format('inchi')
+ obconversion.read_string(obmol,@inchi)
+ smarts_pattern = OpenBabel::OBSmartsPattern.new
+ smarts_array.collect do |smarts|
+ smarts_pattern.init(smarts)
+ smarts if smarts_pattern.match(obmol)
+ end.compact
+ #smarts_array.collect { |s| s if match?(s)}.compact
end
- # AM
- # Match an array of smarts features, returns (0)1 for (non)matching features at each pos
- def match_all(smarts_array)
- smarts_array.collect{|s| match?(s) ? 1 : 0 }
- end
+ # Get URI of compound image with highlighted fragments
+ #
+ # @param [Array] activating Array with activating Smarts strings
+ # @param [Array] deactivating Array with deactivating Smarts strings
+ # @return [String] URI for compound image with highlighted fragments
+ def matching_smarts_image_uri(activating, deactivating)
+ activating_smarts = URI.encode "\"#{activating.join("\"/\"")}\""
+ deactivating_smarts = URI.encode "\"#{deactivating.join("\"/\"")}\""
+ File.join @uri, "smarts/activating", URI.encode(activating_smarts),"deactivating", URI.encode(deactivating_smarts)
+ end
+
+
+ private
- def sdf2inchi(sdf)
- obconversion(sdf,'sdf','inchi')
+ # Convert sdf to inchi
+ def self.sdf2inchi(sdf)
+ Compound.obconversion(sdf,'sdf','inchi')
end
- def smiles2inchi(smiles)
- obconversion(smiles,'smi','inchi')
+ # Convert smiles to inchi
+ def self.smiles2inchi(smiles)
+ Compound.obconversion(smiles,'smi','inchi')
end
- def smiles2cansmi(smiles)
- obconversion(smiles,'smi','can')
+ # Convert smiles to canonical smiles
+ def self.smiles2cansmi(smiles)
+ Compound.obconversion(smiles,'smi','can')
end
- def obconversion(identifier,input_format,output_format)
+ # Convert identifier from OpenBabel input_format to OpenBabel output_format
+ def self.obconversion(identifier,input_format,output_format)
obconversion = OpenBabel::OBConversion.new
obmol = OpenBabel::OBMol.new
obconversion.set_in_and_out_formats input_format, output_format
diff --git a/lib/config/config_ru.rb b/lib/config/config_ru.rb
index 3d8dce2..93df867 100644
--- a/lib/config/config_ru.rb
+++ b/lib/config/config_ru.rb
@@ -12,6 +12,7 @@ $stdout.sync = true
$stderr.sync = true
set :logging, false
set :raise_errors, true
+set :lock, true
['public','tmp'].each do |dir|
FileUtils.mkdir_p dir unless File.exists?(dir)
diff --git a/lib/dataset.rb b/lib/dataset.rb
index 526e0a2..2c47502 100644
--- a/lib/dataset.rb
+++ b/lib/dataset.rb
@@ -1,229 +1,385 @@
module OpenTox
+ # Ruby wrapper for OpenTox Dataset Webservices (http://opentox.org/dev/apis/api-1.2/dataset).
class Dataset
- attr_accessor :uri, :title, :creator, :data, :features, :compounds
+ include OpenTox
- def initialize( owl=nil )
- @data = {}
- @features = []
+ attr_reader :features, :compounds, :data_entries, :metadata
+
+ # Create dataset with optional URI. Does not load data into the dataset - you will need to execute one of the load_* methods to pull data from a service or to insert it from other representations.
+ # @example Create an empty dataset
+ # dataset = OpenTox::Dataset.new
+ # @example Create an empty dataset with URI
+ # dataset = OpenTox::Dataset.new("http:://webservices.in-silico/ch/dataset/1")
+ # @param [optional, String] uri Dataset URI
+ # @return [OpenTox::Dataset] Dataset object
+ def initialize(uri=nil,subjectid=nil)
+ super uri
+ @features = {}
@compounds = []
-
- # creates dataset object from Opentox::Owl object
- # use Dataset.find( <uri> ) to load dataset from rdf-supporting datasetservice
- # note: does not load all feature values, as this is time consuming
- if owl
- raise "invalid param" unless owl.is_a?(OpenTox::Owl)
- @title = owl.get("title")
- @creator = owl.get("creator")
- @uri = owl.uri
- # when loading a dataset from owl, only compound- and feature-uris are loaded
- owl.load_dataset(@compounds, @features)
- # all features are marked as dirty
- # as soon as a feature-value is requested all values for this feature are loaded from the rdf
- @dirty_features = @features.dclone
- @owl = owl
- end
+ @data_entries = {}
end
-
- # Find and a dataset and load all data from the dataset service
- # @param [String] uri Dataset URI
+
+ # Create an empty dataset and save it at the dataset service (assigns URI to dataset)
+ # @example Create new dataset and save it to obtain a URI
+ # dataset = OpenTox::Dataset.create
+ # @param [optional, String] uri Dataset URI
# @return [OpenTox::Dataset] Dataset object
- def self.find(uri, accept_header=nil)
-
- unless accept_header
- if (@@config[:yaml_hosts].include?(URI.parse(uri).host))
- accept_header = 'application/x-yaml'
- else
- accept_header = "application/rdf+xml"
- end
- end
-
- case accept_header
- when "application/x-yaml"
- d = YAML.load RestClientWrapper.get(uri.to_s.strip, :accept => 'application/x-yaml').to_s
- d.uri = uri unless d.uri
- when "application/rdf+xml"
- owl = OpenTox::Owl.from_uri(uri.to_s.strip, "Dataset")
- d = Dataset.new(owl)
- else
- raise "cannot get datset with accept header: "+accept_header.to_s
- end
- d
+ def self.create(uri=CONFIG[:services]["opentox-dataset"], subjectid=nil)
+ dataset = Dataset.new(nil,subjectid)
+ dataset.save(subjectid)
+ dataset
+ end
+
+ # Create dataset from CSV file (format specification: http://toxcreate.org/help)
+ # - loads data_entries, compounds, features
+ # - sets metadata (warnings) for parser errors
+ # - you will have to set remaining metadata manually
+ # @param [String] file CSV file path
+ # @return [OpenTox::Dataset] Dataset object with CSV data
+ def self.create_from_csv_file(file, subjectid=nil)
+ dataset = Dataset.create(CONFIG[:services]["opentox-dataset"], subjectid)
+ parser = Parser::Spreadsheets.new
+ parser.dataset = dataset
+ parser.load_csv(File.open(file).read)
+ dataset.save(subjectid)
+ dataset
end
- # converts a dataset represented in owl to yaml
- # (uses a temporary dataset)
- # note: to_yaml is overwritten, loads complete owl dataset values
- def self.owl_to_yaml( owl_data, uri)
- owl = OpenTox::Owl.from_data(owl_data, uri, "Dataset")
- d = Dataset.new(owl)
- d.to_yaml
+ # 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
+ def self.find(uri, subjectid=nil)
+ return nil unless uri
+ dataset = Dataset.new(uri, subjectid)
+ dataset.load_all(subjectid)
+ dataset
end
- # creates a new dataset, using only those compounsd specified in new_compounds
- # returns uri of new dataset
- def create_new_dataset( new_compounds, new_features, new_title, new_creator )
-
- LOGGER.debug "create new dataset with "+new_compounds.size.to_s+"/"+compounds.size.to_s+" compounds"
- raise "no new compounds selected" unless new_compounds and new_compounds.size>0
-
- # load require features
- if ((defined? @dirty_features) && (@dirty_features & new_features).size > 0)
- (@dirty_features & new_features).each{|f| load_feature_values(f)}
- end
-
- dataset = OpenTox::Dataset.new
- dataset.title = new_title
- dataset.creator = new_creator
- dataset.features = new_features
- dataset.compounds = new_compounds
-
- # Copy dataset data for compounds and features
- # PENDING: why storing feature values in an array?
- new_compounds.each do |c|
- data_c = []
- raise "no data for compound '"+c.to_s+"'" if @data[c]==nil
- @data[c].each do |d|
- m = {}
- new_features.each do |f|
- m[f] = d[f]
- end
- data_c << m
- end
- dataset.data[c] = data_c
+ # replaces find as exist check, takes not as long, does NOT raise an un-authorized exception
+ # @param [String] uri Dataset URI
+ # @return [Boolean] true if dataset exists and user has get rights, false else
+ def self.exist?(uri, subjectid=nil)
+ return false unless uri
+ dataset = Dataset.new(uri, subjectid)
+ begin
+ dataset.load_metadata( subjectid ).size > 0
+ rescue
+ false
end
- return dataset.save
+ end
+
+ # Get all datasets from a service
+ # @param [optional,String] uri URI of the dataset service, defaults to service specified in configuration
+ # @return [Array] Array of dataset object without data (use one of the load_* methods to pull data from the server)
+ def self.all(uri=CONFIG[:services]["opentox-dataset"], subjectid=nil)
+ RestClientWrapper.get(uri,{:accept => "text/uri-list",:subjectid => subjectid}).to_s.each_line.collect{|u| Dataset.new(u, subjectid)}
+ end
+
+ # Load YAML representation into the dataset
+ # @param [String] yaml YAML representation of the dataset
+ # @return [OpenTox::Dataset] Dataset object with YAML data
+ def load_yaml(yaml)
+ copy YAML.load(yaml)
+ end
+
+ def load_rdfxml(rdfxml)
+ raise "rdfxml data is empty" if rdfxml.to_s.size==0
+ file = Tempfile.new("ot-rdfxml")
+ file.puts rdfxml
+ file.close
+ load_rdfxml_file file
+ file.delete
+ end
+
+ # Load RDF/XML representation from a file
+ # @param [String] file File with RDF/XML representation of the dataset
+ # @return [OpenTox::Dataset] Dataset object with RDF/XML data
+ def load_rdfxml_file(file, subjectid=nil)
+ parser = Parser::Owl::Dataset.new @uri, subjectid
+ parser.uri = file.path
+ copy parser.load_uri(subjectid)
+ end
+
+ # Load CSV string (format specification: http://toxcreate.org/help)
+ # - loads data_entries, compounds, features
+ # - sets metadata (warnings) for parser errors
+ # - you will have to set remaining metadata manually
+ # @param [String] csv CSV representation of the dataset
+ # @return [OpenTox::Dataset] Dataset object with CSV data
+ def load_csv(csv, subjectid=nil)
+ save(subjectid) unless @uri # get a uri for creating features
+ parser = Parser::Spreadsheets.new
+ parser.dataset = self
+ parser.load_csv(csv)
+ end
+
+ # Load Spreadsheet book (created with roo gem http://roo.rubyforge.org/, excel format specification: http://toxcreate.org/help)
+ # - loads data_entries, compounds, features
+ # - sets metadata (warnings) for parser errors
+ # - you will have to set remaining metadata manually
+ # @param [Excel] book Excel workbook object (created with roo gem)
+ # @return [OpenTox::Dataset] Dataset object with Excel data
+ def load_spreadsheet(book, subjectid=nil)
+ save(subjectid) unless @uri # get a uri for creating features
+ parser = Parser::Spreadsheets.new
+ parser.dataset = self
+ parser.load_spreadsheet(book)
end
- # returns classification value
- def get_predicted_class(compound, feature)
- v = get_value(compound, feature)
- if v.is_a?(Hash)
- k = v.keys.grep(/classification/).first
- unless k.empty?
- #if v.has_key?(:classification)
- return v[k]
- else
- return "no classification key"
- end
- elsif v.is_a?(Array)
- raise "predicted class value is an array\n"+
- "value "+v.to_s+"\n"+
- "value-class "+v.class.to_s+"\n"+
- "dataset "+@uri.to_s+"\n"+
- "compound "+compound.to_s+"\n"+
- "feature "+feature.to_s+"\n"
+ # Load and return only metadata of a Dataset object
+ # @return [Hash] Metadata of the dataset
+ def load_metadata(subjectid=nil)
+ add_metadata Parser::Owl::Dataset.new(@uri, subjectid).load_metadata(subjectid)
+ self.uri = @uri if @uri # keep uri
+ @metadata
+ end
+
+ # Load all data (metadata, data_entries, compounds and features) from URI
+ def load_all(subjectid=nil)
+ if (CONFIG[:yaml_hosts].include?(URI.parse(@uri).host))
+ copy YAML.load(RestClientWrapper.get(@uri, {:accept => "application/x-yaml", :subjectid => subjectid}))
else
- return v
+ parser = Parser::Owl::Dataset.new(@uri, subjectid)
+ copy parser.load_uri(subjectid)
end
end
-
- # returns regression value
- def get_predicted_regression(compound, feature)
- v = get_value(compound, feature)
- if v.is_a?(Hash)
- k = v.keys.grep(/regression/).first
- unless k.empty?
- return v[k]
- else
- return "no regression key"
- end
- elsif v.is_a?(Array)
- raise "predicted regression value is an array\n"+
- "value "+v.to_s+"\n"+
- "value-class "+v.class.to_s+"\n"+
- "dataset "+@uri.to_s+"\n"+
- "compound "+compound.to_s+"\n"+
- "feature "+feature.to_s+"\n"
- else
- return v
+
+ # Load and return only compound URIs from the dataset service
+ # @return [Array] Compound URIs in the dataset
+ def load_compounds(subjectid=nil)
+ RestClientWrapper.get(File.join(uri,"compounds"),{:accept=> "text/uri-list", :subjectid => subjectid}).to_s.each_line do |compound_uri|
+ @compounds << compound_uri.chomp
end
+ @compounds.uniq!
end
-
- # returns prediction confidence if available
- def get_prediction_confidence(compound, feature)
- v = get_value(compound, feature)
- if v.is_a?(Hash)
- k = v.keys.grep(/confidence/).first
- unless k.empty?
- #if v.has_key?(:confidence)
- return v[k].abs
- #return v["http://ot-dev.in-silico.ch/model/lazar#confidence"].abs
+
+ # Load and return only features from the dataset service
+ # @return [Hash] Features of the dataset
+ def load_features(subjectid=nil)
+ parser = Parser::Owl::Dataset.new(@uri, subjectid)
+ @features = parser.load_features(subjectid)
+ @features
+ end
+
+ # Detect feature type(s) in the dataset
+ # @return [String] `classification", "regression", "mixed" or unknown`
+ def feature_type
+ feature_types = @features.collect{|f,metadata| metadata[OT.isA]}.uniq
+ if feature_types.size > 1
+ "mixed"
+ else
+ case feature_types.first
+ when /NominalFeature/
+ "classification"
+ when /NumericFeature/
+ "regression"
else
- # PENDING: return nil isntead of raising an exception
- raise "no confidence key"
+ "unknown"
end
- else
- LOGGER.warn "no confidence for compound: "+compound.to_s+", feature: "+feature.to_s
- return 1
end
end
+
+ # Get Spreadsheet representation
+ # @return [Spreadsheet::Workbook] Workbook which can be written with the spreadsheet gem (data_entries only, metadata will will be discarded))
+ def to_spreadsheet
+ Serializer::Spreadsheets.new(self).to_spreadsheet
+ end
+
+ # Get Excel representation (alias for to_spreadsheet)
+ # @return [Spreadsheet::Workbook] Workbook which can be written with the spreadsheet gem (data_entries only, metadata will will be discarded))
+ def to_xls
+ to_spreadsheet
+ end
+
+ # Get CSV string representation (data_entries only, metadata will be discarded)
+ # @return [String] CSV representation
+ def to_csv
+ Serializer::Spreadsheets.new(self).to_csv
+ end
+
+ # Get OWL-DL in ntriples format
+ # @return [String] N-Triples representation
+ def to_ntriples
+ s = Serializer::Owl.new
+ s.add_dataset(self)
+ s.to_ntriples
+ end
+
+ # Get OWL-DL in RDF/XML format
+ # @return [String] RDF/XML representation
+ def to_rdfxml
+ s = Serializer::Owl.new
+ s.add_dataset(self)
+ s.to_rdfxml
+ end
+
+ # Get name (DC.title) of a feature
+ # @param [String] feature Feature URI
+ # @return [String] Feture title
+ def feature_name(feature)
+ @features[feature][DC.title]
+ end
+
+ def title
+ @metadata[DC.title]
+ end
+
+ # Insert a statement (compound_uri,feature_uri,value)
+ # @example Insert a statement (compound_uri,feature_uri,value)
+ # dataset.add "http://webservices.in-silico.ch/compound/InChI=1S/C6Cl6/c7-1-2(8)4(10)6(12)5(11)3(1)9", "http://webservices.in-silico.ch/dataset/1/feature/hamster_carcinogenicity", true
+ # @param [String] compound Compound URI
+ # @param [String] feature Compound URI
+ # @param [Boolean,Float] value Feature value
+ def add (compound,feature,value)
+ @compounds << compound unless @compounds.include? compound
+ @features[feature] = {} unless @features[feature]
+ @data_entries[compound] = {} unless @data_entries[compound]
+ @data_entries[compound][feature] = [] unless @data_entries[compound][feature]
+ @data_entries[compound][feature] << value if value!=nil
+ end
+
+ # Add/modify metadata, existing entries will be overwritten
+ # @example
+ # dataset.add_metadata({DC.title => "any_title", DC.creator => "my_email"})
+ # @param [Hash] metadata Hash mapping predicate_uris to values
+ def add_metadata(metadata)
+ metadata.each { |k,v| @metadata[k] = v }
+ end
+
+ # Add a feature
+ # @param [String] feature Feature URI
+ # @param [Hash] metadata Hash with feature metadata
+ def add_feature(feature,metadata={})
+ @features[feature] = metadata
+ end
+
+ # Add/modify metadata for a feature
+ # @param [String] feature Feature URI
+ # @param [Hash] metadata Hash with feature metadata
+ def add_feature_metadata(feature,metadata)
+ metadata.each { |k,v| @features[feature][k] = v }
+ end
- # return compound-feature value
- def get_value(compound, feature)
- if (defined? @dirty_features) && @dirty_features.include?(feature)
- load_feature_values(feature)
- end
-
- v = @data[compound]
- return nil if v == nil # missing values for all features
- if v.is_a?(Array)
- # PENDING: why using an array here?
- v.each do |e|
- if e.is_a?(Hash)
- if e.has_key?(feature)
- return e[feature]
+ # 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|
+ unless @data_entries[c][f]
+ dataset.add(c,f,nil)
+ else
+ @data_entries[c][f].each do |v|
+ dataset.add(c,f,v)
+ end
end
- else
- raise "invalid internal value type"
end
end
- return nil #missing value
- else
- raise "value is not an array\n"+
- "value "+v.to_s+"\n"+
- "value-class "+v.class.to_s+"\n"+
- "dataset "+@uri.to_s+"\n"+
- "compound "+compound.to_s+"\n"+
- "feature "+feature.to_s+"\n"
end
+ dataset.add_metadata(metadata)
+ dataset.save(subjectid)
+ dataset
end
- # loads specified feature and removes dirty-flag, loads all features if feature is nil
- def load_feature_values(feature=nil)
- if feature
- raise "feature already loaded" unless @dirty_features.include?(feature)
- @owl.load_dataset_feature_values(@compounds, @data, [feature])
- @dirty_features.delete(feature)
+ # Save dataset at the dataset service
+ # - creates a new dataset if uri is not set
+ # - overwrites dataset if uri exists
+ # @return [String] Dataset URI
+ def save(subjectid=nil)
+ # TODO: rewrite feature URI's ??
+ @compounds.uniq!
+ if @uri
+ if (CONFIG[:yaml_hosts].include?(URI.parse(@uri).host))
+ RestClientWrapper.post(@uri,self.to_yaml,{:content_type => "application/x-yaml", :subjectid => subjectid})
+ else
+ File.open("ot-post-file.rdf","w+") { |f| f.write(self.to_rdfxml); @path = f.path }
+ task_uri = RestClient.post(@uri, {:file => File.new(@path)},{:accept => "text/uri-list" , :subjectid => subjectid}).to_s.chomp
+ #task_uri = `curl -X POST -H "Accept:text/uri-list" -F "file=@#{@path};type=application/rdf+xml" http://apps.ideaconsult.net:8080/ambit2/dataset`
+ Task.find(task_uri).wait_for_completion
+ self.uri = RestClientWrapper.get(task_uri,{:accept => 'text/uri-list', :subjectid => subjectid})
+ end
else
- @data = {} unless @data
- @owl.load_dataset_feature_values(@compounds, @data, @dirty_features)
- @dirty_features.clear
+ # create dataset if uri is empty
+ self.uri = RestClientWrapper.post(CONFIG[:services]["opentox-dataset"],{:subjectid => subjectid}).to_s.chomp
end
+ @uri
end
-
- # overwrite to yaml:
- # in case dataset is loaded from owl:
- # * load all values
- def to_yaml
- # loads all features
- if ((defined? @dirty_features) && @dirty_features.size > 0)
- load_feature_values
+
+ # Delete dataset at the dataset service
+ def delete(subjectid=nil)
+ RestClientWrapper.delete(@uri, :subjectid => subjectid)
+ end
+
+ private
+ # Copy a dataset (rewrites URI)
+ def copy(dataset)
+ @metadata = dataset.metadata
+ @data_entries = dataset.data_entries
+ @compounds = dataset.compounds
+ @features = dataset.features
+ if @uri
+ self.uri = @uri
+ else
+ @uri = dataset.metadata[XSD.anyURI]
end
- super
end
-
- # * remove @owl from yaml, not necessary
- def to_yaml_properties
- super - ["@owl"]
+ end
+
+ # Class with special methods for lazar prediction datasets
+ class LazarPrediction < Dataset
+
+ # Find a prediction dataset and load all data.
+ # @param [String] uri Prediction dataset URI
+ # @return [OpenTox::Dataset] Prediction dataset object with all data
+ def self.find(uri, subjectid=nil)
+ prediction = LazarPrediction.new(uri, subjectid)
+ prediction.load_all(subjectid)
+ prediction
+ end
+
+ def value(compound)
+ @data_entries[compound.uri].collect{|f,v| v.first if f.match(/prediction/)}.compact.first
end
- # saves (changes) as new dataset in dataset service
- # returns uri
- # uses to yaml method (which is overwritten)
- def save
- OpenTox::RestClientWrapper.post(@@config[:services]["opentox-dataset"],{:content_type => "application/x-yaml"},self.to_yaml).strip
+ def confidence(compound)
+ feature_uri = @data_entries[compound.uri].collect{|f,v| f if f.match(/prediction/)}.compact.first
+ @features[feature_uri][OT.confidence]
end
+
+ def descriptors(compound)
+ @data_entries[compound.uri].collect{|f,v| @features[f] if f.match(/descriptor/)}.compact if @data_entries[compound.uri]
+ end
+
+ def measured_activities(compound)
+ source = @metadata[OT.hasSource]
+ @data_entries[compound.uri].collect{|f,v| v if f.match(/#{source}/)}.compact.flatten
+ end
+
+ def neighbors(compound)
+ @data_entries[compound.uri].collect{|f,v| @features[f] if f.match(/neighbor/)}.compact
+ end
+
+# def errors(compound)
+# features = @data_entries[compound.uri].keys
+# features.collect{|f| @features[f][OT.error]}.join(" ") if features
+# end
+
end
end
diff --git a/lib/environment.rb b/lib/environment.rb
index cfc875d..59578c1 100644
--- a/lib/environment.rb
+++ b/lib/environment.rb
@@ -1,4 +1,3 @@
-require "ot-logger"
# set default environment
ENV['RACK_ENV'] = 'production' unless ENV['RACK_ENV']
@@ -12,8 +11,8 @@ TMP_DIR = File.join(basedir, "tmp")
LOG_DIR = File.join(basedir, "log")
if File.exist?(config_file)
- @@config = YAML.load_file(config_file)
- raise "could not load config, config file: "+config_file.to_s unless @@config
+ CONFIG = YAML.load_file(config_file)
+ raise "could not load config, config file: "+config_file.to_s unless CONFIG
else
FileUtils.mkdir_p TMP_DIR
FileUtils.mkdir_p LOG_DIR
@@ -24,54 +23,61 @@ else
end
# database
-if @@config[:database]
- ['dm-core', 'dm-serializer', 'dm-timestamps', 'dm-types', 'dm-migrations', 'dm-validations' ].each{|lib| require lib }
- case @@config[:database][:adapter]
- when /sqlite/i
- db_dir = File.join(basedir, "db")
- FileUtils.mkdir_p db_dir
- DataMapper::setup(:default, "sqlite3://#{db_dir}/opentox.sqlite3")
- else
- DataMapper.setup(:default, {
- :adapter => @@config[:database][:adapter],
- :database => @@config[:database][:database],
- :username => @@config[:database][:username],
- :password => @@config[:database][:password],
- :host => @@config[:database][:host]})
- end
-end
+`redis-server /opt/redis/redis.conf` unless File.exists? "/var/run/redis.pid"
+Ohm.connect :thread_safe => true
# load mail settings for error messages
load File.join config_dir,"mail.rb" if File.exists?(File.join config_dir,"mail.rb")
logfile = "#{LOG_DIR}/#{ENV["RACK_ENV"]}.log"
-#LOGGER = MyLogger.new(logfile,'daily') # daily rotation
-LOGGER = MyLogger.new(logfile) # no rotation
+#LOGGER = OTLogger.new(logfile,'daily') # daily rotation
+LOGGER = OTLogger.new(logfile) # no rotation
LOGGER.formatter = Logger::Formatter.new #this is neccessary to restore the formating in case active-record is loaded
-if @@config[:logger] and @@config[:logger] == "debug"
+if CONFIG[:logger] and CONFIG[:logger] == "debug"
LOGGER.level = Logger::DEBUG
else
LOGGER.level = Logger::WARN
end
-if File.exist?(user_file)
- @@users = YAML.load_file(user_file)
-else
- FileUtils.cp(File.join(File.dirname(__FILE__), 'templates/users.yaml'), user_file)
- puts "Please edit #{user_file} and restart your application."
- exit
-end
+# Regular expressions for parsing classification data
+TRUE_REGEXP = /^(true|active|1|1.0|tox)$/i
+FALSE_REGEXP = /^(false|inactive|0|0.0|low tox)$/i
+
+# Task durations
+DEFAULT_TASK_MAX_DURATION = 36000
+#EXTERNAL_TASK_MAX_DURATION = 36000
+
+# OWL Namespaces
+class OwlNamespace
+
+ attr_accessor :uri
+ def initialize(uri)
+ @uri = uri
+ end
+
+ def [](property)
+ @uri+property.to_s
+ end
+
+ def type # for RDF.type
+ "#{@uri}type"
+ end
+
+ def method_missing(property)
+ @uri+property.to_s
+ end
-begin
- 0 < @@users[:users].keys.length
-rescue
- raise "Please edit #{user_file} and restart your application. Create at least one user with password."
end
-# Regular expressions for parsing classification data
-TRUE_REGEXP = /^(true|active|1|1.0)$/i
-FALSE_REGEXP = /^(false|inactive|0|0.0)$/i
+AA_SERVER = CONFIG[:authorization] ? (CONFIG[:authorization][:server] ? CONFIG[:authorization][:server] : nil) : nil
+CONFIG[:authorization][:authenticate_request] = [""] unless CONFIG[:authorization][:authenticate_request]
+CONFIG[:authorization][:authorize_request] = [""] unless CONFIG[:authorization][:authorize_request]
+CONFIG[:authorization][:free_request] = [""] unless CONFIG[:authorization][:free_request]
+
+RDF = OwlNamespace.new 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+OWL = OwlNamespace.new 'http://www.w3.org/2002/07/owl#'
+DC = OwlNamespace.new 'http://purl.org/dc/elements/1.1/'
+OT = OwlNamespace.new 'http://www.opentox.org/api/1.1#'
+OTA = OwlNamespace.new 'http://www.opentox.org/algorithmTypes.owl#'
+XSD = OwlNamespace.new 'http://www.w3.org/2001/XMLSchema#'
-# Task durations
-DEFAULT_TASK_MAX_DURATION = 3600
-EXTERNAL_TASK_MAX_DURATION = 3600
diff --git a/lib/error.rb b/lib/error.rb
new file mode 100644
index 0000000..7ca9767
--- /dev/null
+++ b/lib/error.rb
@@ -0,0 +1,99 @@
+
+# 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 ServiceUnavailableError < RuntimeError
+ def http_code; 503; 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
+
+ private
+ def initialize( http_code, erroType, message, actor, errorCause, rest_params=nil, backtrace=nil )
+ @http_code = http_code
+ @errorType = erroType
+ @message = message
+ @actor = actor
+ @errorCause = errorCause
+ @rest_params = rest_params
+ @backtrace = backtrace
+ end
+
+ public
+ # 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 self.create( error, actor )
+ rest_params = error.rest_params if error.is_a?(OpenTox::RestCallError) and error.rest_params
+ backtrace = error.backtrace.short_backtrace if CONFIG[:backtrace]
+ ErrorReport.new( error.http_code, error.class.to_s, error.message, actor, error.errorCause, rest_params, backtrace )
+ end
+
+ def self.from_rdf(rdf)
+ metadata = OpenTox::Parser::Owl.from_rdf( rdf, OT.ErrorReport ).metadata
+ ErrorReport.new(metadata[OT.statusCode], metadata[OT.errorCode], metadata[OT.message], metadata[OT.actor], metadata[OT.errorCause])
+ 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 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
+
+class Array
+ def short_backtrace
+ short = []
+ each do |c|
+ break if c =~ /sinatra\/base/
+ short << c
+ end
+ short.join("\n")
+ end
+end \ No newline at end of file
diff --git a/lib/feature.rb b/lib/feature.rb
new file mode 100644
index 0000000..c0729a7
--- /dev/null
+++ b/lib/feature.rb
@@ -0,0 +1,43 @@
+module OpenTox
+ class Feature
+ include OpenTox
+
+ def self.find(uri, subjectid=nil)
+ return nil unless 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
+ feature.add_metadata Parser::Owl::Dataset.new(uri).load_metadata
+ end
+ feature
+ end
+
+ # provides domain (possible target values) of classification feature
+ # @return [Array] list with possible target values
+ def domain
+ if metadata[OT.acceptValue]
+ raise "accept value found, remove hack and implement correctly"
+ else
+ if @uri=~/feature\/26221/ || @uri=~/feature\/221726/
+ return ["mutagen" , "nonmutagen"]
+ end
+ return [true, false]
+ end
+ 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/features.rb b/lib/features.rb
deleted file mode 100644
index 0fa1cf0..0000000
--- a/lib/features.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# CH: should go into validation service
-# - not a complete OT object
-# - only used twice
-# - what about ./validation/validation/validation_service.rb:241: value = OpenTox::Feature.new(:uri => a.uri).value(prediction_feature).to_s
-module OpenTox
-
- module Feature
-
- def self.domain( feature_uri )
- #TODO
- if feature_uri =~ /ambit/
- return nil
- else
- return ["true", "false"]
- end
- end
-
- end
-end
diff --git a/lib/helper.rb b/lib/helper.rb
index a9f451e..3031b74 100644
--- a/lib/helper.rb
+++ b/lib/helper.rb
@@ -1,26 +1,99 @@
helpers do
- # Authentification
- def protected!
- response['WWW-Authenticate'] = %(Basic realm="Testing HTTP Auth") and \
- throw(:halt, [401, "Not authorized\n"]) and \
- return unless authorized?
+ # Authentification
+ def protected!(subjectid)
+ if env["session"]
+ unless authorized?(subjectid)
+ flash[:notice] = "You don't have access to this section: "
+ redirect back
+ end
+ elsif !env["session"] && subjectid
+ unless authorized?(subjectid)
+ LOGGER.debug "URI not authorized: clean: " + clean_uri("#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}#{request.env['REQUEST_URI']}").to_s + " full: #{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}#{request.env['REQUEST_URI']} with request: #{request.env['REQUEST_METHOD']}"
+ raise OpenTox::NotAuthorizedError.new "Not authorized"
+ end
+ else
+ raise OpenTox::NotAuthorizedError.new "Not authorized" unless authorized?(subjectid)
+ end
end
- def authorized?
- @auth ||= Rack::Auth::Basic::Request.new(request.env)
- @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == ['api', API_KEY]
+ #Check Authorization for URI with method and subjectid.
+ def authorized?(subjectid)
+ request_method = request.env['REQUEST_METHOD']
+ uri = clean_uri("#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}#{request.env['REQUEST_URI']}")
+ request_method = "GET" if request_method == "POST" && uri =~ /\/model\/\d+\/?$/
+ return OpenTox::Authorization.authorized?(uri, request_method, subjectid)
end
-
-=begin
- def xml(object)
- builder do |xml|
- xml.instruct!
- object.to_xml
- end
- end
-=end
+ #cleans URI from querystring and file-extension. Sets port 80 to emptystring
+ # @param [String] uri
+ def clean_uri(uri)
+ uri = uri.sub(" ", "%20") #dirty hacks => to fix
+ uri = uri[0,uri.index("InChI=")] if uri.index("InChI=")
+
+ out = URI.parse(uri)
+ out.path = out.path[0, out.path.length - (out.path.reverse.rindex(/\/{1}\d+\/{1}/))] if out.path.index(/\/{1}\d+\/{1}/) #cuts after /id/ for a&a
+ port = (out.scheme=="http" && out.port==80)||(out.scheme=="https" && out.port==443) ? "" : ":#{out.port.to_s}"
+ "#{out.scheme}://#{out.host}#{port}#{out.path.chomp("/")}" #"
+ end
+
+ #unprotected uri for login
+ def login_requests
+ return env['REQUEST_URI'] =~ /\/login$/
+ end
+
+ def uri_available?(urlStr)
+ url = URI.parse(urlStr)
+ unless @subjectid
+ Net::HTTP.start(url.host, url.port) do |http|
+ return http.head(url.request_uri).code == "200"
+ end
+ else
+ Net::HTTP.start(url.host, url.port) do |http|
+ return http.post(url.request_uri, "subjectid=#{@subjectid}").code == "202"
+ end
+ end
+ end
end
+before do
+ unless !AA_SERVER or login_requests or CONFIG[:authorization][:free_request].include?(env['REQUEST_METHOD'])
+ begin
+ subjectid = nil
+ subjectid = session[:subjectid] if session[:subjectid]
+ subjectid = params[:subjectid] if params[:subjectid] and !subjectid
+ subjectid = request.env['HTTP_SUBJECTID'] if request.env['HTTP_SUBJECTID'] and !subjectid
+ subjectid = request.cookies["subjectid"] unless subjectid
+ # see http://rack.rubyforge.org/doc/SPEC.html
+ subjectid = CGI.unescape(subjectid) if subjectid.include?("%23")
+ @subjectid = subjectid
+ rescue
+ #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
+ @subjectid = subjectid
+ protected!(subjectid)
+
+ extension = File.extname(request.path_info) # params[:id] is not yet available
+ unless extension.empty?
+ #request.path_info.sub!(/\.#{extension}$/,'')
+ case extension
+ when "html"
+ @accept = 'text/html'
+ when "yaml"
+ @accept = 'application/x-yaml'
+ when "csv"
+ @accept = 'text/csv'
+ when "rdfxml"
+ @accept = 'application/rdf+xml'
+ when "xls"
+ @accept = 'application/ms-excel'
+ else
+ halt 404, "File format #{extension} not supported."
+ end
+ end
+
+ end
+end
+
diff --git a/lib/model.rb b/lib/model.rb
index e36b538..74408d8 100644
--- a/lib/model.rb
+++ b/lib/model.rb
@@ -1,143 +1,318 @@
module OpenTox
- module Model
- class Generic
+ module Model
- MODEL_ATTRIBS = [:uri, :title, :creator, :date, :format, :predictedVariables, :independentVariables, :dependentVariables, :trainingDataset, :algorithm]
- MODEL_ATTRIBS.each{ |a| attr_accessor(a) }
+ include OpenTox
- def self.find(uri)
- owl = OpenTox::Owl.from_uri(uri, "Model")
- return self.new(owl)
- end
-
- def self.to_rdf(model)
- owl = OpenTox::Owl.create 'Model', model.uri
- (MODEL_ATTRIBS - [:uri]).each do |a|
- owl.set(a.to_s,model.send(a.to_s))
+ # 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, accept_header=nil, waiting_task=nil )
+ unless accept_header
+ if CONFIG[:yaml_hosts].include?(URI.parse(@uri).host)
+ accept_header = 'application/x-yaml'
+ else
+ accept_header = 'application/rdf+xml'
end
- owl.rdf
end
+ LOGGER.info "running model "+@uri.to_s+", params: "+params.inspect+", accept: "+accept_header.to_s
+ RestClientWrapper.post(@uri,params,{:accept => accept_header},waiting_task).to_s
+ end
+
+ # Generic OpenTox model class for all API compliant services
+ class Generic
+ include Model
- protected
- def initialize(owl)
- MODEL_ATTRIBS.each do |a|
- self.send("#{a.to_s}=".to_sym, owl.get(a.to_s)) unless a==:uri
- end
- @uri = owl.uri
- if ENV['RACK_ENV'] =~ /test|debug/
- begin
- raise "uri invalid" unless Utils.is_uri?(@uri)
- raise "no predicted variables" unless @predictedVariables and @predictedVariables.size>0
- rescue => ex
- RestClientWrapper.raise_uri_error "invalid model: '"+ex.message+"'\n"+self.to_yaml+"\n",@uri.to_s
+ # Find Generic Opentox Model via URI, and loads metadata, could raise NotFound/NotAuthorized error
+ # @param [String] uri Model URI
+ # @return [OpenTox::Model::Generic] Model instance
+ def self.find(uri,subjectid=nil)
+ return nil unless uri
+ model = Generic.new(uri)
+ model.load_metadata(subjectid)
+ raise "could not load model metadata '"+uri.to_s+"'" if model.metadata==nil or model.metadata.size==0
+ model
+ end
+
+ # provides feature type, possible types are "regression" or "classification"
+ # @return [String] feature type, "unknown" if type could not be estimated
+ def feature_type(subjectid=nil)
+ return @feature_type if @feature_type
+
+ # dynamically perform restcalls if necessary
+ load_metadata(subjectid) if @metadata==nil or @metadata.size==0 or (@metadata.size==1 && @metadata.values[0]==@uri)
+ algorithm = OpenTox::Algorithm::Generic.find(@metadata[OT.algorithm], subjectid)
+ algorithm_title = algorithm ? algorithm.metadata[DC.title] : nil
+ algorithm_type = algorithm ? algorithm.metadata[OT.isA] : nil
+ dependent_variable = OpenTox::Feature.find( @metadata[OT.dependentVariables],subjectid )
+ dependent_variable_type = dependent_variable ? dependent_variable.feature_type : nil
+ type_indicators = [dependent_variable_type, @metadata[OT.isA], @metadata[DC.title],
+ @uri, algorithm_type, algorithm_title]
+ type_indicators.each do |type|
+ case type
+ when /(?i)classification/
+ @feature_type = "classification"
+ break
+ when /(?i)regression/
+ @feature_type = "regression"
end
- LOGGER.warn "model has no dependent variable" unless @dependentVariables and @dependentVariables.size>0
- LOGGER.warn "model has no algorithm" unless @algorithm and @algorithm.size>0
- LOGGER.warn "model has no indenpendent variables" unless @independentVariables
end
+ raise "unknown model "+type_indicators.inspect unless @feature_type
+ @feature_type
end
- end
-
- class PredictionModel < Generic
- def self.build( algorithm_uri, algorithm_params )
-
- LOGGER.debug "Build model, algorithm_uri:"+algorithm_uri.to_s+", algorithm_parms: "+algorithm_params.inspect.to_s
- uri = OpenTox::RestClientWrapper.post(algorithm_uri,algorithm_params).to_s
- LOGGER.debug "Build model done: "+uri.to_s
- RestClientWrapper.raise_uri_error("Invalid build model result: '"+uri.to_s+"'", algorithm_uri, algorithm_params ) unless Utils.model_uri?(uri)
- return PredictionModel.find(uri)
- end
-
- def predict_dataset( dataset_uri )
-
- LOGGER.debug "Predict dataset: "+dataset_uri.to_s+" with model "+@uri.to_s
- uri = RestClientWrapper.post(@uri, {:accept => "text/uri-list", :dataset_uri=>dataset_uri})
- RestClientWrapper.raise_uri_error("Prediciton result no dataset uri: "+uri.to_s, @uri, {:dataset_uri=>dataset_uri} ) unless Utils.dataset_uri?(uri)
- uri
- end
-
- def classification?
- #HACK replace with request to ontology server
- if @title =~ /(?i)classification/
- return true
- elsif @title =~ /(?i)regression/
- return false
- elsif @uri =~/ntua/ and @title =~ /mlr/
- return false
- elsif @uri =~/tu-muenchen/ and @title =~ /regression|M5P|GaussP/
- return false
- elsif @uri =~/ambit2/ and @title =~ /pKa/ || @title =~ /Regression|Caco/
- return false
- elsif @uri =~/majority/
- return (@uri =~ /class/) != nil
+ end
+
+ # Lazy Structure Activity Relationship class
+ class Lazar
+
+ include Model
+ include Algorithm
+
+ attr_accessor :compound, :prediction_dataset, :features, :effects, :activities, :p_values, :fingerprints, :feature_calculation_algorithm, :similarity_algorithm, :prediction_algorithm, :min_sim, :subjectid
+
+ def initialize(uri=nil)
+
+ if uri
+ super uri
else
- raise "unknown model, uri:'"+@uri.to_s+"' title:'"+@title.to_s+"'"
+ super CONFIG[:services]["opentox-model"]
end
- end
- end
-
- class Lazar < Generic
-
- attr_accessor :feature_dataset_uri, :effects, :activities, :p_values, :fingerprints, :features
-
- def initialize
- @source = "http://github.com/helma/opentox-model"
- @algorithm = File.join(@@config[:services]["opentox-algorithm"],"lazar")
- #@independent_variables = File.join(@@config[:services]["opentox-algorithm"],"fminer#BBRC_representative")
+
+ @metadata[OT.algorithm] = File.join(CONFIG[:services]["opentox-algorithm"],"lazar")
+
@features = []
@effects = {}
@activities = {}
@p_values = {}
@fingerprints = {}
+
+ @feature_calculation_algorithm = "Substructure.match"
+ @similarity_algorithm = "Similarity.tanimoto"
+ @prediction_algorithm = "Neighbors.weighted_majority_vote"
+
+ @min_sim = 0.3
+
end
- def save
- @features.uniq!
- resource = RestClient::Resource.new(@@config[:services]["opentox-model"], :user => @@users[:users].keys[0], :password => @@users[:users].values[0])
- resource.post(self.to_yaml, :content_type => "application/x-yaml").chomp.to_s
+ # Get URIs of all lazar models
+ # @return [Array] List of lazar model URIs
+ def self.all(subjectid=nil)
+ RestClientWrapper.get(CONFIG[:services]["opentox-model"], :subjectid => subjectid).to_s.split("\n")
end
- def self.find_all
- RestClientWrapper.get(@@config[:services]["opentox-model"]).chomp.split("\n")
+ # Find a lazar model
+ # @param [String] uri Model URI
+ # @return [OpenTox::Model::Lazar] lazar model
+ def self.find(uri, subjectid=nil)
+ YAML.load RestClientWrapper.get(uri,{:accept => 'application/x-yaml', :subjectid => subjectid})
end
- def self.predict(compound_uri,model_uri)
- #RestClientWrapper.post(model_uri,{:compound_uri => compound_uri, :accept => 'application/x-yaml'})
- `curl -X POST -d 'compound_uri=#{compound_uri}' -H 'Accept:application/x-yaml' #{model_uri}`
+ # Create a new lazar model
+ # @param [optional,Hash] params Parameters for the lazar algorithm (OpenTox::Algorithm::Lazar)
+ # @return [OpenTox::Model::Lazar] lazar model
+ def self.create(params)
+ lazar_algorithm = OpenTox::Algorithm::Generic.new File.join( CONFIG[:services]["opentox-algorithm"],"lazar")
+ model_uri = lazar_algorithm.run(params)
+ OpenTox::Model::Lazar.find(model_uri, params[:subjectid])
end
- end
-
- class PropertyLazar < Generic
-
- attr_accessor :feature_dataset_uri, :properties, :features, :activities#, :effects, :p_values
-
- def initialize
- @source = "http://github.com/helma/opentox-model"
- @algorithm = File.join(@@config[:services]["opentox-algorithm"],"property_lazar")
- #@independent_variables = File.join(@@config[:services]["opentox-algorithm"],"fminer#BBRC_representative")
- @features = []
- #@effects = {}
- @activities = {}
- #@p_values = {}
- @properties = {}
+
+ # Get a parameter value
+ # @param [String] param Parameter name
+ # @return [String] Parameter value
+ def parameter(param)
+ @metadata[OT.parameters].collect{|p| p[OT.paramValue] if p[DC.title] == param}.compact.first
end
- def save
- @features.uniq!
- resource = RestClient::Resource.new(@@config[:services]["opentox-model"], :user => @@users[:users].keys[0], :password => @@users[:users].values[0])
- resource.post(self.to_yaml, :content_type => "application/x-yaml").chomp.to_s
+ # 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, waiting_task=nil)
+ @prediction_dataset = Dataset.create(CONFIG[:services]["opentox-dataset"], subjectid)
+ @prediction_dataset.add_metadata({
+ OT.hasSource => @uri,
+ DC.creator => @uri,
+ DC.title => URI.decode(File.basename( @metadata[OT.dependentVariables] )),
+ OT.parameters => [{DC.title => "dataset_uri", OT.paramValue => dataset_uri}]
+ })
+ d = Dataset.new(dataset_uri,subjectid)
+ d.load_compounds(subjectid)
+ count = 0
+ d.compounds.each do |compound_uri|
+ 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
end
- def self.find_all
- RestClientWrapper.get(@@config[:services]["opentox-model"]).chomp.split("\n")
+ # Predict a compound
+ # @param [String] compound_uri Compound URI
+ # @param [optinal,Boolean] verbose Verbose prediction (output includes neighbors and features)
+ # @return [OpenTox::Dataset] Dataset with prediction
+ def predict(compound_uri,verbose=false,subjectid=nil)
+
+ @compound = Compound.new compound_uri
+ features = {}
+
+ unless @prediction_dataset
+ #@prediction_dataset = cached_prediction
+ #return @prediction_dataset if cached_prediction
+ @prediction_dataset = Dataset.create(CONFIG[:services]["opentox-dataset"], subjectid)
+ @prediction_dataset.add_metadata( {
+ OT.hasSource => @uri,
+ DC.creator => @uri,
+ # TODO: fix dependentVariable
+ DC.title => URI.decode(File.basename( @metadata[OT.dependentVariables] )),
+ OT.parameters => [{DC.title => "compound_uri", OT.paramValue => compound_uri}]
+ } )
+ end
+
+ return @prediction_dataset if database_activity(subjectid)
+
+ neighbors
+ prediction = eval("#{@prediction_algorithm}(@neighbors,{:similarity_algorithm => @similarity_algorithm, :p_values => @p_values})")
+
+ prediction_feature_uri = File.join( @prediction_dataset.uri, "feature", "prediction", File.basename(@metadata[OT.dependentVariables]),@prediction_dataset.compounds.size.to_s)
+ # TODO: fix dependentVariable
+ @prediction_dataset.metadata[OT.dependentVariables] = prediction_feature_uri
+
+ if @neighbors.size == 0
+ @prediction_dataset.add_feature(prediction_feature_uri, {
+ OT.isA => OT.MeasuredFeature,
+ OT.hasSource => @uri,
+ DC.creator => @uri,
+ DC.title => URI.decode(File.basename( @metadata[OT.dependentVariables] )),
+ OT.error => "No similar compounds in training dataset.",
+ OT.parameters => [{DC.title => "compound_uri", OT.paramValue => compound_uri}]
+ })
+ @prediction_dataset.add @compound.uri, prediction_feature_uri, prediction[:prediction]
+
+ else
+ @prediction_dataset.add_feature(prediction_feature_uri, {
+ OT.isA => OT.ModelPrediction,
+ OT.hasSource => @uri,
+ DC.creator => @uri,
+ DC.title => URI.decode(File.basename( @metadata[OT.dependentVariables] )),
+ OT.prediction => prediction[:prediction],
+ OT.confidence => prediction[:confidence],
+ OT.parameters => [{DC.title => "compound_uri", OT.paramValue => compound_uri}]
+ })
+ @prediction_dataset.add @compound.uri, prediction_feature_uri, prediction[:prediction]
+
+ if verbose
+ if @feature_calculation_algorithm == "Substructure.match"
+ f = 0
+ @compound_features.each do |feature|
+ feature_uri = File.join( @prediction_dataset.uri, "feature", "descriptor", f.to_s)
+ features[feature] = feature_uri
+ @prediction_dataset.add_feature(feature_uri, {
+ OT.isA => OT.Substructure,
+ OT.smarts => feature,
+ OT.pValue => @p_values[feature],
+ OT.effect => @effects[feature]
+ })
+ @prediction_dataset.add @compound.uri, feature_uri, true
+ f+=1
+ end
+ else
+ @compound_features.each do |feature|
+ features[feature] = feature
+ @prediction_dataset.add @compound.uri, feature, true
+ end
+ end
+ n = 0
+ @neighbors.each do |neighbor|
+ neighbor_uri = File.join( @prediction_dataset.uri, "feature", "neighbor", n.to_s )
+ @prediction_dataset.add_feature(neighbor_uri, {
+ OT.compound => neighbor[:compound],
+ OT.similarity => neighbor[:similarity],
+ OT.measuredActivity => neighbor[:activity],
+ OT.isA => OT.Neighbor
+ })
+ @prediction_dataset.add @compound.uri, neighbor_uri, true
+ f = 0 unless f
+ neighbor[:features].each do |feature|
+ if @feature_calculation_algorithm == "Substructure.match"
+ feature_uri = File.join( @prediction_dataset.uri, "feature", "descriptor", f.to_s) unless feature_uri = features[feature]
+ else
+ feature_uri = feature
+ end
+ @prediction_dataset.add neighbor[:compound], feature_uri, true
+ unless features.has_key? feature
+ features[feature] = feature_uri
+ @prediction_dataset.add_feature(feature_uri, {
+ OT.isA => OT.Substructure,
+ OT.smarts => feature,
+ OT.pValue => @p_values[feature],
+ OT.effect => @effects[feature]
+ })
+ f+=1
+ end
+ end
+ n+=1
+ end
+ # what happens with dataset predictions?
+ end
+ end
+
+ @prediction_dataset.save(subjectid)
+ @prediction_dataset
+ end
+
+ # Find neighbors and store them as object variable
+ def neighbors
+
+ @compound_features = eval("#{@feature_calculation_algorithm}(@compound,@features)") if @feature_calculation_algorithm
+
+ @neighbors = []
+ @fingerprints.each do |training_compound,training_features|
+ sim = eval("#{@similarity_algorithm}(@compound_features,training_features,@p_values)")
+ if sim > @min_sim
+ @activities[training_compound].each do |act|
+ @neighbors << {
+ :compound => training_compound,
+ :similarity => sim,
+ :features => training_features,
+ :activity => act
+ }
+ end
+ end
+ end
+
+ end
+
+ # Find database activities and store them in @prediction_dataset
+ # @return [Boolean] true if compound has databasse activities, false if not
+ def database_activity(subjectid)
+ if @activities[@compound.uri]
+ @activities[@compound.uri].each { |act| @prediction_dataset.add @compound.uri, @metadata[OT.dependentVariables], act }
+ @prediction_dataset.add_metadata(OT.hasSource => @metadata[OT.trainingDataset])
+ @prediction_dataset.save(subjectid)
+ true
+ else
+ false
+ end
+ end
+
+ # Save model at model service
+ def save(subjectid)
+ self.uri = RestClientWrapper.post(@uri,self.to_yaml,{:content_type => "application/x-yaml", :subjectid => subjectid})
end
- def self.predict(compound_uri,model_uri)
- #RestClientWrapper.post(model_uri,{:compound_uri => compound_uri, :accept => 'application/x-yaml'})
- `curl -X POST -d 'compound_uri=#{compound_uri}' -H 'Accept:application/x-yaml' #{model_uri}`
+ # Delete model at model service
+ def delete(subjectid)
+ RestClientWrapper.delete(@uri, :subjectid => subjectid) unless @uri == CONFIG[:services]["opentox-model"]
end
+
end
end
end
diff --git a/lib/ontology_service.rb b/lib/ontology_service.rb
new file mode 100644
index 0000000..4ff688f
--- /dev/null
+++ b/lib/ontology_service.rb
@@ -0,0 +1,43 @@
+module OpenTox
+ module OntologyService
+ module Endpoints
+ require 'sparql/client'
+ @sparql = SPARQL::Client.new("http://apps.ideaconsult.net:8080/ontology")
+ def self.qs(classname="Endpoints")
+ return "PREFIX ot:<http://www.opentox.org/api/1.1#>
+ PREFIX ota:<http://www.opentox.org/algorithms.owl#>
+ PREFIX owl:<http://www.w3.org/2002/07/owl#>
+ PREFIX dc:<http://purl.org/dc/elements/1.1/>
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ PREFIX rdf:<http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+ PREFIX otee:<http://www.opentox.org/echaEndpoints.owl#>
+ PREFIX toxcast:<http://www.opentox.org/toxcast.owl#>
+ select ?Endpoints ?title ?id
+ where {?Endpoints rdfs:subClassOf otee:#{classname}.
+ OPTIONAL {?Endpoints dc:title ?title}.
+ OPTIONAL {?Endpoints dc:identifier ?id}.}
+ ORDER BY ?title"
+ end
+
+ def self.make_option_list(endpoint="Endpoints", level=1)
+ out = ""
+ results = @sparql.query(qs(endpoint)) rescue results = []
+ results.each do |result|
+ endpointname = result.Endpoints.to_s.split('#').last
+ title = result.bound?(:title) ? result.title : endpointname
+ out += "<option value='#{title}' id='#{endpointname}' class='level_#{level}'>#{title}</option>\n"
+ out += make_option_list(endpointname, level + 1)
+ end
+ return out
+ end
+
+ def self.get_endpoint_selectlist(include_blank=true)
+ out = "<select id='endpoint' name='endpoint'>\n"
+ out += "<option value='' id='please_select'>Please select</option>\n" if include_blank
+ out += make_option_list
+ out += "</select>\n"
+ return out
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/opentox-ruby-api-wrapper.rb b/lib/opentox-ruby-api-wrapper.rb
deleted file mode 100644
index 45a3428..0000000
--- a/lib/opentox-ruby-api-wrapper.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-['rubygems', 'sinatra', 'sinatra/url_for', 'rest_client', 'yaml', 'cgi', 'spork', 'redland', 'rdf/redland', 'rdf/redland/util', 'environment'].each do |lib|
- require lib
-end
-
-begin
- require 'openbabel'
-rescue LoadError
- puts "Please install Openbabel with 'rake openbabel:install' in the compound component"
-end
-
-['owl', 'compound','dataset','algorithm','model','task','validation','utils','authorization','features', 'ot-logger', 'overwrite', 'rest_client_wrapper'].each do |lib|
- require lib
-end
diff --git a/lib/opentox-ruby.rb b/lib/opentox-ruby.rb
new file mode 100644
index 0000000..ab8d824
--- /dev/null
+++ b/lib/opentox-ruby.rb
@@ -0,0 +1,14 @@
+['rubygems', 'sinatra', 'sinatra/url_for', 'ohm', 'rest_client', 'yaml', 'cgi', 'spork', 'error', 'overwrite', 'environment'].each do |lib|
+ require lib
+end
+
+begin
+ require 'openbabel'
+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', 'to-html' ].each do |lib|
+ require lib
+end
diff --git a/lib/opentox.rb b/lib/opentox.rb
new file mode 100644
index 0000000..1992896
--- /dev/null
+++ b/lib/opentox.rb
@@ -0,0 +1,52 @@
+module OpenTox
+
+ attr_reader :uri
+ attr_accessor :metadata
+
+ # Initialize OpenTox object with optional uri
+ # @param [optional, String] URI
+ def initialize(uri=nil)
+ @metadata = {}
+ self.uri = uri if uri
+ end
+
+ # Set URI
+ # @param [String] URI
+ def uri=(uri)
+ @uri = uri
+ @metadata[XSD.anyURI] = uri
+ end
+
+ # Get all objects from a service
+ # @return [Array] List of available URIs
+ def self.all(uri, subjectid=nil)
+ RestClientWrapper.get(uri,:accept => "text/uri-list", :subjectid => subjectid).to_s.split(/\n/)
+ end
+
+ # Load (and return) metadata from object URI
+ # @return [Hash] Metadata
+ def load_metadata(subjectid=nil)
+ @metadata = Parser::Owl::Generic.new(@uri).load_metadata(subjectid)
+ @metadata
+ end
+
+ def add_metadata(metadata)
+ metadata.each { |k,v| @metadata[k] = v }
+ end
+
+ # Get OWL-DL representation in RDF/XML format
+ # @return [application/rdf+xml] RDF/XML representation
+ def to_rdfxml
+ s = Serializer::Owl.new
+ s.add_metadata(@uri,@metadata)
+ #s.add_parameters(@uri,@parameters) if @parameters
+ s.to_rdfxml
+ end
+
+ # deletes the resource, deletion should have worked when no RestCallError raised
+ def delete(subjectid=nil)
+ RestClientWrapper.delete(uri,:subjectid => subjectid)
+ end
+
+end
+
diff --git a/lib/ot-logger.rb b/lib/ot-logger.rb
deleted file mode 100644
index df38d77..0000000
--- a/lib/ot-logger.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'logger'
-# logging
-class MyLogger < Logger
-
- def pwd
- path = Dir.pwd.to_s
- index = path.rindex(/\//)
- return path if index==nil
- path[(index+1)..-1]
- end
-
- def trace()
- lines = caller(0)
- n = 2
- line = lines[n]
-
- while (line =~ /spork.rb/ or line =~ /as_task/ or line =~ /ot-logger.rb/)
- n += 1
- line = lines[n]
- end
-
- index = line.rindex(/\/.*\.rb/)
- return line if index==nil
- line[index..-1]
- end
-
- def format(msg)
- pwd.ljust(18)+" :: "+msg.to_s+" :: "+trace+" :: "+($sinatra ? $sinatra.request.env['REMOTE_ADDR'] : nil).to_s
- end
-
- def debug(msg)
- super format(msg)
- end
-
- def info(msg)
- super format(msg)
- end
-
- def warn(msg)
- super format(msg)
- end
-
- def error(msg)
- super format(msg)
- end
-
-end
-
diff --git a/lib/overwrite.rb b/lib/overwrite.rb
index 1d0161b..fbe775d 100644
--- a/lib/overwrite.rb
+++ b/lib/overwrite.rb
@@ -1,14 +1,145 @@
# 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/
+}
+
+# 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 message and backtrace to logfile
+ LOGGER.error error.class.to_s+": "+error.message
+ LOGGER.error ":\n"+error.backtrace.join("\n")
+
+ actor = "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}#{request.env['REQUEST_URI']}"
+ rep = OpenTox::ErrorReport.create(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, @subjectid)
+ else
+ content_type 'application/x-yaml'
+ halt error.http_code,rep.to_yaml
+ end
+end
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
+
+ def return_task( task )
+ code = task.running? ? 202 : 200
+ case request.env['HTTP_ACCEPT']
+ when /rdf/
+ response['Content-Type'] = "application/rdf+xml"
+ halt code,task.to_rdfxml
+ when /yaml/
+ response['Content-Type'] = "application/x-yaml"
+ halt code,task.to_yaml # PENDING differs from task-webservice
+ when /html/
+ response['Content-Type'] = "text/html"
+ halt code,OpenTox.text_to_html(task.to_yaml, @subjectid)
+ else # default /uri-list/
+ response['Content-Type'] = "text/uri-list"
+ halt code,task.uri+"\n"
+ end
+ 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 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'
+# logging
+#class Logger
+class OTLogger < Logger
+
+ def pwd
+ path = Dir.pwd.to_s
+ index = path.rindex(/\//)
+ return path if index==nil
+ path[(index+1)..-1]
+ end
+
+ def trace()
+ lines = caller(0)
+ n = 2
+ line = lines[n]
+
+ while (line =~ /spork.rb/ or line =~ /create/ or line =~ /overwrite.rb/)
+ n += 1
+ line = lines[n]
+ end
+
+ index = line.rindex(/\/.*\.rb/)
+ return line if index==nil
+ line[index..-1]
+ end
+
+ def format(msg)
+ pwd.ljust(18)+" :: "+msg.to_s+" :: "+trace
+ end
+
+ def debug(msg)
+ super format(msg)
+ end
+
+ def info(msg)
+ super format(msg)
+ end
+
+ def warn(msg)
+ super format(msg)
+ end
+
+ def error(msg)
+ super format(msg)
+ end
+
+end
+
+# make migration from datamapper more straightforward
+class Ohm::Model
+ def self.get(id)
+ self[id]
+ end
+end
diff --git a/lib/owl.rb b/lib/owl.rb
deleted file mode 100644
index dcf26a5..0000000
--- a/lib/owl.rb
+++ /dev/null
@@ -1,552 +0,0 @@
-# RDF namespaces
-RDF = Redland::Namespace.new 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
-OWL = Redland::Namespace.new 'http://www.w3.org/2002/07/owl#'
-DC = Redland::Namespace.new 'http://purl.org/dc/elements/1.1/'
-OT = Redland::Namespace.new 'http://www.opentox.org/api/1.1#'
-#OT = Redland::Namespace.new 'http://ortona.informatik.uni-freiburg.de/opentox.owl#'
-XML = Redland::Namespace.new 'http://www.w3.org/2001/XMLSchema#'
-
-# overriding literal to give nice access to datatype
-# and to access the stored value as correct ruby type
-class Redland::Literal
-
- def self.create(value, type)
- raise "literal datatype may not be nil" unless type
- type = parse_datatype_uri(value) if OpenTox::Owl::PARSE_LITERAL_TYPE==type
-
- if type.is_a?(Redland::Uri)
- Redland::Literal.new(value.to_s,nil,type)
- else
- Redland::Literal.new(value.to_s,nil,Redland::Uri.new(type.to_s))
- end
- end
-
- # the literal node of the ruby swig api provdides the 'value' of a literal but not the 'datatype'
- # found solution in mailing list
- def datatype
- uri = Redland.librdf_node_get_literal_value_datatype_uri(self.node)
- return Redland.librdf_uri_to_string(uri) if uri
- end
-
- # gets value of literal, value class is se according to literal datatype
- def get_value
- Redland::Literal.parse_value( self.value, self.datatype )
- end
-
- private
- # parses value according to datatype uri
- def self.parse_value(string_value, datatype_uri)
-
- if (datatype_uri==nil || datatype_uri.size==0)
- LOGGER.warn("empty datatype for literal with value: '"+string_value+"'")
- return string_value
- end
- case datatype_uri
- when OpenTox::Owl::LITERAL_DATATYPE_STRING.to_s
- return string_value
- when OpenTox::Owl::LITERAL_DATATYPE_URI.to_s
- return string_value #PENDING uri as string?
- when OpenTox::Owl::LITERAL_DATATYPE_FLOAT.to_s
- return string_value.to_f
- when OpenTox::Owl::LITERAL_DATATYPE_DOUBLE.to_s
- return string_value.to_f
- when OpenTox::Owl::LITERAL_DATATYPE_BOOLEAN.to_s
- return string_value.upcase=="TRUE"
- when OpenTox::Owl::LITERAL_DATATYPE_DATE.to_s
- return Time.parse(string_value)
- when OpenTox::Owl::LITERAL_DATATYPE_DATETIME.to_s
- return Time.parse(string_value)
- when OpenTox::Owl::LITERAL_DATATYPE_INTEGER.to_s
- return string_value.to_i
- else
- raise "unknown literal datatype: '"+datatype_uri.to_s+"' (value is "+string_value+
- "), please specify new OpenTox::Owl::LITERAL_DATATYPE"
- end
- end
-
- # parse datatype uri accoring to value class
- def self.parse_datatype_uri(value)
- if value==nil
- raise "illegal datatype: value is nil"
- elsif value.is_a?(String)
- # PENDING: uri check too slow?
- if OpenTox::Utils.is_uri?(value)
- return OpenTox::Owl::LITERAL_DATATYPE_URI
- else
- return OpenTox::Owl::LITERAL_DATATYPE_STRING
- end
- elsif value.is_a?(Float)
- return OpenTox::Owl::LITERAL_DATATYPE_FLOAT
- elsif value.is_a?(TrueClass) or value.is_a?(FalseClass)
- return OpenTox::Owl::LITERAL_DATATYPE_BOOLEAN
- elsif value.is_a?(Integer)
- return OpenTox::Owl::LITERAL_DATATYPE_INTEGER
- elsif value.is_a?(DateTime)
- return OpenTox::Owl::LITERAL_DATATYPE_DATETIME
- elsif value.is_a?(Time)
- return OpenTox::Owl::LITERAL_DATATYPE_DATETIME
- else
- raise "illegal datatype: "+value.class.to_s+" "+value.to_s
- end
- end
-end
-
-module OpenTox
-
- class Owl
-
- # to get correct owl-dl, properties and objects have to be typed
- # i.e. the following triple is insufficient:
- # ModelXY,ot:algorithm,AlgorithmXY
- # further needed:
- # ot:algorithm,rdf:type,owl:ObjectProperty
- # AlgorithmXY,rdf:type,ot:Algorithm
- # ot:Algorithm,rdf:type,owl:Class
- #
- # therefore OpentoxOwl needs info about the opentox-ontology
- # the info is stored in OBJECT_PROPERTY_CLASS and LITERAL_TYPES
-
- # contains all owl:ObjectProperty as keys, and the respective classes as value
- # some object properties link to objects from different classes (e.g. "values can be "Tuple", or "FeatureValue")
- # in this case, use set_object_property() (instead of set()) and specify class manually
- OBJECT_PROPERTY_CLASS = {}
- [ "model" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Model"}
- [ "algorithm" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Algorithm"}
- [ "trainingDataset", "testTargetDataset", "predictionDataset",
- "testDataset", "dataset" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Dataset"}
- [ "feature", "dependentVariables", "independentVariables",
- "predictedVariables", "predictionFeature" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Feature"}
- [ "parameters" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Parameter"}
- [ "compound" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Compound"}
- [ "dataEntry" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "DataEntry"}
- [ "complexValue" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "FeatureValue"}
- [ "classificationStatistics" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "ClassificationStatistics"}
- [ "classValueStatistics" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "ClassValueStatistics"}
- [ "confusionMatrix" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "ConfusionMatrix"}
- [ "confusionMatrixCell" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "ConfusionMatrixCell"}
- [ "regressionStatistics" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "RegressionStatistics"}
- [ "validation" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Validation"}
- [ "crossvalidationInfo" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "CrossvalidationInfo"}
- [ "crossvalidation" ].each{ |c| OBJECT_PROPERTY_CLASS[c] = "Crossvalidation"}
-
- # literals point to primitive values (not to other resources)
- # the literal datatype is encoded via uri:
- LITERAL_DATATYPE_STRING = XML["string"].uri
- LITERAL_DATATYPE_URI = XML["anyURI"].uri
- LITERAL_DATATYPE_FLOAT = XML["float"].uri
- LITERAL_DATATYPE_DOUBLE = XML["double"].uri
- LITERAL_DATATYPE_DATE = XML["date"].uri
- LITERAL_DATATYPE_BOOLEAN = XML["boolean"].uri
- LITERAL_DATATYPE_DATETIME = XML["dateTime"].uri
- LITERAL_DATATYPE_INTEGER = XML["integer"].uri
-
- # list all literals (to distinguish from objectProperties) as keys, datatype as values
- # (do not add dc-identifier, deprecated, object are identified via name=uri)
- LITERAL_TYPES = {}
- [ "title", "creator", "format", "description", "hasStatus", "paramScope", "paramValue",
- "classValue", "reportType", "confusionMatrixActual",
- "confusionMatrixPredicted" ].each{ |l| LITERAL_TYPES[l] = LITERAL_DATATYPE_STRING }
- [ "date", "due_to_time" ].each{ |l| LITERAL_TYPES[l] = LITERAL_DATATYPE_DATE }
- [ "percentageCompleted", "truePositiveRate", "fMeasure", "falseNegativeRate",
- "areaUnderRoc", "falsePositiveRate", "trueNegativeRate", "precision", "recall",
- "percentCorrect", "percentIncorrect", "weightedAreaUnderRoc", "numCorrect",
- "percentIncorrect", "percentUnpredicted", "realRuntime",
- "percentWithoutClass", "rootMeanSquaredError", "meanAbsoluteError", "rSquare",
- "targetVarianceActual", "targetVariancePredicted", "sumSquaredError",
- "sampleCorrelationCoefficient" ].each{ |l| LITERAL_TYPES[l] = LITERAL_DATATYPE_DOUBLE }
- [ "numTrueNegatives", "numWithoutClass", "numFalseNegatives", "numTruePositives",
- "numFalsePositives", "numIncorrect", "numInstances", "numUnpredicted",
- "randomSeed", "numFolds", "confusionMatrixValue",
- "crossvalidationFold" ].each{ |l| LITERAL_TYPES[l] = LITERAL_DATATYPE_INTEGER }
- [ "resultURI" ].each{ |l| LITERAL_TYPES[l] = LITERAL_DATATYPE_URI }
- [ "stratified" ].each{ |l| LITERAL_TYPES[l] = LITERAL_DATATYPE_BOOLEAN }
- # some literals can have different types, parse from ruby type
- PARSE_LITERAL_TYPE = "PARSE_LITERAL_TYPE"
- [ "value" ].each{ |l| LITERAL_TYPES[l] = PARSE_LITERAL_TYPE }
-
- # constants for often used redland-resources
- OWL_TYPE_LITERAL = OWL["AnnotationProperty"]
- OWL_TYPE_CLASS = OWL["Class"]
- OWL_TYPE_OBJECT_PROPERTY = OWL["ObjectProperty"]
- RDF_TYPE = RDF['type']
-
- # store redland:resources (=nodes) to:
- # * separate namespaces (OT from RDF and DC)
- # * save time, as generating resources is timeconsuming in redland
- @@nodes = {}
- [ "type", "about"].each{ |l| @@nodes[l] = RDF[l] }
- [ "title", "creator", "date", "format" ].each{ |l| @@nodes[l] = DC[l] }
-
- def node(property)
- raise "can only create node for non-empty-string, but given "+property.class.to_s+" (value: "+
- property.to_s+")" unless property.is_a?(String) and property.size>0
- raise "dc[identifier] deprecated, use owl.uri" if property=="identifier"
- @@nodes[property] = OT[property] unless @@nodes.has_key?(property)
- return @@nodes[property]
- end
-
- # ot_class is the class of the object as string, e.g. "Model","Dataset", ...
- # root_node is the root-object node in the rdf
- # uri the uri of the object
- attr_accessor :ot_class, :root_node, :uri, :model
-
- private
- def initialize
- @model = Redland::Model.new Redland::MemoryStore.new
- end
-
- # build new owl object
- # ot_class is the class of this object, should be a string like "Model", "Task", ...
- # uri is name and identifier of this object
- public
- def self.create( ot_class, uri )
-
- owl = OpenTox::Owl.new
- owl.ot_class = ot_class
- owl.root_node = Redland::Resource.new(uri.to_s.strip)
- owl.set("type",owl.ot_class)
- owl.uri = uri
- owl
- end
-
- # loads owl from data
- def self.from_data(data, base_uri, ot_class)
-
- owl = OpenTox::Owl.new
- parser = Redland::Parser.new
-
- begin
- parser.parse_string_into_model(owl.model, data, base_uri)
-
- # now loading root_node and uri
- owl.model.find(nil, RDF_TYPE, owl.node(ot_class)) do |s,p,o|
- #LOGGER.debug "about statements "+s.to_s+" . "+p.to_s+" -> "+o.to_s
- is_root = true
- owl.model.find(nil, nil, s) do |ss,pp,oo|
- is_root = false
- break
- end
- if is_root
- # handle error if root is already set
- raise "cannot derieve root object from rdf, more than one object specified" if owl.uri
- raise "illegal root node type, no uri specified\n"+data.to_s if s.blank?
- #store root note and uri
- owl.uri = s.uri.to_s
- owl.root_node = s
- end
- end
-
- # handle error if no root node was found
- unless owl.root_node
- types = []
- owl.model.find(nil, RDF_TYPE, nil){ |s,p,o| types << o.to_s }
- raise "root node for class '"+owl.node(ot_class).to_s+"' not found (available type nodes: "+types.inspect+")"
- end
- raise "no uri in rdf: '"+owl.uri+"'" unless owl.uri and Utils.is_uri?(owl.uri)
- owl.ot_class = ot_class
- owl
- rescue => e
- RestClientWrapper.raise_uri_error(e.message, base_uri)
- end
- end
-
- def self.from_uri(uri, ot_class)
- return from_data(RestClientWrapper.get(uri,:accept => "application/rdf+xml").to_s, uri, ot_class)
- end
-
- def rdf
- @model.to_string
- end
-
- # returns the first object for subject:root_node and property
- # (sufficient for accessing simple, root-node properties)
- def get( property )
- raise "uri is no prop, use owl.uri instead" if property=="uri"
- return get_value( @model.object( @root_node, node(property.to_s)) )
- end
-
- # returns an array of objects (not only the first one) that fit for the property
- # accepts array of properties to access not-root-node vaules
- # i.e. validation_owl.get_nested( [ "confusionMatrix", "confusionMatrixCell", "confusionMatrixValue" ]
- # returns an array of all confusionMatrixValues
- def get_nested( property_array )
- n = [ @root_node ]
- property_array.each do |p|
- new_nodes = []
- n.each do |nn|
- @model.find( nn, node(p), nil ) do |sub,pred,obj|
- new_nodes << obj
- end
- end
- n = new_nodes
- end
- return n.collect{|nn| get_value( nn )}
- end
-
- private
- # returns node-value
- def get_value( node )
- return nil unless node
- if node.is_a?(Redland::Literal)
- return node.get_value
- elsif node.blank?
- return nil
- else
- return node.uri.to_s
- end
- end
-
- public
- # sets values of current_node (by default root_node)
- #
- # note: this does not delete existing triples
- # * there can be several triples for the same subject and predicate
- # ( e.g. after set("description","bla1") and set("description","bla2")
- # both descriptions are in the model,
- # but the get("description") will give you only one object (by chance)
- # * this does not matter in pratice (only dataset uses this -> load_dataset-methods)
- # * identical values appear only once in rdf
- def set(predicate, object, current_node=@root_node )
-
- pred = predicate.to_s
- raise "uri is no prop, cannot set uri" if pred=="uri"
- raise "dc[identifier] deprecated, use owl.uri" if pred=="identifier"
- if (object.is_a?(Redland::Node) and object.blank?) or nil==object or object.to_s.size==0
- # set only not-nil values
- LOGGER.warn "skipping (not setting) empty value in rdf for property: '"+pred+"'"
- return
- end
-
- if pred=="type"
- # predicate is type, set class of current node
- set_type(object, current_node)
- elsif LITERAL_TYPES.has_key?(pred)
- # predicate is literal
- set_literal(pred,object,LITERAL_TYPES[pred],current_node)
- elsif OBJECT_PROPERTY_CLASS.has_key?(pred)
- # predicte is objectProperty, object is another resource
- set_object_property(pred,object,OBJECT_PROPERTY_CLASS[pred],current_node)
- else
- raise "unkonwn rdf-property, please add: '"+pred+"' to OpenTox::OWL.OBJECT_PROPERTY_CLASS or OpenTox::OWL.LITERAL_TYPES"
- end
- end
-
- # example-triples for setting rdf-type to model:
- # model_xy,rdf:type,ot:Model
- # ot:Model,rdf:type,owl:Class
- def set_type(ot_class, current_node=@root_node)
- @model.add current_node, RDF_TYPE, node(ot_class)
- @model.add node(ot_class), RDF_TYPE, OWL_TYPE_CLASS
- end
-
- # example-triples for setting description of a model:
- # model_xy,ot:description,bla..bla^^xml:string
- # ot:description,rdf:type,owl:Literal
- def set_literal(literal_name, literal_value, literal_datatype, current_node=@root_node)
- @model.add current_node, node(literal_name), Redland::Literal.create(literal_value, literal_datatype)
- @model.add node(literal_name), RDF_TYPE, OWL_TYPE_LITERAL
- end
-
- # example-triples for setting algorithm property of a model:
- # model_xy,ot:algorithm,algorihtm_xy
- # ot:algorithm,rdf:type,owl:ObjectProperty
- # algorihtm_xy,rdf:type,ot:Algorithm
- # ot:Algorithm,rdf:type,owl:Class
- def set_object_property(property, object, object_class, current_node=@root_node)
- object_node = Redland::Resource.new(object)
- @model.add current_node, node(property), object_node
- @model.add node(property), RDF_TYPE, OWL_TYPE_OBJECT_PROPERTY
- @model.add object_node, RDF_TYPE, node(object_class)
- @model.add node(object_class), RDF_TYPE, OWL_TYPE_CLASS
- end
-
- # this is (a recursiv method) to set nested-data via hashes (not only simple properties)
- # example (for a dataset)
- # { :description => "bla",
- # :dataEntry => { :compound => "compound_uri",
- # :values => [ { :class => "FeatureValue"
- # :feature => "feat1",
- # :value => 42 },
- # { :class => "FeatureValue"
- # :feature => "feat2",
- # :value => 123 } ] } }
- def set_data(hash, current_node=@root_node)
-
- hash.each do |k,v|
- if v.is_a?(Hash)
- # value is again a hash
- prop = k.to_s
-
- # :class is a special key to specify the class value, if not defined in OBJECT_PROPERTY_CLASS
- object_class = v.has_key?(:class) ? v.delete(:class) : OBJECT_PROPERTY_CLASS[prop]
- raise "hash key must be a object-property, please add '"+prop.to_s+
- "' to OpenTox::OWL.OBJECT_PROPERTY_CLASS or specify :class value" unless object_class
-
- # the new node is a class node, to specify the uri of the resource use key :uri
- if v[:uri]
- # identifier is either a specified uri
- class_node = Redland::Resource.new(v.delete(:uri))
- else
- # or a new uri, make up internal uri with increment
- class_node = new_class_node(object_class,current_node)
- end
- set_object_property(prop,class_node,object_class,current_node)
- # recursivly call set_data method with new node
- set_data(v,class_node)
- elsif v.is_a?(Array)
- # value is an array, each array element is added with current key as predicate
- v.each do |value|
- set_data( { k => value }, current_node )
- end
- else
- # neither hash nor array, call simple set-method
- set( k, v, current_node )
- end
- end
- end
-
- # create a new (internal class) node with unique, uri-like name
- def new_class_node(name, current_node=@root_node)
- # to avoid anonymous nodes, make up uris for sub-objects
- # use counter to make sure each uri is unique
- # for example we will get ../confusion_matrix_cell/1, ../confusion_matrix_cell/2, ...
- count = 1
- while (true)
- res = Redland::Resource.new( File.join(current_node.uri.to_s,name.to_s,count.to_s) )
- match = false
- @model.find(nil, nil, res) do |s,p,o|
- match = true
- break
- end
- if match
- count += 1
- else
- break
- end
- end
- return res
- end
-
- # for "backwards-compatiblity"
- # better use directly:
- # set_data( { "parameters" => [ { "title" => <t>, "paramScope" => <s>, "paramValue" => <v> } ] )
- def parameters=(params)
-
- converted_params = []
- params.each do |name, settings|
- converted_params << { :title => name, :paramScope => settings[:scope], :paramValue => settings[:value] }
- end
- set_data( :parameters => converted_params )
- end
-
- # PENDING move to dataset.rb
- # this is for dataset.to_owl
- # adds feautre value for a single compound
- def add_data_entries(compound_uri,features)
-
- data_entry = { :compound => compound_uri }
- if features
- feature_values = []
- features.each do |f|
- f.each do |feature_uri,value|
- if value.is_a?(Hash)
- complex_values = []
- value.each do |uri,v|
- complex_values << { :feature => uri, :value => v }
- end
- feature_values << { :class => "Tuple", :feature => feature_uri, :complexValue => complex_values }
- else
- feature_values << { :class => "FeatureValue", :feature => feature_uri, :value => value }
- end
- end
- end
- data_entry[:values] = feature_values
- end
- set_data( :dataEntry => data_entry )
- end
-
- # PENDING move to dataset.rb
- # feature values are not loaded for performance reasons
- # loading compounds and features into arrays that are given as params
- def load_dataset( compounds, features )
-
- @model.subjects(RDF_TYPE, node('Compound')).each do |compound|
- compounds << get_value(compound)
- end
-
- @model.subjects(RDF_TYPE, node('Feature')).each do |feature|
- feature_value_found=false
- @model.find(nil, node("feature"), feature) do |potential_feature_value,p,o|
- @model.find(nil, node("values"), potential_feature_value) do |s,p,o|
- feature_value_found=true
- break
- end
- break if feature_value_found
- end
- features << get_value(feature) if feature_value_found
- end
- LOGGER.debug "loaded "+compounds.size.to_s+" compounds and "+features.size.to_s+" features from dataset "+uri.to_s
- end
-
- # PENDING move to dataset.rb
- # loading feature values for the specified feature
- # if feature is nil, all feature values are loaded
- #
- # general remark on the rdf loading (found out with some testing):
- # the search methods (subjects/find) are fast, the time consuming parts is creating resources,
- # which cannot be avoided in general
- def load_dataset_feature_values( compounds, data, feature_uris )
-
- raise "no feature-uri array" unless feature_uris.is_a?(Array)
-
- # values are stored in the data-hash, hash has a key for each compound
- compounds.each{|c| data[c] = [] unless data[c]}
-
- count = 0
-
- feature_uris.each do |feature_uri|
- LOGGER.debug("load feature values for feature: "+feature_uri )
- feature_node = Redland::Resource.new(feature_uri)
-
- # search for all feature_value_node with property 'ot_feature' and the feature we are looking for
- @model.find(nil, node('feature'), feature_node) do |feature_value_node,p,o|
-
- # get compound_uri by "backtracking" to values node (property is 'values'), then get compound_node via 'compound'
- value_nodes = @model.subjects(node('values'),feature_value_node)
- if value_nodes.size>0
- raise "more than one value node "+value_nodes.size.to_s if value_nodes.size>1
- value_node = value_nodes[0]
-
- compound_uri = get_value( @model.object(value_node, node('compound')) )
- unless compound_uri
- LOGGER.warn "'compound' missing for data-entry of feature "+feature_uri.to_s+
- ", value: "+@model.object(feature_value_node,node("value")).to_s
- next
- end
-
- value_node_type = @model.object(feature_value_node, RDF_TYPE)
- if (value_node_type == node('FeatureValue'))
- value_literal = @model.object( feature_value_node, node('value'))
- raise "plain feature value no literal: "+value_literal.to_s unless value_literal.is_a?(Redland::Literal)
- data[compound_uri] << {feature_uri => value_literal.get_value }
- elsif (value_node_type == node('Tuple'))
- complex_values = {}
- @model.find(feature_value_node,node('complexValue'),nil) do |p,s,complex_value|
- complex_value_type = @model.object(complex_value, RDF_TYPE)
- raise "complex feature value no feature value: "+complex_value.to_s unless complex_value_type==node('FeatureValue')
- complex_feature_uri = get_value(@model.object( complex_value, node('feature')))
- complex_value = @model.object( complex_value, node('value'))
- raise "complex value no literal: "+complex_value.to_s unless complex_value.is_a?(Redland::Literal)
- complex_values[ complex_feature_uri ] = complex_value.get_value
- end
- data[compound_uri] << { feature_uri => complex_values } if complex_values.size>0
- end
- count += 1
- LOGGER.debug "loading feature values ("+count.to_s+")" if (count%1000 == 0)
- end
- end
- LOGGER.debug "loaded "+count.to_s+" feature values for feature "+feature_node.to_s
- end
- end
- end
-end
diff --git a/lib/parser.rb b/lib/parser.rb
new file mode 100644
index 0000000..f33017d
--- /dev/null
+++ b/lib/parser.rb
@@ -0,0 +1,381 @@
+require 'spreadsheet'
+require 'roo'
+
+class String
+
+ # Split RDF statement into triples
+ # @return [Array] Array with [subject,predicate,object]
+ def to_triple
+ self.chomp.split(' ',3).collect{|i| i.sub(/\s+.$/,'').gsub(/[<>"]/,'')}
+ end
+
+end
+
+module OpenTox
+
+ # Parser for various input formats
+ module Parser
+
+ # OWL-DL parser
+ module Owl
+
+ # Create a new OWL-DL parser
+ # @param uri URI of OpenTox object
+ # @return [OpenTox::Parser::Owl] OWL-DL parser
+ def initialize(uri)
+ @uri = uri
+ @metadata = {}
+ end
+
+ # Read metadata from opentox service
+ # @return [Hash] Object metadata
+ def load_metadata(subjectid=nil)
+ # avoid using rapper directly because of 2 reasons:
+ # * http errors wont be noticed
+ # * subjectid cannot be sent as header
+ ##uri += "?subjectid=#{CGI.escape(subjectid)}" if subjectid
+ ## `rapper -i rdfxml -o ntriples #{uri} 2>/dev/null`.each_line do |line|
+ if File.exist?(@uri)
+ file = File.new(@uri)
+ else
+ file = Tempfile.new("ot-rdfxml")
+ if @dataset
+ # do not concat /metadata to uri string, this would not work for dataset/R401577?max=3
+ uri = URI::parse(@uri)
+ uri.path = File.join(uri.path,"metadata")
+ uri = uri.to_s
+ else
+ uri = @uri
+ end
+ file.puts OpenTox::RestClientWrapper.get uri,{:subjectid => subjectid,:accept => "application/rdf+xml"},nil,false
+ file.close
+ to_delete = file.path
+ end
+ statements = []
+ parameter_ids = []
+ `rapper -i rdfxml -o ntriples #{file.path} 2>/dev/null`.each_line do |line|
+ triple = line.to_triple
+ @metadata[triple[1]] = triple[2].split('^^').first if triple[0] == @uri and triple[1] != RDF['type']
+ statements << triple
+ parameter_ids << triple[2] if triple[1] == OT.parameters
+ end
+ File.delete(to_delete) if to_delete
+ unless parameter_ids.empty?
+ @metadata[OT.parameters] = []
+ parameter_ids.each do |p|
+ parameter = {}
+ statements.each{ |t| parameter[t[1]] = t[2] if t[0] == p and t[1] != RDF['type']}
+ @metadata[OT.parameters] << parameter
+ end
+ end
+ @metadata
+ end
+
+ # creates owl object from rdf-data
+ # @param [String] rdf
+ # @param [String] type of the info (e.g. OT.Task, OT.ErrorReport) needed to get the subject-uri
+ # @return [Owl] with uri and metadata set
+ def self.from_rdf( rdf, type )
+ # write to file and read convert with rapper into tripples
+ file = Tempfile.new("ot-rdfxml")
+ file.puts rdf
+ file.close
+ #puts "cmd: rapper -i rdfxml -o ntriples #{file} 2>/dev/null"
+ triples = `rapper -i rdfxml -o ntriples #{file.path} 2>/dev/null`
+
+ # load uri via type
+ uri = nil
+ triples.each_line do |line|
+ triple = line.to_triple
+ if triple[1] == RDF['type'] and triple[2]==type
+ raise "uri already set, two uris found with type: "+type.to_s if uri
+ uri = triple[0]
+ end
+ end
+ File.delete(file.path)
+ # load metadata
+ metadata = {}
+ triples.each_line do |line|
+ triple = line.to_triple
+ metadata[triple[1]] = triple[2].split('^^').first if triple[0] == uri and triple[1] != RDF['type']
+ end
+ owl = Owl::Generic.new(uri)
+ owl.metadata = metadata
+ owl
+ end
+
+ # Generic parser for all OpenTox classes
+ class Generic
+ include Owl
+
+ attr_accessor :uri, :metadata
+ end
+
+ # OWL-DL parser for datasets
+ class Dataset
+
+ include Owl
+
+ attr_writer :uri
+
+ # Create a new OWL-DL dataset parser
+ # @param uri Dataset URI
+ # @return [OpenTox::Parser::Owl::Dataset] OWL-DL parser
+ def initialize(uri, subjectid=nil)
+ super uri
+ @dataset = ::OpenTox::Dataset.new(@uri, subjectid)
+ end
+
+ # Read data from dataset service. Files can be parsed by setting #uri to a filename (after initialization with a real URI)
+ # @example Read data from an external service
+ # parser = OpenTox::Parser::Owl::Dataaset.new "http://wwbservices.in-silico.ch/dataset/1"
+ # dataset = parser.load_uri
+ # @example Create dataset from RDF/XML file
+ # dataset = OpenTox::Dataset.create
+ # parser = OpenTox::Parser::Owl::Dataaset.new dataset.uri
+ # parser.uri = "dataset.rdfxml" # insert your input file
+ # dataset = parser.load_uri
+ # dataset.save
+ # @return [Hash] Internal dataset representation
+ def load_uri(subjectid=nil)
+
+ # avoid using rapper directly because of 2 reasons:
+ # * http errors wont be noticed
+ # * subjectid cannot be sent as header
+ ##uri += "?subjectid=#{CGI.escape(subjectid)}" if subjectid
+ ##`rapper -i rdfxml -o ntriples #{file} 2>/dev/null`.each_line do |line|
+ if File.exist?(@uri)
+ file = File.new(@uri)
+ else
+ file = Tempfile.new("ot-rdfxml")
+ file.puts OpenTox::RestClientWrapper.get @uri,{:subjectid => subjectid,:accept => "application/rdf+xml"},nil,false
+ file.close
+ to_delete = file.path
+ end
+
+ data = {}
+ feature_values = {}
+ feature = {}
+ other_statements = {}
+ `rapper -i rdfxml -o ntriples #{file.path} 2>/dev/null`.each_line do |line|
+ triple = line.chomp.split(' ',3)
+ triple = triple[0..2].collect{|i| i.sub(/\s+.$/,'').gsub(/[<>"]/,'')}
+ case triple[1]
+ when /#{OT.values}/i
+ data[triple[0]] = {:compound => "", :values => []} unless data[triple[0]]
+ data[triple[0]][:values] << triple[2]
+ when /#{OT.value}/i
+ feature_values[triple[0]] = triple[2]
+ when /#{OT.compound}/i
+ data[triple[0]] = {:compound => "", :values => []} unless data[triple[0]]
+ data[triple[0]][:compound] = triple[2]
+ when /#{OT.feature}/i
+ feature[triple[0]] = triple[2]
+ when /#{RDF.type}/i
+ if triple[2]=~/#{OT.Compound}/i and !data[triple[0]]
+ data[triple[0]] = {:compound => triple[0], :values => []}
+ end
+ else
+ end
+ end
+ File.delete(to_delete) if to_delete
+ data.each do |id,entry|
+ if entry[:values].size==0
+ # no feature values add plain compounds
+ @dataset.add_compound(entry[:compound])
+ else
+ entry[:values].each do |value_id|
+ split = feature_values[value_id].split(/\^\^/)
+ case split[-1]
+ when XSD.double, XSD.float
+ value = split.first.to_f
+ when XSD.boolean
+ value = split.first=~/(?i)true/ ? true : false
+ else
+ value = split.first
+ end
+ @dataset.add entry[:compound],feature[value_id],value
+ end
+ end
+ end
+ load_features subjectid
+ @dataset.metadata = load_metadata(subjectid)
+ @dataset
+ end
+
+ # Read only features from a dataset service.
+ # @return [Hash] Internal features representation
+ def load_features(subjectid=nil)
+ if File.exist?(@uri)
+ file = File.new(@uri)
+ else
+ file = Tempfile.new("ot-rdfxml")
+ # do not concat /features to uri string, this would not work for dataset/R401577?max=3
+ uri = URI::parse(@uri)
+ uri.path = File.join(uri.path,"features")
+ uri = uri.to_s
+ file.puts OpenTox::RestClientWrapper.get uri,{:subjectid => subjectid,:accept => "application/rdf+xml"},nil,false
+ file.close
+ to_delete = file.path
+ end
+ statements = []
+ features = Set.new
+ `rapper -i rdfxml -o ntriples #{file.path} 2>/dev/null`.each_line do |line|
+ triple = line.chomp.split('> ').collect{|i| i.sub(/\s+.$/,'').gsub(/[<>"]/,'')}[0..2]
+ statements << triple
+ features << triple[0] if triple[1] == RDF['type'] and (triple[2] == OT.Feature || triple[2] == OT.NumericFeature)
+ end
+ File.delete(to_delete) if to_delete
+ statements.each do |triple|
+ if features.include? triple[0]
+ @dataset.features[triple[0]] = {} unless @dataset.features[triple[0]]
+ @dataset.features[triple[0]][triple[1]] = triple[2].split('^^').first
+ end
+ end
+ @dataset.features
+ end
+
+ end
+
+ end
+
+ # Parser for getting spreadsheet data into a dataset
+ class Spreadsheets
+
+ attr_accessor :dataset
+
+ def initialize
+ @data = []
+ @features = []
+ @feature_types = {}
+
+ @format_errors = ""
+ @smiles_errors = []
+ @activity_errors = []
+ @duplicates = {}
+ end
+
+ # Load Spreadsheet book (created with roo gem http://roo.rubyforge.org/, excel format specification: http://toxcreate.org/help)
+ # @param [Excel] book Excel workbook object (created with roo gem)
+ # @return [OpenTox::Dataset] Dataset object with Excel data
+ def load_spreadsheet(book)
+ book.default_sheet = 0
+ add_features book.row(1)
+ 2.upto(book.last_row) { |i| add_values book.row(i) }
+ warnings
+ @dataset
+ end
+
+ # Load CSV string (format specification: http://toxcreate.org/help)
+ # @param [String] csv CSV representation of the dataset
+ # @return [OpenTox::Dataset] Dataset object with CSV data
+ def load_csv(csv)
+ row = 0
+ input = csv.split("\n")
+ add_features split_row(input.shift)
+ input.each { |row| add_values split_row(row) }
+ warnings
+ @dataset
+ end
+
+ private
+
+ def warnings
+
+ info = ''
+ @feature_types.each do |feature,types|
+ if types.uniq.size > 1
+ type = OT.NumericFeature
+ else
+ type = types.first
+ end
+ @dataset.add_feature_metadata(feature,{OT.isA => type})
+ info += "\"#{@dataset.feature_name(feature)}\" detected as #{type.split('#').last}."
+
+ # TODO: rewrite feature values
+ # TODO if value.to_f == 0 @activity_errors << "#{smiles} Zero values not allowed for regression datasets - entry ignored."
+ end
+
+ @dataset.metadata[OT.Info] = info
+
+ warnings = ''
+ warnings += "<p>Incorrect Smiles structures (ignored):</p>" + @smiles_errors.join("<br/>") unless @smiles_errors.empty?
+ warnings += "<p>Irregular activities (ignored):</p>" + @activity_errors.join("<br/>") unless @activity_errors.empty?
+ duplicate_warnings = ''
+ @duplicates.each {|inchi,lines| duplicate_warnings << "<p>#{lines.join('<br/>')}</p>" if lines.size > 1 }
+ warnings += "<p>Duplicated structures (all structures/activities used for model building, please make sure, that the results were obtained from <em>independent</em> experiments):</p>" + duplicate_warnings unless duplicate_warnings.empty?
+
+ @dataset.metadata[OT.Warnings] = warnings
+
+ end
+
+ def add_features(row)
+ row.shift # get rid of smiles entry
+ row.each do |feature_name|
+ feature_uri = File.join(@dataset.uri,"feature",URI.encode(feature_name))
+ @feature_types[feature_uri] = []
+ @features << feature_uri
+ @dataset.add_feature(feature_uri,{DC.title => feature_name})
+ end
+ end
+
+ def add_values(row)
+
+ smiles = row.shift
+ compound = Compound.from_smiles(smiles)
+ if compound.nil? or compound.inchi.nil? or compound.inchi == ""
+ @smiles_errors << smiles+", "+row.join(", ")
+ return false
+ end
+ @duplicates[compound.inchi] = [] unless @duplicates[compound.inchi]
+ @duplicates[compound.inchi] << smiles+", "+row.join(", ")
+
+ row.each_index do |i|
+ value = row[i]
+ feature = @features[i]
+ type = feature_type(value)
+
+ @feature_types[feature] << type
+
+ case type
+ when OT.NominalFeature
+ case value.to_s
+ when TRUE_REGEXP
+ @dataset.add(compound.uri, feature, true )
+ when FALSE_REGEXP
+ @dataset.add(compound.uri, feature, false )
+ end
+ when OT.NumericFeature
+ @dataset.add compound.uri, feature, value.to_f
+ when OT.StringFeature
+ @dataset.add compound.uri, feature, value.to_s
+ @activity_errors << smiles+", "+row.join(", ")
+ end
+ end
+ end
+
+ def numeric?(value)
+ true if Float(value) rescue false
+ end
+
+ def classification?(value)
+ !value.to_s.strip.match(TRUE_REGEXP).nil? or !value.to_s.strip.match(FALSE_REGEXP).nil?
+ end
+
+ def feature_type(value)
+ if classification? value
+ return OT.NominalFeature
+ elsif numeric? value
+ return OT.NumericFeature
+ else
+ return OT.StringFeature
+ end
+ end
+
+ def split_row(row)
+ row.chomp.gsub(/["']/,'').split(/\s*[,;]\s*/) # remove quotes
+ end
+
+ end
+ end
+end
diff --git a/lib/policy.rb b/lib/policy.rb
new file mode 100644
index 0000000..8591d52
--- /dev/null
+++ b/lib/policy.rb
@@ -0,0 +1,261 @@
+module OpenTox
+ require "rexml/document"
+
+ #Module for policy-processing
+ # @see also http://www.opentox.org/dev/apis/api-1.2/AA for opentox API specs
+ # Class Policies corresponds to <policies> container of an xml-policy-fle
+ class Policies
+
+ attr_accessor :name, :policies
+
+ def initialize()
+ @policies = {}
+ end
+
+ #create new policy instance with name
+ # @param [String]name of the policy
+ def new_policy(name)
+ @policies[name] = Policy.new(name)
+ end
+
+ #drop a specific policy in a policies instance
+ # @param [String]name of the policy
+ # @return [Boolean]
+ def drop_policy(name)
+ return true if @policies.delete(name)
+ end
+
+ #drop all policies in a policies instance
+ def drop_policies
+ @policies.each do |name, policy|
+ drop_policy(name)
+ 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
+ out = []
+ @policies.each do |name, policy|
+ out << name
+ end
+ return out
+ end
+
+ #loads a default policy template in policies instance
+ def load_default_policy(user, uri, group="member")
+ template = case user
+ when "guest", "anonymous" then "default_guest_policy"
+ else "default_policy"
+ end
+ xml = File.read(File.join(File.dirname(__FILE__), "templates/#{template}.xml"))
+ self.load_xml(xml)
+ datestring = Time.now.strftime("%Y-%m-%d-%H-%M-%S-x") + rand(1000).to_s
+
+ @policies["policy_user"].name = "policy_user_#{user}_#{datestring}"
+ @policies["policy_user"].rules["rule_user"].uri = uri
+ @policies["policy_user"].rules["rule_user"].name = "rule_user_#{user}_#{datestring}"
+ @policies["policy_user"].subjects["subject_user"].name = "subject_user_#{user}_#{datestring}"
+ @policies["policy_user"].subjects["subject_user"].value = "uid=#{user},ou=people,dc=opentox,dc=org"
+ @policies["policy_user"].subject_group = "subjects_user_#{user}_#{datestring}"
+
+ @policies["policy_group"].name = "policy_group_#{group}_#{datestring}"
+ @policies["policy_group"].rules["rule_group"].uri = uri
+ @policies["policy_group"].rules["rule_group"].name = "rule_group_#{group}_#{datestring}"
+ @policies["policy_group"].subjects["subject_group"].name = "subject_group_#{group}_#{datestring}"
+ @policies["policy_group"].subjects["subject_group"].value = "cn=#{group},ou=groups,dc=opentox,dc=org"
+ @policies["policy_group"].subject_group = "subjects_#{group}_#{datestring}"
+ return true
+ end
+
+ #loads a xml template
+ def load_xml(xml)
+ rexml = REXML::Document.new(xml)
+ rexml.elements.each("Policies/Policy") do |pol| #Policies
+ policy_name = pol.attributes["name"]
+ new_policy(policy_name)
+ #@policies[policy_name] = Policy.new(policy_name)
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Rule") do |r| #Rules
+ rule_name = r.attributes["name"]
+ uri = rexml.elements["Policies/Policy[@name='#{policy_name}']/Rule[@name='#{rule_name}']/ResourceName"].attributes["name"]
+ @policies[policy_name].rules[rule_name] = @policies[policy_name].new_rule(rule_name, uri)
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Rule[@name='#{rule_name}']/AttributeValuePair") do |attribute_pairs|
+ action=nil; value=nil;
+ attribute_pairs.each_element do |elem|
+ action = elem.attributes["name"] if elem.attributes["name"]
+ value = elem.text if elem.text
+ end
+ if action and value
+ case action
+ when "GET"
+ @policies[policy_name].rules[rule_name].get = value
+ when "POST"
+ @policies[policy_name].rules[rule_name].post = value
+ when "PUT"
+ @policies[policy_name].rules[rule_name].put = value
+ when "DELETE"
+ @policies[policy_name].rules[rule_name].delete = value
+ end
+ end
+ end
+ end
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Subjects") do |subjects| #Subjects
+ @policies[policy_name].subject_group = subjects.attributes["name"]
+ rexml.elements.each("Policies/Policy[@name='#{policy_name}']/Subjects[@name='#{@policies[policy_name].subject_group}']/Subject") do |s| #Subject
+ subject_name = s.attributes["name"]
+ subject_type = s.attributes["type"]
+ subject_value = rexml.elements["Policies/Policy[@name='#{policy_name}']/Subjects[@name='#{@policies[policy_name].subject_group}']/Subject[@name='#{subject_name}']/AttributeValuePair/Value"].text
+ @policies[policy_name].new_subject(subject_name, subject_type, subject_value) if subject_name and subject_type and subject_value
+ end
+ end
+ end
+ end
+
+ #generates xml from policies instance
+ def to_xml
+ doc = REXML::Document.new()
+ doc << REXML::DocType.new("Policies", "PUBLIC \"-//Sun Java System Access Manager7.1 2006Q3\n Admin CLI DTD//EN\" \"jar://com/sun/identity/policy/policyAdmin.dtd\"")
+ doc.add_element(REXML::Element.new("Policies"))
+
+ @policies.each do |name, pol|
+ policy = REXML::Element.new("Policy")
+ policy.attributes["name"] = pol.name
+ policy.attributes["referralPolicy"] = false
+ policy.attributes["active"] = true
+ @policies[name].rules.each do |r,rl|
+ rule = @policies[name].rules[r]
+ out_rule = REXML::Element.new("Rule")
+ out_rule.attributes["name"] = rule.name
+ servicename = REXML::Element.new("ServiceName")
+ servicename.attributes["name"]="iPlanetAMWebAgentService"
+ out_rule.add_element(servicename)
+ rescourcename = REXML::Element.new("ResourceName")
+ rescourcename.attributes["name"] = rule.uri
+ out_rule.add_element(rescourcename)
+
+ ["get","post","delete","put"].each do |act|
+ if rule.method(act).call
+ attribute = REXML::Element.new("Attribute")
+ attribute.attributes["name"] = act.upcase
+ attributevaluepair = REXML::Element.new("AttributeValuePair")
+ attributevaluepair.add_element(attribute)
+ attributevalue = REXML::Element.new("Value")
+ attributevaluepair.add_element(attributevalue)
+ attributevalue.add_text REXML::Text.new(rule.method(act).call)
+ out_rule.add_element(attributevaluepair)
+
+ end
+ end
+ policy.add_element(out_rule)
+ end
+
+ subjects = REXML::Element.new("Subjects")
+ subjects.attributes["name"] = pol.subject_group
+ subjects.attributes["description"] = ""
+ @policies[name].subjects.each do |subj, subjs|
+ subject = REXML::Element.new("Subject")
+ subject.attributes["name"] = pol.subjects[subj].name
+ subject.attributes["type"] = pol.subjects[subj].type
+ subject.attributes["includeType"] = "inclusive"
+ attributevaluepair = REXML::Element.new("AttributeValuePair")
+ attribute = REXML::Element.new("Attribute")
+ attribute.attributes["name"] = "Values"
+ attributevaluepair.add_element(attribute)
+ attributevalue = REXML::Element.new("Value")
+ attributevalue.add_text REXML::Text.new(pol.subjects[subj].value)
+ attributevaluepair.add_element(attributevalue)
+ subject.add_element(attributevaluepair)
+ subjects.add_element(subject)
+ end
+ policy.add_element(subjects)
+ doc.root.add_element(policy)
+ end
+ out = ""
+ doc.write(out, 2)
+ return out
+ end
+
+ end
+
+ #single policy in a policies instance
+ class Policy
+
+ attr_accessor :name, :rules, :subject_group, :subjects
+
+ def initialize(name)
+ @name = name
+ @rules = {}
+ @subject_group = ""
+ @subjects = {}
+ end
+
+ #create a new rule instance for the policy
+ def new_rule(name, uri)
+ @rules[name] = Rule.new(name, uri)
+ end
+
+ #create a new subject instance for the policy
+ def new_subject(name, type, value)
+ @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
+
+ attr_accessor :name, :uri, :get, :post, :put, :delete
+
+ def initialize(name, uri)
+ @name = name
+ @uri = uri
+ end
+
+ def rename(new, old)
+ self[new] = self.delete(old)
+ self[new].name = new
+ end
+
+ def get=(value)
+ @get = check_value(value, @get)
+ end
+
+ def post=(value)
+ @post = check_value(value, @post)
+ end
+
+ def delete=(value)
+ @delete = check_value(value, @delete)
+ end
+
+ def put=(value)
+ @put = check_value(value, @put)
+ end
+
+ private
+ #checks if value is allow or deny. returns old value if not valid.
+ def check_value(new_value, old_value)
+ return (new_value=="allow" || new_value=="deny" || new_value==nil) ? new_value : old_value
+ end
+ end
+
+ class Subject
+
+ attr_accessor :name, :type, :value
+
+ def initialize(name, type, value)
+ @name = name
+ @type = type
+ @value = value
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/rest_client_wrapper.rb b/lib/rest_client_wrapper.rb
index 1282bee..dac24dc 100644
--- a/lib/rest_client_wrapper.rb
+++ b/lib/rest_client_wrapper.rb
@@ -1,34 +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
@@ -36,45 +6,80 @@ 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={}, waiting_task=nil, wait=true )
+ execute( "get", uri, nil, headers, 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,String] payload data posted to the service
+ # @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.post(uri, payload=nil, headers={}, waiting_task=nil, wait=true )
+ execute( "post", uri, payload, headers, waiting_task, wait )
end
- def self.put(uri, headers, payload=nil )
- execute( "put", uri, headers, payload )
+ # 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, payload=nil, headers={} )
+ execute( "put", uri, payload, headers )
end
- def self.delete(uri, headers=nil)
- execute( "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, nil, headers)
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, payload=nil, headers={}, 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 Utils.is_uri?(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 "headers are no hash: "+headers.inspect unless headers==nil or headers.is_a?(Hash)
+ raise OpenTox::BadRequestError.new "accept should go into the headers" if payload and payload.is_a?(Hash) and payload[:accept]
+ raise OpenTox::BadRequestError.new "content_type should go into the headers" if payload and payload.is_a?(Hash) and payload[:content_type]
+ raise "__waiting_task__ must be 'nil' or '(sub)task', is "+waiting_task.class.to_s if
+ waiting_task!=nil and !(waiting_task.is_a?(Task) || waiting_task.is_a?(SubTask))
headers.each{ |k,v| headers.delete(k) if v==nil } if headers #remove keys with empty values, as this can cause problems
+ ## PENDING partner services accept subjectid only in header
+ headers = {} unless headers
+ headers[:subjectid] = payload.delete(:subjectid) if payload and payload.is_a?(Hash) and payload.has_key?(:subjectid)
+
+ # PENDING needed for NUTA, until we finally agree on how to send subjectid
+ headers[:subjectid] = payload.delete(:subjectid) if uri=~/ntua/ and payload and payload.is_a?(Hash) and payload.has_key?(:subjectid)
begin
- #LOGGER.debug "RestCall: "+rest_call.to_s+" "+uri.to_s+" "+headers.inspect
- resource = RestClient::Resource.new(uri,{:timeout => 60}) #, :user => @@users[:users].keys[0], :password => @@users[:users].values[0]})
- if payload
+ #LOGGER.debug "RestCall: "+rest_call.to_s+" "+uri.to_s+" "+headers.inspect+" "+payload.inspect
+ resource = RestClient::Resource.new(uri,{:timeout => 60})
+ if rest_call=="post" || rest_call=="put"
result = resource.send(rest_call, payload, headers)
- elsif headers
- result = resource.send(rest_call, headers)
else
- result = resource.send(rest_call)
+ result = resource.send(rest_call, headers)
end
+ #LOGGER.debug "result body size: #{result.body.size}"
+
+ # PENDING NTUA does return errors with 200
+ raise RestClient::ExceptionWithResponse.new(result) if uri=~/ntua/ and result.body =~ /about.*http:\/\/anonymous.org\/error/
# result is a string, with the additional fields content_type and code
res = WrapperResult.new(result.body)
@@ -82,86 +87,92 @@ module OpenTox
raise "content-type not set" unless res.content_type
res.code = result.code
+ # 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, :payload => payload}
+ rescue Errno::ECONNREFUSED => ex
+ received_error ex.message, 500, nil, {:rest_uri => uri, :headers => headers, :payload => payload}
+ 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, :payload => payload}
+ 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 )
+ #TODO remove TUM hack
+ res.content_type = "text/uri-list" if base_uri =~/tu-muenchen/ and res.content_type == "application/x-www-form-urlencoded;charset=UTF-8"
+
task = nil
case res.content_type
- when /application\/rdf\+xml|application\/x-yaml/
- task = OpenTox::Task.from_data(res, res.content_type, res.code, base_uri)
+ when /application\/rdf\+xml/
+ task = OpenTox::Task.from_rdfxml(res)
+ when /yaml/
+ 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 Utils.task_uri?(res)
+ 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.chomp) if res.to_s.uri?
else
- raise "unknown content-type for task: '"+res.content_type.to_s+"'" #+"' content: "+res[0..200].to_s
+ raise "unknown content-type for task : '"+res.content_type.to_s+"'"+" base-uri: "+base_uri.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
-
- res = WrapperResult.new task.resultURI
+ 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.as_task 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.as_task
- #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
new file mode 100644
index 0000000..44b4414
--- /dev/null
+++ b/lib/serializer.rb
@@ -0,0 +1,469 @@
+require 'spreadsheet'
+require 'yajl'
+
+module OpenTox
+
+ # Serialzer for various oputput formats
+ module Serializer
+
+ # OWL-DL Serializer, modelled according to to http://n2.talis.com/wiki/RDF_JSON_Specification
+ class Owl
+
+ attr_accessor :object
+
+ def initialize
+
+ @object = {
+ # 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'] }] } ,
+ OT.NumericFeature => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } ,
+ OT.StringFeature => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } ,
+ OT.Dataset => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } ,
+ OT.DataEntry => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } ,
+ OT.FeatureValue => { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } ,
+ 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 }] } ,
+ OT.dataEntry => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } ,
+ OT.acceptValue => { RDF["type"] => [{ "type" => "uri", "value" => OWL.ObjectProperty }] } ,
+ 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 }] } ,
+ DC.contributor => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } ,
+ DC.creator => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } ,
+ DC.description => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } ,
+ DC.date => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } ,
+ OT.isA => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } ,
+ OT.Warnings => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } ,
+ XSD.anyURI => { RDF["type"] => [{ "type" => "uri", "value" => OWL.AnnotationProperty }] } ,
+ 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 }] } ,
+ OT.paramScope => { RDF["type"] => [{ "type" => "uri", "value" => OWL.DatatypeProperty }] } ,
+ OT.paramValue => { RDF["type"] => [{ "type" => "uri", "value" => OWL.DatatypeProperty }] } ,
+ }
+
+ @data_entries = {}
+ @values_id = 0
+ @parameter_id = 0
+
+ @classes = Set.new
+ @object_properties = Set.new
+ @annotation_properties = Set.new
+ @datatype_properties = Set.new
+
+ @objects = Set.new
+ end
+
+ # Add a compound
+ # @param [String] uri Compound URI
+ def add_compound(uri)
+ @object[uri] = { RDF["type"] => [{ "type" => "uri", "value" => OT.Compound }] }
+ end
+
+ # Add a feature
+ # @param [String] uri Feature URI
+ def add_feature(uri,metadata)
+ @object[uri] = { RDF["type"] => [{ "type" => "uri", "value" => OT.Feature }] }
+ add_metadata uri, metadata
+ end
+
+ # Add a dataset
+ # @param [String] uri Dataset URI
+ def add_dataset(dataset)
+
+ @dataset = dataset.uri
+
+ @object[dataset.uri] = { RDF["type"] => [{ "type" => "uri", "value" => OT.Dataset }] }
+
+ add_metadata dataset.uri, dataset.metadata
+
+ dataset.compounds.each { |compound| add_compound compound }
+
+ dataset.features.each { |feature,metadata| add_feature feature,metadata }
+
+ dataset.data_entries.each do |compound,entry|
+ entry.each do |feature,values|
+ values.each { |value| add_data_entry compound,feature,value }
+ end
+ end
+
+ end
+
+ # Add a algorithm
+ # @param [String] uri Algorithm URI
+ def add_algorithm(uri,metadata)
+ @object[uri] = { RDF["type"] => [{ "type" => "uri", "value" => OT.Algorithm }] }
+ add_metadata uri, metadata
+ end
+
+ # Add a model
+ # @param [String] uri Model URI
+ def add_model(uri,metadata)
+ @object[uri] = { RDF["type"] => [{ "type" => "uri", "value" => OT.Model }] }
+ add_metadata uri, metadata
+ end
+
+ # Add a task
+ # @param [String] uri Model URI
+ def add_task(uri,metadata)
+ @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
+ def add_metadata(uri,metadata)
+ id = 0
+ metadata.each do |u,v|
+ if v.is_a? Array and u == OT.parameters
+ @object[uri][u] = [] unless @object[uri][u]
+ v.each do |value|
+ id+=1
+ genid = "_:genid#{id}"
+ @object[uri][u] << {"type" => "bnode", "value" => genid}
+ @object[genid] = { RDF["type"] => [{ "type" => "uri", "value" => OT.Parameter}] }
+ value.each do |name,entry|
+ @object[genid][name] = [{"type" => type(entry), "value" => entry }]
+ end
+ end
+ else # v.is_a? String
+ @object[uri] = {} unless @object[uri]
+ @object[uri][u] = [{"type" => type(v), "value" => v }]
+ end
+ end
+ end
+
+ # Add a data entry
+ # @param [String] compound Compound URI
+ # @param [String] feature Feature URI
+ # @param [Boolead,Float] value Feature value
+ def add_data_entry(compound,feature,value)
+ add_compound(compound) unless @object[compound]
+ add_feature(feature,{}) unless @object[feature]
+ unless data_entry = @data_entries[compound]
+ data_entry = "_:dataentry#{@data_entries.size}"
+ @data_entries[compound] = data_entry
+ @object[@dataset][OT.dataEntry] = [] unless @object[@dataset][OT.dataEntry]
+ @object[@dataset][OT.dataEntry] << {"type" => "bnode", "value" => data_entry}
+ @object[data_entry] = {
+ RDF["type"] => [{ "type" => "uri", "value" => OT.DataEntry }],
+ OT.compound => [{ "type" => "uri", "value" => compound }],
+ OT.values => [],
+ }
+ end
+ values = "_:values#{@values_id}"
+ @values_id += 1
+ @object[data_entry][OT.values] << {"type" => "bnode", "value" => values}
+ case type(value)
+ when "uri"
+ v = [{ "type" => "uri", "value" => value}]
+ when "literal"
+ v = [{ "type" => "literal", "value" => value, "datatype" => datatype(value) }]
+ else
+ raise "Illegal type #{type(value)} for #{value}."
+ end
+ @object[values] = {
+ RDF["type"] => [{ "type" => "uri", "value" => OT.FeatureValue }],
+ OT.feature => [{ "type" => "uri", "value" => feature }],
+ OT.value => v
+ }
+ @object[feature][RDF["type"]] << { "type" => "uri", "value" => featuretype(value) }
+ end
+
+ # Serializers
+
+ # Convert to N-Triples
+ # @return [text/plain] Object OWL-DL in N-Triples format
+ def to_ntriples
+
+ @triples = Set.new
+ @object.each do |s,entry|
+ s = url(s) if type(s) == "uri"
+ entry.each do |p,objects|
+ p = url(p)
+ objects.each do |o|
+ case o["type"]
+ when "uri"
+ o = url(o["value"])
+ when "literal"
+ o = literal(o["value"],datatype(o["value"]))
+ when "bnode"
+ o = o["value"]
+ end
+ @triples << [s,p,o]
+ end
+ end
+ end
+ @triples.sort.collect{ |s| s.join(' ').concat(" .") }.join("\n")+"\n"
+ end
+
+ # Convert to RDF/XML
+ # @return [text/plain] Object OWL-DL in RDF/XML format
+ def to_rdfxml
+ Tempfile.open("owl-serializer"){|f| f.write(self.to_ntriples); @path = f.path}
+ `rapper -i ntriples -f 'xmlns:ot="#{OT.uri}"' -f 'xmlns:dc="#{DC.uri}"' -f 'xmlns:rdf="#{RDF.uri}"' -f 'xmlns:owl="#{OWL.uri}"' -o rdfxml #{@path} 2>/dev/null`
+ end
+
+ # Convert to JSON as specified in http://n2.talis.com/wiki/RDF_JSON_Specification
+ # (Ambit services use a different JSON representation)
+ # @return [text/plain] Object OWL-DL in JSON format
+ def to_json
+ #rdf_types
+ Yajl::Encoder.encode(@object)
+ end
+
+ # Helpers for type detection
+ private
+
+ def datatype(value)
+ if value.is_a? TrueClass or value.is_a? FalseClass
+ XSD.boolean
+ elsif value.is_a? Float
+ XSD.float
+ else
+ XSD.string
+ end
+ end
+
+ def featuretype(value)
+ if value.is_a? TrueClass or value.is_a? FalseClass
+ datatype = OT.NominalFeature
+ elsif value.is_a? Float
+ datatype = OT.NumericFeature
+ else
+ datatype = OT.StringFeature
+ end
+ end
+
+ def type(value)
+ begin
+ uri = URI.parse(value)
+ if uri.class == URI::HTTP or uri.class == URI::HTTPS
+ "uri"
+ elsif value.match(/^_/)
+ "bnode"
+ else
+ "literal"
+ end
+ rescue
+ "literal"
+ end
+ end
+
+ def literal(value,type)
+ # concat and << are faster string concatination operators than +
+ '"'.concat(value.to_s).concat('"^^<').concat(type).concat('>')
+ end
+
+ def url(uri)
+ # concat and << are faster string concatination operators than +
+ '<'.concat(uri).concat('>')
+ end
+
+ def rdf_types
+ @classes.each { |c| @object[c] = { RDF["type"] => [{ "type" => "uri", "value" => OWL['Class'] }] } }
+ @object_properties.each { |p| @object[p] = { RDF["type"] => [{ "type" => "uri", "value" => OWL['ObjectProperty'] }] } }
+ @annotation_properties.each { |a| @object[a] = { RDF["type"] => [{ "type" => "uri", "value" => OWL['AnnotationProperty'] }] } }
+ @datatype_properties.each { |d| @object[d] = { RDF["type"] => [{ "type" => "uri", "value" => OWL['DatatypeProperty'] }] } }
+ end
+
+ end
+
+ # Serializer for spreadsheet formats
+ class Spreadsheets # to avoid nameclash with Spreadsheet gem
+
+ # Create a new spreadsheet serializer
+ # @param [OpenTox::Dataset] dataset Dataset object
+ def initialize(dataset)
+ @rows = []
+ @rows << ["SMILES"]
+ features = dataset.features.keys
+ @rows.first << features
+ @rows.first.flatten!
+ dataset.data_entries.each do |compound,entries|
+ smiles = Compound.new(compound).to_smiles
+ row = Array.new(@rows.first.size)
+ row[0] = smiles
+ entries.each do |feature, values|
+ i = features.index(feature)+1
+ values.each do |value|
+ if row[i]
+ row[i] = "#{row[i]} #{value}" # multiple values
+ else
+ row[i] = value
+ end
+ end
+ end
+ @rows << row
+ end
+ end
+
+ # Convert to CSV string
+ # @return [String] CSV string
+ def to_csv
+ @rows.collect{|r| r.join(", ")}.join("\n")
+ end
+
+ # Convert to spreadsheet workbook
+ # @return [Spreadsheet::Workbook] Workbook object (use the spreadsheet gemc to write a file)
+ def to_spreadsheet
+ Spreadsheet.client_encoding = 'UTF-8'
+ book = Spreadsheet::Workbook.new
+ sheet = book.create_worksheet(:name => '')
+ sheet.column(0).width = 100
+ i = 0
+ @rows.each do |row|
+ row.each do |c|
+ sheet.row(i).push c
+ end
+ i+=1
+ end
+ book
+ end
+
+ end
+
+
+ end
+end
diff --git a/lib/task.rb b/lib/task.rb
index 1ab3893..0ee3a11 100644
--- a/lib/task.rb
+++ b/lib/task.rb
@@ -1,42 +1,207 @@
-$self_task=nil
module OpenTox
+ # Class for handling asynchronous tasks
class Task
+ include OpenTox
+ attr_accessor :http_code, :due_to_time
+
+ def initialize(uri=nil)
+ super uri
+ @metadata = {
+ DC.title => "",
+ DC.date => "",
+ OT.hasStatus => "Running",
+ OT.percentageCompleted => 0.0,
+ OT.resultURI => "",
+ DC.creator => "", # not mandatory according to API
+ DC.description => "", # not mandatory according to API
+ }
+ end
+
+ # Create a new task for the code in the block. Catches halts and exceptions and sets task state to error if necessary. The block has to return the URI of the created resource.
+ # @example
+ # task = OpenTox::Task.create do
+ # # this code will be executed as a task
+ # model = OpenTox::Algorithm.run(params) # this can be time consuming
+ # model.uri # Important: return URI of the created resource
+ # end
+ # task.status # returns "Running", because tasks are forked
+ # @param [String] title Task title
+ # @param [String] creator Task creator
+ # @return [OPenTox::Task] Task
+ def self.create( title=nil, creator=nil, max_duration=DEFAULT_TASK_MAX_DURATION, description=nil )
+
+ params = {:title=>title, :creator=>creator, :max_duration=>max_duration, :description=>description }
+ task_uri = RestClientWrapper.post(CONFIG[:services]["opentox-task"], params, {}, nil, false).to_s
+ task = Task.new(task_uri.chomp)
+
+ # measure current memory consumption
+ memory = `free -m|sed -n '2p'`.split
+ free_memory = memory[3].to_i + memory[6].to_i # include cache
+ if free_memory < 20 # require at least 200 M free memory
+ LOGGER.warn "Cannot start task - not enough memory left (#{free_memory} M free)"
+ task.cancel
+ return task
+ #raise "Insufficient memory to start a new task"
+ end
+
+ cpu_load = `cat /proc/loadavg`.split(/\s+/)[0..2].collect{|c| c.to_f}
+ nr_cpu_cores = `cat /proc/cpuinfo |grep "cpu cores"|cut -d ":" -f2|tr -d " "`.split("\n").collect{|c| c.to_i}.inject{|sum,n| sum+n}
+ nr_cpu_cores = 1 if !nr_cpu_cores
+ #if cpu_load[0] > nr_cpu_cores and cpu_load[0] > cpu_load[1] and cpu_load[1] > cpu_load[2] # average CPU load of the last minute is high and CPU load is increasing
+ # LOGGER.warn "Cannot start task - CPU load too high (#{cpu_load.join(", ")})"
+ # task.cancel
+ # return task
+ # #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}"
+ begin
+ result = yield task
+ LOGGER.debug "Task #{task.uri} done #{Time.now} -> "+result.to_s
+ task.completed(result)
+ rescue => error
+ LOGGER.error "task failed: "+error.class.to_s+": "+error.message
+ LOGGER.error ":\n"+error.backtrace.join("\n")
+ task.error(OpenTox::ErrorReport.create(error, creator))
+ end
+ end
+ task.pid = task_pid
+ LOGGER.debug "Started task: "+task.uri.to_s
+ task
+ end
+
+ # Find a task for querying, status changes
+ # @param [String] uri Task URI
+ # @return [OpenTox::Task] Task object
+ def self.find(uri)
+ return nil unless uri
+ task = Task.new(uri)
+ task.load_metadata
+ raise "could not load task metadata" if task.metadata==nil or task.metadata.size==0
+ task
+ end
+
+ # Find a task for querying, status changes
+ # @param [String] uri Task URI
+ # @return [OpenTox::Task] Task object
+ def self.exist?(uri)
+ begin
+ return find(uri)
+ rescue
+ end
+ end
+
+ # Get a list of all tasks
+ # @param [optional, String] uri URI of task service
+ # @return [text/uri-list] Task URIs
+ def self.all(uri=CONFIG[:services]["opentox-task"])
+ OpenTox.all uri
+ end
- # due_to_time is only set in local tasks
- TASK_ATTRIBS = [ :uri, :date, :title, :creator, :description, :hasStatus, :percentageCompleted, :resultURI, :due_to_time ]
- TASK_ATTRIBS.each{ |a| attr_accessor(a) }
- attr_accessor :http_code
+ def self.from_yaml(yaml)
+ @metadata = YAML.load(yaml)
+ end
- private
- def initialize(uri)
- @uri = uri.to_s.strip
+ def self.from_rdfxml(rdfxml)
+ owl = OpenTox::Parser::Owl.from_rdf(rdfxml, OT.Task)
+ task = Task.new(owl.uri)
+ task.add_metadata(owl.metadata)
+ task
+ end
+
+ 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
+
+ def status
+ @metadata[OT.hasStatus]
+ end
+
+ def result_uri
+ @metadata[OT.resultURI]
+ end
+
+ def description
+ @metadata[DC.description]
end
- # create is private now, use OpenTox::Task.as_task
- def self.create( params )
- task_uri = RestClientWrapper.post(@@config[:services]["opentox-task"], params, nil, false).to_s
- Task.find(task_uri.chomp)
+ def errorReport
+ @metadata[OT.errorReport]
end
-
- public
- def self.find( uri, accept_header=nil )
- task = Task.new(uri)
- task.reload( accept_header )
- return task
+
+ def cancel
+ RestClientWrapper.put(File.join(@uri,'Cancelled'),{:cannot_be => "empty"})
+ load_metadata
+ end
+
+ def completed(uri)
+ RestClientWrapper.put(File.join(@uri,'Completed'),{:resultURI => uri})
+ load_metadata
+ end
+
+ 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
+
+ def running?
+ @metadata[OT.hasStatus] == 'Running'
+ end
+
+ def completed?
+ @metadata[OT.hasStatus] == 'Completed'
+ end
+
+ def error?
+ @metadata[OT.hasStatus] == 'Error'
+ end
+
+ def load_metadata
+ if (CONFIG[:yaml_hosts].include?(URI.parse(@uri).host))
+ 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'}, nil, false).code
+ end
+ raise "could not load task metadata for task "+@uri.to_s if @metadata==nil || @metadata.size==0
+ end
+
+ # create is private now, use OpenTox::Task.as_task
+ #def self.create( params )
+ #task_uri = RestClientWrapper.post(CONFIG[:services]["opentox-task"], params, {}, false).to_s
+ #Task.find(task_uri.chomp)
+ #end
+
+=begin
def self.from_data(data, content_type, code, base_uri)
task = Task.new(nil)
task.http_code = code
task.reload_from_data(data, content_type, base_uri)
return task
end
-
+
def reload( accept_header=nil )
unless accept_header
- if (@@config[:yaml_hosts].include?(URI.parse(uri).host))
+ if (CONFIG[:yaml_hosts].include?(URI.parse(uri).host))
accept_header = "application/x-yaml"
else
accept_header = 'application/rdf+xml'
@@ -64,113 +229,168 @@ module OpenTox
end
raise "uri is null after loading" unless @uri and @uri.to_s.strip.size>0
end
-
- def cancel
- RestClientWrapper.put(File.join(@uri,'Cancelled'))
- reload
- end
-
- def completed(uri)
- RestClientWrapper.put(File.join(@uri,'Completed'),{:resultURI => uri})
- reload
- end
-
- def error(description)
- RestClientWrapper.put(File.join(@uri,'Error'),{:description => description.to_s[0..2000]})
- reload
- end
-
- def pid=(pid)
- RestClientWrapper.put(File.join(@uri,'pid'), {:pid => pid})
- end
-
- def running?
- @hasStatus.to_s == 'Running'
- end
-
- def completed?
- @hasStatus.to_s == 'Completed'
- end
-
- def error?
- @hasStatus.to_s == 'Error'
- end
+=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)
- if (@uri.match(@@config[:services]["opentox-task"]))
- due_to_time = (@due_to_time.is_a?(Time) ? @due_to_time : Time.parse(@due_to_time))
- running_time = due_to_time - (@date.is_a?(Time) ? @date : Time.parse(@date))
- else
- # the date of the external task cannot be trusted, offest to local time might be to big
- due_to_time = Time.new + EXTERNAL_TASK_MAX_DURATION
- running_time = EXTERNAL_TASK_MAX_DURATION
- end
+ waiting_task.waiting_for(self.uri) if waiting_task
+ 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
+ load_metadata # for extremely fast tasks
+ check_state
while self.running?
sleep dur
- reload
+ load_metadata
+ # if another (sub)task is waiting for self, set progress accordingly
+ waiting_task.progress(@metadata[OT.percentageCompleted].to_f) if waiting_task
check_state
if (Time.new > due_to_time)
- raise "max wait time exceeded ("+running_time.to_s+"sec), task: '"+@uri.to_s+"'"
+ raise "max wait time exceeded ("+DEFAULT_TASK_MAX_DURATION.to_s+"sec), task: '"+@uri.to_s+"'"
end
end
-
- LOGGER.debug "Task '"+@hasStatus+"': "+@uri.to_s+", Result: "+@resultURI.to_s
+ waiting_task.waiting_for(nil) if waiting_task
+ 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
+
+ def waiting_for(task_uri)
+ RestClientWrapper.put(File.join(@uri,'Running'),{:waiting_for => task_uri})
+ end
+
+ private
+ VALID_TASK_STATES = ["Cancelled", "Completed", "Running", "Error"]
+
def check_state
begin
- raise "illegal task state, task is completed, resultURI is no URI: '"+@resultURI.to_s+
- "'" unless @resultURI and Utils.is_uri?(@resultURI) if completed?
-
+ raise "illegal task state, invalid status: '"+@metadata[OT.hasStatus].to_s+"'" unless
+ @metadata[OT.hasStatus] unless VALID_TASK_STATES.include?(@metadata[OT.hasStatus])
+ raise "illegal task state, task is completed, resultURI is no URI: '"+@metadata[OT.resultURI].to_s+
+ "'" unless @metadata[OT.resultURI] and @metadata[OT.resultURI].to_s.uri? if completed?
if @http_code == 202
- raise "illegal task state, code is 202, but hasStatus is not Running: '"+@hasStatus+"'" unless running?
+ raise "#{@uri}: illegal task state, code is 202, but hasStatus is not Running: '"+@metadata[OT.hasStatus]+"'" unless running?
elsif @http_code == 201
- raise "illegal task state, code is 201, but hasStatus is not Completed: '"+@hasStatus+"'" unless completed?
- raise "illegal task state, code is 201, resultURI is no task-URI: '"+@resultURI.to_s+
- "'" unless @resultURI and Utils.task_uri?(@resultURI)
+ raise "#{@uri}: illegal task state, code is 201, but hasStatus is not Completed: '"+@metadata[OT.hasStatus]+"'" unless completed?
+ raise "#{@uri}: illegal task state, code is 201, resultURI is no task-URI: '"+@metadata[OT.resultURI].to_s+
+ "'" 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" if task!=nil and !(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 waiting_for(task_uri)
+ @task.waiting_for(task_uri)
+ 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
+
- # returns the task uri
- # catches halts and exceptions, task state is set to error then
- def self.as_task( title, creator, max_duration=DEFAULT_TASK_MAX_DURATION, description=nil )
- #return yield nil
-
- params = {:title=>title, :creator=>creator, :max_duration=>max_duration, :description=>description }
- task = OpenTox::Task.create(params)
- 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
- 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)
+ # 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
- task.pid = task_pid
- LOGGER.debug "Started task: "+task.uri.to_s
- task.uri
- 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/templates/config.yaml b/lib/templates/config.yaml
index 00c00cb..8a5e460 100644
--- a/lib/templates/config.yaml
+++ b/lib/templates/config.yaml
@@ -39,3 +39,48 @@
# Uncomment for verbose logging
# :logger: debug
+# :backtrace: 1
+
+
+# OpenSSO Authorization
+# set ":server: " to disable A&A
+:authorization:
+ :server: "https://opensso.in-silico.ch"
+ :free_request: #request-method not controlled by A&A
+ - "GET"
+ :authenticate_request: #only for authenticated user
+ - "POST"
+ :authorize_request: #only for authenticated and authorizeduser
+ - "DELETE"
+ - "PUT"
+ # Exceptions:
+ :free_uris: #request-method for uri not controlled by A&A
+ ? - :GET
+ : - !ruby/regexp /localhost\/algorithm/
+ - "http://localhost/dataset"
+ - "http://localhost/model"
+ - "http://localhost/validation"
+ - "http://localhost/validation/crossvalidation"
+ - "http://localhost/validation/reach_report"
+ - "http://localhost/validation/reach_report/crossvalidation"
+ - "http://localhost/validation/report"
+ - "http://localhost/validation/report/crossvalidation"
+ - "http://localhost/validation/reach_report/qmrf"
+ ? - :GET
+ - :POST
+ : - !ruby/regexp /localhost\/toxcreate/
+ - !ruby/regexp /localhost\/task/
+ - !ruby/regexp /localhost\/compound/
+ ? - :PUT
+ : - !ruby/regexp /localhost\/task/
+
+ :authorize_exceptions: #request-method for uri only authenticated, no authorization
+ ? - :POST
+ : - !ruby/regexp /localhost\/algorithm/
+ - "http://localhost/dataset"
+ - "http://localhost/model"
+ - "http://localhost/validation"
+ - !ruby/regexp /localhost\/validation\/[a-z,A-Z,\/,_\-]*$/
+
+
+ \ No newline at end of file
diff --git a/lib/templates/default_guest_policy.xml b/lib/templates/default_guest_policy.xml
new file mode 100644
index 0000000..a778070
--- /dev/null
+++ b/lib/templates/default_guest_policy.xml
@@ -0,0 +1,53 @@
+<!DOCTYPE Policies PUBLIC "-//Sun Java System Access Manager7.1 2006Q3
+ Admin CLI DTD//EN" "jar://com/sun/identity/policy/policyAdmin.dtd">
+
+<Policies>
+<Policy name="policy_user" referralPolicy="false" active="true">
+ <Rule name="rule_user">
+ <ServiceName name="iPlanetAMWebAgentService" />
+ <ResourceName name="uri"/>
+ <AttributeValuePair>
+ <Attribute name="GET" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ <AttributeValuePair>
+ <Attribute name="POST" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ <AttributeValuePair>
+ <Attribute name="PUT" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ <AttributeValuePair>
+ <Attribute name="DELETE" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ </Rule>
+ <Subjects name="subjects_user" description="">
+ <Subject name="subject_user" type="LDAPUsers" includeType="inclusive">
+ <AttributeValuePair>
+ <Attribute name="Values"/>
+ <Value>uid=guest,ou=people,dc=opentox,dc=org</Value>
+ </AttributeValuePair>
+ </Subject>
+ </Subjects>
+</Policy>
+<Policy name="policy_group" referralPolicy="false" active="true">
+ <Rule name="rule_group">
+ <ServiceName name="iPlanetAMWebAgentService" />
+ <ResourceName name="uri"/>
+ <AttributeValuePair>
+ <Attribute name="GET" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ </Rule>
+ <Subjects name="subjects_group" description="">
+ <Subject name="subject_group" type="LDAPGroups" includeType="inclusive">
+ <AttributeValuePair>
+ <Attribute name="Values"/>
+ <Value>cn=member,ou=groups,dc=opentox,dc=org</Value>
+ </AttributeValuePair>
+ </Subject>
+ </Subjects>
+</Policy>
+</Policies>
diff --git a/lib/templates/default_policy.xml b/lib/templates/default_policy.xml
new file mode 100644
index 0000000..a778070
--- /dev/null
+++ b/lib/templates/default_policy.xml
@@ -0,0 +1,53 @@
+<!DOCTYPE Policies PUBLIC "-//Sun Java System Access Manager7.1 2006Q3
+ Admin CLI DTD//EN" "jar://com/sun/identity/policy/policyAdmin.dtd">
+
+<Policies>
+<Policy name="policy_user" referralPolicy="false" active="true">
+ <Rule name="rule_user">
+ <ServiceName name="iPlanetAMWebAgentService" />
+ <ResourceName name="uri"/>
+ <AttributeValuePair>
+ <Attribute name="GET" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ <AttributeValuePair>
+ <Attribute name="POST" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ <AttributeValuePair>
+ <Attribute name="PUT" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ <AttributeValuePair>
+ <Attribute name="DELETE" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ </Rule>
+ <Subjects name="subjects_user" description="">
+ <Subject name="subject_user" type="LDAPUsers" includeType="inclusive">
+ <AttributeValuePair>
+ <Attribute name="Values"/>
+ <Value>uid=guest,ou=people,dc=opentox,dc=org</Value>
+ </AttributeValuePair>
+ </Subject>
+ </Subjects>
+</Policy>
+<Policy name="policy_group" referralPolicy="false" active="true">
+ <Rule name="rule_group">
+ <ServiceName name="iPlanetAMWebAgentService" />
+ <ResourceName name="uri"/>
+ <AttributeValuePair>
+ <Attribute name="GET" />
+ <Value>allow</Value>
+ </AttributeValuePair>
+ </Rule>
+ <Subjects name="subjects_group" description="">
+ <Subject name="subject_group" type="LDAPGroups" includeType="inclusive">
+ <AttributeValuePair>
+ <Attribute name="Values"/>
+ <Value>cn=member,ou=groups,dc=opentox,dc=org</Value>
+ </AttributeValuePair>
+ </Subject>
+ </Subjects>
+</Policy>
+</Policies>
diff --git a/lib/templates/users.yaml b/lib/templates/users.yaml
deleted file mode 100644
index 483fd7b..0000000
--- a/lib/templates/users.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-# please insert users and passwords here.
-# one user and password each line. uncomment the line.
-:users:
-# username: "secretpassword"
-# exampleuser: "ih9aiTog" \ No newline at end of file
diff --git a/lib/to-html.rb b/lib/to-html.rb
new file mode 100644
index 0000000..6785974
--- /dev/null
+++ b/lib/to-html.rb
@@ -0,0 +1,112 @@
+
+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(s?):\/\/[^\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, subjectid=nil, 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 = "<html>"
+ html += "<title>"+title+"</title>" if title
+ html += "<img src="+OT_LOGO+"><body>"
+
+ if AA_SERVER
+ user = OpenTox::Authorization.get_user(subjectid) if subjectid
+ html += "<pre><p align=\"right\">"
+ unless user
+ html += "You are currently not logged in to "+$url_provider.url_for("",:full)+
+ ", <a href="+$url_provider.url_for("/login",:full)+">login</a>"
+ else
+ html += "You are logged in as '#{user}' to "+$url_provider.url_for("",:full)+
+ ", <a href="+$url_provider.url_for("/logout",:full)+">logout</a>"
+ end
+ html += " </p></pre>"
+ end
+
+ 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 += "<pre><p style=\"padding:15px; border:10px solid \#5D308A\">"
+ html += text.link_urls
+ html += "</p></pre></body><html>"
+ html
+ end
+
+ def self.login( msg=nil )
+ html = "<html><title>Login</title><img src="+OT_LOGO+"><body>"
+ html += "<form method='POST' action='"+$url_provider.url_for("/login",:full)+"'>"
+ html += "<pre><p style=\"padding:15px; border:10px solid \#5D308A\">"
+ html += msg+"\n\n" if msg
+ html += "Please login to "+$url_provider.url_for("",:full)+"\n\n"
+ html += "<table border=0>"
+ html += "<tr><td>user:</td><td><input type='text' name='user' size='15' /></td></tr>"+
+ "<tr><td>password:</td><td><input type='password' name='password' size='15' /></td></tr>"+
+ #"<input type=hidden name=back_to value="+back_to.to_s+">"+
+ "<tr><td><input type='submit' value='Login' /></td></tr>"
+ html += "</table></p></pre></form></body><html>"
+ html
+ end
+end
+
+=begin
+get '/logout/?' do
+ response.set_cookie("subjectid",{:value=>nil})
+ content_type "text/html"
+ content = "Sucessfully logged out from "+$url_provider.url_for("",:full)
+ OpenTox.text_to_html(content)
+end
+
+get '/login/?' do
+ content_type "text/html"
+ OpenTox.login
+end
+
+post '/login/?' do
+ subjectid = OpenTox::Authorization.authenticate(params[:user], params[:password])
+ if (subjectid)
+ response.set_cookie("subjectid",{:value=>subjectid})
+ content_type "text/html"
+ content = "Sucessfully logged in as '"+params[:user]+"' to "+$url_provider.url_for("",:full)
+ OpenTox.text_to_html(content,subjectid)
+ else
+ content_type "text/html"
+ OpenTox.login("Login failed, please try again")
+ end
+end
+=end
+
diff --git a/lib/utils.rb b/lib/utils.rb
deleted file mode 100644
index a0e0cbe..0000000
--- a/lib/utils.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module OpenTox
- module Utils
- # gauss kernel
- def self.gauss(sim, sigma = 0.3)
- x = 1.0 - sim
- Math.exp(-(x*x)/(2*sigma*sigma))
- end
-
- def self.task_uri?(uri)
- is_uri?(uri) && uri.to_s =~ /task/
- end
-
- def self.dataset_uri?(uri)
- is_uri?(uri) && uri.to_s =~ /dataset/
- end
-
- def self.model_uri?(uri)
- is_uri?(uri) && uri.to_s =~ /model/
- end
-
-
- def self.is_uri?(uri)
- return false if uri==nil || uri.to_s.size==0
- begin
- u = URI::parse(uri)
- return (u.scheme!=nil and u.host!=nil)
- rescue URI::InvalidURIError
- return false
- end
- end
-
- def self.median(array)
- return nil if array.empty?
- array.sort!
- m_pos = array.size / 2
- return array.size % 2 == 1 ? array[m_pos] : (array[m_pos-1] + array[m_pos])/2
- end
-
- end
-
-# ['rubygems', 'rest_client'].each do |r|
-# require r
-# end
-# ["bla", "google.de", "http://google.de"].each do |u|
-# puts u+"? "+Utils.is_uri?(u).to_s
-# end
-
-
-end
-
diff --git a/lib/validation.rb b/lib/validation.rb
index 89a2a0c..a47a554 100644
--- a/lib/validation.rb
+++ b/lib/validation.rb
@@ -1,20 +1,196 @@
module OpenTox
- class Validation
+ class Validation
+ include OpenTox
+
+ # find validation, raises error if not found
+ # @param [String] uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::Validation]
+ def self.find( uri, subjectid=nil )
+ val = Validation.new(uri)
+ val.load_metadata( subjectid )
+ val
+ end
+
+ # creates a validation object from crossvaldiation statistics, raise error if not found
+ # (as crossvaldiation statistics are returned as an average valdidation over all folds)
+ # @param [String] crossvalidation uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::Validation]
+ def self.from_cv_statistics( crossvalidation_uri, subjectid=nil )
+ find( File.join(crossvalidation_uri, 'statistics'),subjectid )
+ end
+
+ # loads metadata via yaml from validation object
+ # fields (like for example the validated model) can be acces via validation.metadata[OT.model]
+ def load_metadata( subjectid=nil )
+ @metadata = YAML.load(OpenTox::RestClientWrapper.get(uri,{:subjectid => subjectid, :accept => "application/x-yaml"}))
+ end
+
+ # PENDING: creates summary as used for ToxCreate
+ def summary
+ if @metadata[OT.classificationStatistics]
+ res = {
+ :nr_predictions => @metadata[OT.numInstances] - @metadata[OT.numUnpredicted],
+ :correct_predictions => @metadata[OT.classificationStatistics][OT.percentCorrect],
+ :weighted_area_under_roc => @metadata[OT.classificationStatistics][OT.weightedAreaUnderRoc],
+ }
+ @metadata[OT.classificationStatistics][OT.classValueStatistics].each do |s|
+ if s[OT.classValue].to_s=="true"
+ res[:true_positives] = s[OT.numTruePositives]
+ res[:false_positives] = s[OT.numFalsePositives]
+ res[:true_negatives] = s[OT.numTrueNegatives]
+ res[:false_negatives] = s[OT.numFalseNegatives]
+ res[:sensitivity] = s[OT.truePositiveRate]
+ res[:specificity] = s[OT.falsePositiveRate]
+ break
+ end
+ end
+ res
+ elsif @metadata[OT.regressionStatistics]
+ {
+ :nr_predictions => @metadata[OT.numInstances] - @metadata[OT.numUnpredicted],
+ :r_square => @metadata[OT.regressionStatistics][OT.rSquare],
+ :root_mean_squared_error => @metadata[OT.regressionStatistics][OT.rootMeanSquaredError],
+ :mean_absolute_error => @metadata[OT.regressionStatistics][OT.meanAbsoluteError],
+ }
+ end
+ end
+ end
+
+ class Crossvalidation
+ include OpenTox
- attr_accessor :uri
-
- def initialize(params)
- @uri = OpenTox::RestClientWrapper.post(File.join(@@config[:services]["opentox-validation"],"/crossvalidation"),params,nil,false)
- end
+ attr_reader :report
+
+ # find crossvalidation, raises error if not found
+ # @param [String] uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::Crossvalidation]
+ def self.find( uri, subjectid=nil )
+ cv = Crossvalidation.new(uri)
+ cv.load_metadata( subjectid )
+ cv
+ end
- def self.crossvalidation(params)
- params[:uri] = File.join(@@config[:services]['opentox-validation'], "crossvalidation")
- params[:num_folds] = 10 unless params[:num_folds]
- params[:random_seed] = 2 unless params[:random_seed]
- params[:stratified] = false unless params[:stratified]
- OpenTox::Validation.new(params)
- end
+ # creates a crossvalidations, waits until it finishes, may take some time
+ # @param [Hash] params (required:algorithm_uri,dataset_uri,prediction_feature, optional:algorithm_params,num_folds(10),random_seed(1),stratified(false))
+ # @param [String,optional] subjectid
+ # @param [OpenTox::Task,optional] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly
+ # @return [OpenTox::Crossvalidation]
+ def self.create( params, subjectid=nil, waiting_task=nil )
+ params[:subjectid] = subjectid if subjectid
+ uri = OpenTox::RestClientWrapper.post( File.join(CONFIG[:services]["opentox-validation"],"crossvalidation"),
+ params,{:content_type => "text/uri-list"},waiting_task )
+ Crossvalidation.new(uri)
+ end
+
+ # looks for report for this crossvalidation, creates a report if no report is found
+ # @param [String,optional] subjectid
+ # @param [OpenTox::Task,optional] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly
+ # @return [String] report uri
+ def find_or_create_report( subjectid=nil, waiting_task=nil )
+ @report = CrossvalidationReport.find_for_crossvalidation(@uri, subjectid) unless @report
+ @report = CrossvalidationReport.create(@uri, subjectid, waiting_task) unless @report
+ @report.uri
+ end
+
+ # loads metadata via yaml from crossvalidation object
+ # fields (like for example the validations) can be acces via validation.metadata[OT.validation]
+ def load_metadata( subjectid=nil )
+ @metadata = YAML.load(OpenTox::RestClientWrapper.get(uri,{:subjectid => subjectid, :accept => "application/x-yaml"}))
+ end
+
+ # PENDING: creates summary as used for ToxCreate
+ def summary( subjectid=nil )
+ Validation.from_cv_statistics( @uri, subjectid ).summary
+ end
+ end
+
+ class ValidationReport
+ include OpenTox
+
+ # finds ValidationReport for a particular validation
+ # @param [String] crossvalidation uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::ValidationReport] nil if no report found
+ def self.find_for_validation( validation_uri, subjectid=nil )
+ uris = RestClientWrapper.get(File.join(CONFIG[:services]["opentox-validation"],
+ "/report/validation?validation="+validation_uri), {:subjectid => subjectid}).chomp.split("\n")
+ uris.size==0 ? nil : ValidationReport.new(uris[-1])
+ end
+
+ end
- end
+ class CrossvalidationReport
+ include OpenTox
+
+ # finds CrossvalidationReport via uri, raises error if not found
+ # @param [String] uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::CrossvalidationReport]
+ def self.find( uri, subjectid=nil )
+ # PENDING load report data?
+ OpenTox::RestClientWrapper.get(uri,{:subjectid => subjectid})
+ CrossvalidationReport.new(uri)
+ end
+
+ # finds CrossvalidationReport for a particular crossvalidation
+ # @param [String] crossvalidation uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::CrossvalidationReport] nil if no report found
+ def self.find_for_crossvalidation( crossvalidation_uri, subjectid=nil )
+ uris = RestClientWrapper.get(File.join(CONFIG[:services]["opentox-validation"],
+ "/report/crossvalidation?crossvalidation="+crossvalidation_uri), {:subjectid => subjectid}).chomp.split("\n")
+ uris.size==0 ? nil : CrossvalidationReport.new(uris[-1])
+ end
+
+ # creates a crossvalidation report via crossvalidation
+ # @param [String] crossvalidation uri
+ # @param [String,optional] subjectid
+ # @param [OpenTox::Task,optional] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly
+ # @return [OpenTox::CrossvalidationReport]
+ def self.create( crossvalidation_uri, subjectid=nil, waiting_task=nil )
+ uri = RestClientWrapper.post(File.join(CONFIG[:services]["opentox-validation"],"/report/crossvalidation"),
+ { :validation_uris => crossvalidation_uri, :subjectid => subjectid }, {}, waiting_task )
+ CrossvalidationReport.new(uri)
+ end
+ end
+
+ class QMRFReport
+ include OpenTox
+
+ # finds QMRFReport, raises Error if not found
+ # @param [String] uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::QMRFReport]
+ def self.find( uri, subjectid=nil )
+ # PENDING load crossvalidation data?
+ OpenTox::RestClientWrapper.get(uri,{:subjectid => subjectid})
+ QMRFReport.new(uri)
+ end
+
+ # finds QMRF report for a particular model
+ # @param [String] model_uri
+ # @param [String,optional] subjectid
+ # @return [OpenTox::QMRFReport] nil if no report found
+ def self.find_for_model( model_uri, subjectid=nil )
+ uris = RestClientWrapper.get(File.join(CONFIG[:services]["opentox-validation"],
+ "/reach_report/qmrf?model="+model_uri), {:subjectid => subjectid}).chomp.split("\n")
+ uris.size==0 ? nil : QMRFReport.new(uris[-1])
+ end
+
+ # creates a qmrf report via model
+ # @param [String] model_uri
+ # @param [String,optional] subjectid
+ # @param [OpenTox::Task,optional] waiting_task (can be a OpenTox::Subtask as well), progress is updated accordingly
+ # @return [OpenTox::QMRFReport]
+ def self.create( model_uri, subjectid=nil, waiting_task=nil )
+ uri = RestClientWrapper.post(File.join(CONFIG[:services]["opentox-validation"],"/reach_report/qmrf"),
+ { :model_uri => model_uri, :subjectid => subjectid }, {}, waiting_task )
+ QMRFReport.new(uri)
+ end
+ end
+
end
diff --git a/opentox-ruby-api-wrapper.gemspec b/opentox-ruby.gemspec
index f0a0816..3903af7 100644
--- a/opentox-ruby-api-wrapper.gemspec
+++ b/opentox-ruby.gemspec
@@ -4,15 +4,15 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
- s.name = %q{opentox-ruby-api-wrapper}
- s.version = "1.6.5"
+ s.name = %q{opentox-ruby}
+ s.version = "0.0.2"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
- s.authors = ["Christoph Helma, Martin Guetlein"]
- s.date = %q{2010-08-25}
+ s.authors = ["Christoph Helma, Martin Guetlein, Andreas Maunz, Micha Rautenberg, David Vorgrimmler"]
+ s.date = %q{2011-01-27}
s.description = %q{Ruby wrapper for the OpenTox REST API (http://www.opentox.org)}
s.email = %q{helma@in-silico.ch}
- s.executables = ["opentox-install-ubuntu.sh", "yaml2owl.rb", "opentox-install-debian.sh"]
+ s.executables = ["opentox-install-ubuntu.sh", "opentox-install-debian.sh"]
s.extra_rdoc_files = [
"LICENSE",
"README.rdoc"
@@ -24,30 +24,38 @@ Gem::Specification.new do |s|
"VERSION",
"bin/opentox-install-debian.sh",
"bin/opentox-install-ubuntu.sh",
- "bin/yaml2owl.rb",
"lib/algorithm.rb",
"lib/authorization.rb",
"lib/compound.rb",
"lib/config/config_ru.rb",
"lib/dataset.rb",
"lib/environment.rb",
- "lib/features.rb",
+ "lib/error.rb",
+ "lib/feature.rb",
"lib/helper.rb",
"lib/model.rb",
- "lib/opentox-ruby-api-wrapper.rb",
+ "lib/ontology_service.rb",
+ "lib/opentox-ruby.rb",
"lib/opentox.owl",
- "lib/ot-logger.rb",
+ "lib/opentox.rb",
"lib/overwrite.rb",
- "lib/owl.rb",
+ "lib/owl.rb.RDF",
+ "lib/owl.rb.nt",
+ "lib/owl.rb.rdfxml.initial",
+ "lib/owl.rb.redland",
+ "lib/parser.rb",
+ "lib/policy.rb",
"lib/rest_client_wrapper.rb",
+ "lib/serializer.rb",
"lib/spork.rb",
"lib/task.rb",
"lib/templates/config.yaml",
- "lib/templates/users.yaml",
- "lib/utils.rb",
+ "lib/templates/default_guest_policy.xml",
+ "lib/templates/default_policy.xml",
+ "lib/to-html.rb",
"lib/validation.rb"
]
- s.homepage = %q{http://github.com/helma/opentox-ruby-api-wrapper}
+ s.homepage = %q{http://github.com/helma/opentox-ruby}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.7}
@@ -68,10 +76,10 @@ Gem::Specification.new do |s|
s.add_runtime_dependency(%q<rack-flash>, [">= 0"])
s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
s.add_runtime_dependency(%q<rubyzip>, [">= 0"])
- s.add_runtime_dependency(%q<builder>, [">= 0"])
s.add_runtime_dependency(%q<roo>, [">= 0"])
s.add_runtime_dependency(%q<spreadsheet>, [">= 0"])
s.add_runtime_dependency(%q<google-spreadsheet-ruby>, [">= 0"])
+ s.add_runtime_dependency(%q<yajl-ruby>, [">= 0"])
s.add_runtime_dependency(%q<tmail>, [">= 0"])
s.add_runtime_dependency(%q<rinruby>, [">= 0"])
s.add_runtime_dependency(%q<rjb>, [">= 0"])
@@ -83,7 +91,6 @@ Gem::Specification.new do |s|
s.add_runtime_dependency(%q<dm-mysql-adapter>, [">= 1"])
s.add_runtime_dependency(%q<dm-validations>, [">= 1"])
s.add_runtime_dependency(%q<haml>, [">= 3"])
- s.add_development_dependency(%q<cucumber>, [">= 0"])
s.add_development_dependency(%q<jeweler>, [">= 0"])
else
s.add_dependency(%q<sinatra>, [">= 0"])
@@ -96,10 +103,10 @@ Gem::Specification.new do |s|
s.add_dependency(%q<rack-flash>, [">= 0"])
s.add_dependency(%q<nokogiri>, [">= 0"])
s.add_dependency(%q<rubyzip>, [">= 0"])
- s.add_dependency(%q<builder>, [">= 0"])
s.add_dependency(%q<roo>, [">= 0"])
s.add_dependency(%q<spreadsheet>, [">= 0"])
s.add_dependency(%q<google-spreadsheet-ruby>, [">= 0"])
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
s.add_dependency(%q<tmail>, [">= 0"])
s.add_dependency(%q<rinruby>, [">= 0"])
s.add_dependency(%q<rjb>, [">= 0"])
@@ -111,7 +118,6 @@ Gem::Specification.new do |s|
s.add_dependency(%q<dm-mysql-adapter>, [">= 1"])
s.add_dependency(%q<dm-validations>, [">= 1"])
s.add_dependency(%q<haml>, [">= 3"])
- s.add_dependency(%q<cucumber>, [">= 0"])
s.add_dependency(%q<jeweler>, [">= 0"])
end
else
@@ -125,10 +131,10 @@ Gem::Specification.new do |s|
s.add_dependency(%q<rack-flash>, [">= 0"])
s.add_dependency(%q<nokogiri>, [">= 0"])
s.add_dependency(%q<rubyzip>, [">= 0"])
- s.add_dependency(%q<builder>, [">= 0"])
s.add_dependency(%q<roo>, [">= 0"])
s.add_dependency(%q<spreadsheet>, [">= 0"])
s.add_dependency(%q<google-spreadsheet-ruby>, [">= 0"])
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
s.add_dependency(%q<tmail>, [">= 0"])
s.add_dependency(%q<rinruby>, [">= 0"])
s.add_dependency(%q<rjb>, [">= 0"])
@@ -140,7 +146,6 @@ Gem::Specification.new do |s|
s.add_dependency(%q<dm-mysql-adapter>, [">= 1"])
s.add_dependency(%q<dm-validations>, [">= 1"])
s.add_dependency(%q<haml>, [">= 3"])
- s.add_dependency(%q<cucumber>, [">= 0"])
s.add_dependency(%q<jeweler>, [">= 0"])
end
end