#!/usr/bin/ruby # # This file is part of CPEE-LOGGING-XES-YAML. # # CPEE-LOGGING-XES-YAML is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # CPEE-LOGGING-XES-YAML is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for # more details. # # You should have received a copy of the GNU Lesser General Public License along with # CPEE-LOGGING-XES-YAML (file LICENSE in the main directory). If not, see # . require 'rubygems' require 'optparse' require 'fileutils' require 'xml/smart' require 'yaml' require 'typhoeus' require 'stringio' require 'typhoeus' require 'date' require 'msgpack' require 'csv' def wrap(s, width=78, indent=19, extra_indent=2) lines = [] line, s = s[0..indent-1], s[indent..-1] s.split(/\n/).each_with_index do |ss,i| ss.split(/[ \t]+/).each_with_index do |word,j| if line.size + word.size >= width lines << line line = (" " * (indent)) + word else line << " " if i > 0 || j != 0 line << (" " * (extra_indent)) if i > 0 && j == 0 line << word end end lines << line if line line = (" " * (indent-1)) end return lines.join "\n" end TEMPLATE_XES_XML = <<-END END def rec_type(it) if it.is_a?(String) && it =~ /^[\dT:+.-]+$/ && (Time.parse(it) rescue nil) 'x:date' elsif it.is_a? Float 'x:float' elsif it.is_a? Integer 'x:int' elsif it.is_a? String 'x:string' end end def rec_a_insert(event,node,level=0) event.each do |i| tnode = node case i when Hash rec_insert(i,tnode,level+1) when Array rec_a_insert(i,tnode,level+1) when String node.add(rec_type(i), 'key' => i, 'value' => (i.empty? ? "__UNSPECIFIED__" : i)) end end end def rec_insert(event,node,level=0) event.each do |k,v| case v when String node.add(rec_type(v), 'key' => k, 'value' => (v.empty? ? "__UNSPECIFIED__" : v)) when Integer node.add(rec_type(v), 'key' => k, 'value' => v) when Float node.add(rec_type(v), 'key' => k, 'value' => v) when Array tnode = node.add('x:list', 'key' => k) rec_a_insert(v,tnode,level+1) when Hash tnode = node.add('x:string', 'key' => k) rec_insert(v,tnode) end end end def follow(fname,io,copy,deep=0,index=nil) tname = if fname =~ /\.xes\.shift\.yaml/ File.basename(fname,'.xes.shift.yaml') elsif fname =~ /\.xes\.yaml/ File.basename(fname,'.xes.yaml') end if copy File.write(File.basename(fname),io.read) io.rewind end YAML.load_stream(io) do |e| if name = e.dig('log','trace','cpee:name') index.write " " * deep + name + " (#{tname}) - #{e.dig('log','trace','concept:name')}\n" end if e.dig('event','cpee:lifecycle:transition') == 'task/instantiation' base = e.dig('event','raw') val = base.dig('CPEE-INSTANCE') rescue nil if val.nil? val = File.basename(base) end uuid = base.dig('CPEE-INSTANCE-UUID') rescue nil if uuid react File.dirname(fname) + "/#{uuid}.xes.yaml", copy, deep + 2, index end end end end def react(name,copy=false,deep=0,index=nil) index ||= File.open('index.txt','a') if name.nil? help elsif name =~ /^https?:\/\// res = Typhoeus.get(name) if res.success? file = Tempfile.new('sic') file.write(res.body) file.rewind follow name, file, copy, deep, index file.close file.unlink end elsif File.exist? name follow name, File.open(name), copy, deep, index else help end end def extract(path) unlink = false if path =~ /^http.*/ unlink = true text = Tempfile.new('extract-model-download') request = Typhoeus::Request.new(path) request.on_headers do |response| if response.code != 200 raise "Request failed" end end request.on_body do |chunk| text.write(chunk) end request.on_complete do |response| text.rewind end request.run else text = File.open(path) end yaml = Psych.load_stream(text) changes = [] info = yaml.shift uuid = info.dig('log','trace','cpee:instance') yaml.each do |el| if el['event']['id:id'] == 'external' && (el.dig('event','cpee:lifecycle:transition') == 'endpoints/change' || el.dig('event','cpee:lifecycle:transition') == 'dataelements/change'|| el.dig('event','cpee:lifecycle:transition') == 'description/change') changes.push(el) end end changes.sort! { |a,b| DateTime.strptime(a.dig('event','time:timestamp'),'%Y-%m-%dT%H:%M:%S.%L%:z') <=> DateTime.strptime(b.dig('event','time:timestamp'),'%Y-%m-%dT%H:%M:%S.%L%:z') } de = ep = desc = nil counter = 0 changes.each do |change| if change.dig('event','cpee:lifecycle:transition') == 'dataelements/change' de = change.dig('event','data') end if change.dig('event','cpee:lifecycle:transition') == 'endpoints/change' ep = change.dig('event','data') end if change.dig('event','cpee:lifecycle:transition') == 'description/change' desc = change.dig('event','cpee:description') end if change.dig('event','cpee:lifecycle:transition') == 'description/change' || change.dig('event','cpee:lifecycle:transition') == 'endpoints/change' yield uuid, de, ep, desc, counter if block_given? counter += 1 end end text.close text.unlink if unlink [de, ep, desc] end exname = File.basename($0) ARGV.options { |opt| opt.summary_indent = ' ' * 2 opt.summary_width = 16 opt.banner = "Usage:\n#{opt.summary_indent}#{exname} new [DIR] | view [URI] | copy [URI]\n" opt.on("Options:") opt.on("--help", "-h", "This text") { puts opt; exit } opt.on("") opt.on(wrap("\"#{exname}\" will be call \"c\" in the examples for each command.")) exname = 'c' opt.on("") opt.on(wrap("new [DIR] scaffolds a sample logging service. Add a handler to a cpee instance to experience the pleasure.")) opt.on("") opt.on(wrap("view [LOG] view the dependencies between processes and subprocesses. Works for local and remote logs. Examples:\n#{exname} view https://cpee.org/log/123.xes.yaml\n#{exname} view https://cpee.org/log/a.xes.yaml > index.txt\n#{exname} view ~/log/logs/456.xes.yaml")) opt.on("") opt.on(wrap("copy [LOG] copy dependent processes and subprocesses to the current directory. Works for local and remote logs. Examples: \n#{exname} copy https://cpee.org/log/123.xes.yaml\n#{exname} copy ~/log/logs/456.xes.yaml")) opt.on("") opt.on(wrap("extract-all [LOG] extract cpee testset from cpee xes-yaml log. Works for local and remote logs. Write logs to files in folder named like the instance uuid contained in the log. Examples: \n#{exname} extract https://cpee.org/log/123.xes.yaml\n#{exname} extract ~/log/logs/456.xes.yaml")) opt.on("") opt.on(wrap("extract-last [LOG] extract cpee testset from cpee xes-yaml log. Works for local and remote logs. When called without [LOG], models for all log files in the current directory are extracted. Examples:\n#{exname} extract https://cpee.org/log/123.xes.yaml\n#{exname} extract ~/log/logs/456.xes.yaml")) opt.on("") opt.on(wrap("index [LOG] creates an index for a log file, for more efficient parsing. When called without [LOG], indexes all log files in the current directory. Examples:\n#{exname} index https://cpee.org/log/123.xes.yaml\n#{exname} index ~/log/logs/456.xes.yaml")) opt.on("") opt.on(wrap("to-xes-xml [LOG] convert cpee xes-yaml to xes-xml. Works for local and remote logs. When called without [LOG], all log files in the current directory are converted. Examples:\n#{exname} to-xes-xml https://cpee.org/log/123.xes.yaml\n#{exname} to-xes-xml ~/log/logs/456.xes.yaml")) opt.parse! } if (ARGV.length < 1 || ARGV.length > 2) puts ARGV.options exit elsif ARGV.length == 2 command = ARGV[0] path = ARGV[1] elsif ARGV.length == 1 && %w{index extract-last to-xes-xml}.include?(ARGV[0]) command = ARGV[0] end if command == 'new' if !File.exist?(path) FileUtils.cp_r(File.join(__dir__,'..','server'),path) FileUtils.mkdir(File.join(path,'logs')) rescue nil else puts 'Directory already exists.' end elsif command == 'view' react path, false elsif command == 'copy' react path, true elsif command == 'extract-all' extract(path) do |uuid, de, ep, desc, version| xml = XML::Smart.string('ruby') dataelements = xml.root().add('dataelements') endpoints = xml.root().add('endpoints') description = xml.root().add('description').add(XML::Smart.string('').root()) unless de.nil? de.each do |d| dataelements.add(d['name'],d['value']) end end unless ep.nil? ep.each do |e| endpoints.add(e['name'],e['value']) end end unless desc.nil? description.replace_by(XML::Smart.string(desc).root()) end dirname = File.join(uuid) filename = File.join(dirname,"#{uuid}_#{version}.xml") Dir.mkdir(dirname) unless Dir.exist?(dirname) File.write(filename, xml.to_s()) end elsif command == 'extract-last' path = if path [path] else Dir.glob('*.xes.yaml') end path.each do |f| de, ep, desc = extract(f) xml = XML::Smart.string('ruby') dataelements = xml.root().add('dataelements') endpoints = xml.root().add('endpoints') description = xml.root().add('description').add(XML::Smart.string('').root()) unless de.nil? de.each do |d| dataelements.add(d['name'],d['value']) end end unless ep.nil? ep.each do |e| endpoints.add(e['name'],e['value']) end end unless desc.nil? description.replace_by(XML::Smart.string(desc).root()) end File.write(f + '.model', xml.to_s) end elsif command == 'index' path = if path [path] else Dir.glob('*.xes.yaml') end path.each do |f| index = [] io = File.open(f) while not io.eof? start = io.pos docs = io.readline("---\n",chomp: true) doc = YAML::load(docs, permitted_classes: [Time]) if doc transition = doc.dig('event','cpee:lifecycle:transition') if transition =~ /^(activity\/calling|activity\/receiving|task\/instantiation)/ endpoint = doc.dig('event','concept:endpoint') uuid = doc.dig('event','cpee:activity_uuid') transition = case transition when 'activity/calling' 'c' when 'activity/receiving' 'r' when 'task/instantiation' 'i' end index << { :e => endpoint.to_s, :u => uuid.to_s, :t => transition.to_s, :s => start.to_i, :l => docs.length.to_i } end end end io.close CSV.open(f + '.index.csv', 'w') do |csv| index.each do |e| csv << e.values end end nindex = index.group_by{ |a| a[:u] }.collect do |k,v| [v[0][:e], v.collect{ |a| [ a[:t], {:s => a[:s], :l => a[:l]} ] } ] end File.write(f + '.index', MessagePack.pack(nindex)) end elsif command == 'to-xes-xml' path = if path [path] else Dir.glob('*.xes.yaml') end path.each do |f| xml = XML::Smart.string(TEMPLATE_XES_XML) xml.register_namespace 'x', 'http://www.xes-standard.org/' io = File.open(f) YAML.load_stream(io) do |e| if trace = e.dig('log','trace') trace.each do |t,tv| xml.find('//x:trace').each do |ele| ele.add('x:string', 'key' => t, 'value' => tv) end end end if e.dig('event') xml.find('//x:trace').each do |node| rec_insert(e.dig('event'),node.add('x:event')) end end end File.write(File.basename(f,'.xes.yaml') + '.xes.xml', xml.to_s) end else puts ARGV.options end