#!/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