#!/usr/bin/env ruby require 'rubygems' if RUBY_VERSION < '1.9' require 'sinatra/base' require 'haml' require 'open-uri' require 'rest-client' require 't2-server' require 'myexperiment-rest' require 'cgi' require 'stringio' require 'nokogiri' #require "t2-web" # TODO: move whole of T2WebApp in lib # TODO: add constants.rb file in lib for all constants (e.g. myExp user URL, # TMP_UPLOAD_PATH, other URLs -- consider adding those in rack config.ru class T2WebApp < Sinatra::Base WEB_APP_NAME = "t2web" set :views, File.dirname(__FILE__) + '/../views' #set :public_folder, '/var/lib/gems/1.9.1/gems/t2-web-0.1.9' #set :public_folder, File.dirname(__FILE__) + '/../public' set :public_folder, File.expand_path(File.join('/', 'var', 'www', 't2web')) #puts File.dirname(__FILE__) + '/../public' #puts public_folder #ENV.each { |e| puts "#{e[0]} => #{e[1]}" } # Can be used from routes and views (haml) -- not public methods helpers do # Used before we sent the workflow inputs to the taverna server. Just converts # double quotations to single ones. That is because the t2-server library # is confused with double quotes and apparently it is not easy to fix. def t2_library_sanitize(string) ex = string.gsub('"', "'") end # Deletes last new line of file if it exists! It is needed for t2 workflows that # do not sanitize properly, i.e. via a user-provided beanshell script def chomp_last_newline(file) if File.file?(file) and File.size(file) > 1 f = open(file, "rb+") f.seek(-1, File::SEEK_END) f.truncate(File.size(file) - 1) if f.read(1) == "\n" f.close end end # Construct the complete or partial URL as MyExperimentRest lib expects it # TODO: Maybe update myExperimentREST lib to accept only wid and wkf_version def get_url_from_wid(wid, wkf_version) if wkf_version == "default" "/workflows/#{wid}" else "/workflows/#{wid}?version=#{wkf_version}" end end # Generates the contents of the header frame def generate_header_frame(my_exp_wkf, my_exp_usr) < #{generate_header_table(my_exp_wkf, my_exp_usr)} END end # Generates the contents of the header frame table def generate_header_table(my_exp_wkf, my_exp_usr) <
NBIC logo
#{@my_exp_wkf.title}
workflow by #{@my_exp_usr.name}
LUMC logo
END end # Generates tooltip html text with type prefix def generate_tooltip(type, input) if type =~ /description/i type << CGI::unescapeHTML(input.descriptions[0]) else type << CGI::unescapeHTML(input.examples[0]) end end # Generates the contents of the data-navigation frame def generate_data_navigation_frame(my_exp_wkf, uuid, wid, wkf_version, t2_server, finished_execution) puts "generate data navigation frame function called" data_navigation_frame = "" if my_exp_wkf.outputs.size >=1 my_exp_wkf.outputs.each do |output| data_navigation_frame << "" if finished_execution # TODO: currently t2_server, wid and wkf_version are not used data_navigation_frame << "" else data_navigation_frame << "" end data_navigation_frame << "" end end data_navigation_frame << "
#{output.name}#{output.name}Loading Content...
" end # Takes string list as input and prepares for HTML def generate_html_result(data) data.gsub(/[\n]/, '
') end # Takes image url and prepares for HTML def generate_image_result(img) "" end # Takes list as input (or single element), returns as multi-line string # TODO: when 'typing' issues are resolved think of using system_flatten_list_results method # TODO: is added to single images -- remember to clean after 'typing' work def flatten_list_results(data_lists) #puts data_lists.to_yaml # mark data = "" if data_lists.instance_of? String #puts ">>>>>>>>>>> changing type of data_lists" data_lists = [ data_lists ] end data_lists.each do |list| if list.instance_of? Array tmp_data = list.flatten.compact.map {|e| e.chomp} tmp_data[0] = "" + tmp_data[0] + "" data += tmp_data.join("\n") + "\n" else data += list.chomp + "\n" end end data end # Takes list as input (or single element), returns as file stream # TODO: for images it works when not a list! #def system_flatten_list_results(list) # data = "" # if list.instance_of? Array # # flatten (sub-)list and remove nils and if present, newlines # # then join list as a multi-line string # data = list.flatten.compact.map {|e| e.chomp}.join("\n") # else # data = list.chomp # end # StringIO.new data #end # Copies output stream to a tmp file in the specified subdir def copy_to_tmp(subdir, filename, out) # used to copy them at tmp #FileUtils.mkdir_p("/tmp/#{WEB_APP_NAME}/#{subdir}") unless File.exist?("/tmp/#{WEB_APP_NAME}/#{subdir}") #tmp_file = File.open("/tmp/#{WEB_APP_NAME}/#{subdir}/#{filename}", "w") # copy at our tmp dir below public folder FileUtils.mkdir_p("#{settings.public_folder}/#{WEB_APP_NAME}/tmp/#{subdir}") unless File.exist?("#{settings.public_folder}/#{WEB_APP_NAME}/tmp/#{subdir}") tmp_file = File.open("#{settings.public_folder}/#{WEB_APP_NAME}/tmp/#{subdir}/#{filename}", "w") FileUtils.copy_stream(out, tmp_file) tmp_file.close "Successfully copied #{filename}!!" end end # Create the Web UI for that workflow get "/#{WEB_APP_NAME}/workflow/:wid" do # Get http request's parameters # TODO: consider adding the following to a session @wid = params[:wid] @wkf_version = params[:wkf_version] || nil #@t2_server = params[:server] || "http://test.mybiobank.org/taverna-server" @t2_server = params[:server] || "http://localhost:8080/taverna-server" # Get myExperiment workflow object # TODO: catch exception @my_exp_wkf = MyExperimentREST::Workflow.from_id_and_version(@wid, @wkf_version) #puts @my_exp_wkf.to_yaml # Get myExperiment user object # TODO: catch exception @my_exp_usr = MyExperimentREST::User.from_uri(@my_exp_wkf.uploader_uri) haml :form end # Enact the workflow with the submitted parameters and returns # result display (involves checking run's status and getting results) post "/#{WEB_APP_NAME}/enact" do #p params # Get http POST request's parameters # TODO: consider adding the following to a session @wid = params[:wid] @wkf_version = params[:wkf_version] #@t2_server = params[:server] @t2_server = "http://workflow.biosemantics.org/taverna-server" # hacked by mark @my_exp_wkf = MyExperimentREST::Workflow.from_id_and_version(@wid, @wkf_version) # Get myExperiment user object (TODO: session!, exceptions) @my_exp_usr = MyExperimentREST::User.from_uri(@my_exp_wkf.uploader_uri) # use the uri reference to download the workflow locally #wkf_file = URI.parse(@my_exp_wkf.content_uri) wkf = open(@my_exp_wkf.content_uri).read # Parse wkf XML to add username/password for RSHELL if applicable wkf_xml = Nokogiri::XML(wkf) rshellConfig = wkf_xml.xpath("//net.sf.taverna.t2.activities.rshell.RshellActivityConfigurationBean").first if (!(rshellConfig.nil?) and rshellConfig.xpath("//connectionSettings/username").first.nil?) newNode = Nokogiri::XML::Node.new("username", wkf_xml) wkf_xml.xpath("//net.sf.taverna.t2.activities.rshell.RshellActivityConfigurationBean/connectionSettings").first.add_child(newNode) end #puts wkf_xml.xpath("//connectionSettings") wkf = wkf_xml.to_s # create run begin run = T2Server::Run.create(@t2_server, wkf.sub(/<\?xml(.*)?>/,"")) #puts ">>> wkf from t2-web #{@my_exp_wkf.content_uri}:\n" rescue T2Server::T2ServerError => e #mark: puts e.to_s #mark: puts e.to_yaml return "404 Not Found: run could not be instantiated!" end # Get the run as instance member to make visible in views @run_uuid = run.uuid # Set workflow inputs @my_exp_wkf.inputs.each do |input| if params[:"upload-checkbox-#{input.name}"] == "yes" filename = params[:"#{input.name}-file"] tmp_filename = "#{settings.public_folder}/#{WEB_APP_NAME}/tmp/#{filename}" puts "tmp_filename: #{tmp_filename}" # make sure that the file is uploaded completely, i.e. all it's # connections are closed #while `lsof -c cp | grep /tmp/#{WEB_APP_NAME}/#{filename}` != "" # sleep 1 #end chomp_last_newline(tmp_filename) run.upload_input_file(input.name, tmp_filename) else run.set_input(input.name, t2_library_sanitize(params["#{input.name}-input".to_sym]) ) end end # start run and wait until it is finished puts "run started" run.start puts "webapp name: #{WEB_APP_NAME}" haml :results end # Proxy operation (to bypass cross-domain AJAX) to get T2 run's status get "/#{WEB_APP_NAME}/runs/:uuid/status" do uuid = params[:uuid] t2_server = params[:server] forceNoCache = (0...8).map{65.+(rand(25)).chr}.join statusUrl = t2_server + "/rest/runs/" + uuid + "/status" + "?random=" + forceNoCache #puts "Getting status from: " + statusUrl response = RestClient.get statusUrl, :content_type => "text/plain" #puts response.to_str response.to_str end # Get results from all outputs and store in tmp folder # Cross-domain AJAX to get result from T2 get "/#{WEB_APP_NAME}/run/:uuid/outputs" do @run_uuid = params[:uuid] @t2_server = params[:server] # Get t2 server and then the run using uuid server = T2Server::Server.connect(@t2_server) run = server.run(@run_uuid) # Get output ports from t2 server and store in tmp folder # TODO: TMP folder constant from config! # TODO: that gets the whole output.. request client lib to support partial # download if result too large! run.get_output_ports.each do |t2_output| begin data_lists = run.get_output(t2_output, false) rescue Exception => e data_lists = run.get_output("#{t2_output}.error", false) end # flatten the results data_stream = flatten_list_results(data_lists) # copy results to our app's tmp copy_to_tmp("outputs/#{@run_uuid}", t2_output, (StringIO.new data_stream)) end # return success! "Successful download!" end # Get results for specified run and output (stored locally from tmp) # and display it in data-display div # Cross-domain AJAX to get result from T2 get "/#{WEB_APP_NAME}/run/:uuid/output/:out" do # change public only for this route so that /tmp/t2web can be accessed # unfortunately contrary to an article the following does not work #set :public_folder, "./tmp/#{WEB_APP_NAME}" run_uuid = params[:uuid] t2_output = params[:out] display_file = "#{settings.public_folder}/#{WEB_APP_NAME}/tmp/outputs/#{run_uuid}/#{t2_output}" data = File.open(display_file, "r").read # Add HTML tags to display properly # Check what kind of file it is. for now use system 'file' -- note that it will only # work with one image since flatten_list_results only deals with multi-line strings # and single strings or single images (indirectly!) mime_type = %x( file --mime-type #{display_file} ) # TODO: image types can go to array and regex auto-generated # TODO: application/octet-stream is a temporary hack until we deal with 'typing' properly if mime_type =~ /image\/png|image\/jpg|image\/gif|image\/bmp|application\/octet-stream/ # clean image from --- will not be needed after we deal with 'typing' properly t2-server-0.9.x if mime_type =~ /octet-stream/ unless File.file?("#{settings.public_folder}/#{WEB_APP_NAME}/tmp/outputs/#{run_uuid}/clean_#{t2_output}") new_length = File.size?(display_file) - 3 - 5 # for and \n respectively %x( dd if="#{display_file}" of="#{settings.public_folder}/#{WEB_APP_NAME}/tmp/outputs/#{run_uuid}/clean_#{t2_output}" bs=1 skip=3 count="#{new_length}" ) end generate_image_result("/#{WEB_APP_NAME}/tmp/outputs/#{run_uuid}/clean_#{t2_output}") else generate_image_result("/#{WEB_APP_NAME}/tmp/outputs/#{run_uuid}/#{t2_output}") end else generate_html_result(data) end end # Used from input's upload-form to upload files without refresh (actually # a hidden iframe is refreshed). # TODO: use copy_to_tmp method!! Uploads still go to system's tmp and not # to our webapp's public tmp post "/#{WEB_APP_NAME}/upload" do filename = params[:file][:filename] tempfile = params[:file][:tempfile] FileUtils.mkdir("/tmp/#{WEB_APP_NAME}") unless File.exist?("/tmp/#{WEB_APP_NAME}") FileUtils.copy(tempfile.path, "/tmp/#{WEB_APP_NAME}/#{filename}") "Successfully copied #{filename}!!" end # start the server if ruby file executed directly run! if app_file == $0 end # Start Web App from the ./bin directory in deamon mode at port 9494 # $ thin -d -p 9494