logicgraph_parser.rb revision e646a91b592d4badd9d70a91f2bbd528a066f90d
c636315472e4f87313af7be30b7fbcad4b8ca8a4Stephen Gallagher# This module parses the logic graph outputed by Hets.
c636315472e4f87313af7be30b7fbcad4b8ca8a4Stephen Gallagher#
fd5a4eacd56700ffb08a73121aeacdc806cb0132Sumit Bose# It is a use-once-and-discard code and for this reason it does not have
8b1f525acd20f36c836e827de3c251088961c5d9Stephen Gallagher# regression tests.
8b1f525acd20f36c836e827de3c251088961c5d9Stephen Gallagher#
8b1f525acd20f36c836e827de3c251088961c5d9Stephen Gallagher# Author:: Daniel Couto Vale (mailto:danielvale@uni-bremen.de)
8b1f525acd20f36c836e827de3c251088961c5d9Stephen Gallagher# Copyright:: Copyright (c) 2013 Bremen University, SFBTR8
8b1f525acd20f36c836e827de3c251088961c5d9Stephen Gallagher# License:: Distributed as a part of Ontohub.
84ae5edab16ad6be5e3be956cb6fa031c1428eb5Stephen Gallagher#
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallaghermodule LogicgraphParser
df4cc3a83c5d6700b6a09ff96cb4a6b1949b1aa9Stephen Gallagher
df4cc3a83c5d6700b6a09ff96cb4a6b1949b1aa9Stephen Gallagher class ParseException < Exception; end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # Parses the given string and executes the callback for each symbol
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def self.parse(input, callbacks)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # Create a new parser
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher parser = Nokogiri::XML::SAX::Parser.new(Listener.new(callbacks))
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # Feed the parser some XML
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher parser.parse(input)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # Listener for the SAX Parser
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher class Listener < Nokogiri::XML::SAX::Document
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose ROOT = 'LogicGraph'
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose LOGIC = 'logic'
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose COMORPHISM = 'comorphism'
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher SOURCE_SUBLOGIC = 'sourceSublogic'
2a552e43581c74f51205c7141ec9f6e9542509f8Stephen Gallagher TARGET_SUBLOGIC = 'targetSublogic'
2a552e43581c74f51205c7141ec9f6e9542509f8Stephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher DESCRIPTION = 'Description'
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher SERIALIZATION = 'Serialization'
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher PROVER = 'Prover'
d921c1eba437662437847279f251a0a5d8f70127Maxim CONSERVATIVITY = 'ConservativityChecker'
2cbdd12983eb85eddb90f64cfafb24eae5b448f4Jakub Hrozek CONSISTENCY = 'ConsistencyChecker'
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # the callback function is called for each Symbol tag
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def initialize(callbacks)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @callbacks = callbacks
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @path = []
eb2e21b764d03544d8161e9956d7f70b07b75f77Simo Sorce @current_logic = nil
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_language = nil
2a5790216f57e9bdfb2930d52860bb5300366536Jakub Hrozek @current_support = nil
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_logic_mapping = nil
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @mappings = Hash.new
4dd615c01357b8715711aad6820ba9595d3ad377Stephen Gallagher @logics = Hash.new
4b6a0d0b3d42e5fdb457f47d9adfa5e66b160256Stephen Gallagher @languages = Hash.new
e124844907ed6973915e4d56f5442ecd07535a12Jakub Hrozek @supports = Hash.new
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
b32159300fea63222d8dd9200ed634087704ea74Stephen Gallagher
b32159300fea63222d8dd9200ed634087704ea74Stephen Gallagher # Makes a logic mapping singleton for a given key
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def make_mapping(key)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @mappings[key] ||= begin
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher mapping = LogicMapping.new
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher mapping.iri = "http://purl.net/dol/logic-mapping/" + key
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher mapping.standardization_status = "Unofficial"
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher mapping
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
be1ef1c62ad13612be5e1f879476c24452a5d6d0Stephen Gallagher end
a3d176d116ceccd6a7547c128fab5df5cdd2c2b6Michal Zidek
a3d176d116ceccd6a7547c128fab5df5cdd2c2b6Michal Zidek # Make a logic singleton for a given key
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def make_logic(key)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @logics[key] ||= begin
4dd615c01357b8715711aad6820ba9595d3ad377Stephen Gallagher iri = "http://purl.net/dol/logics/" + key
4dd615c01357b8715711aad6820ba9595d3ad377Stephen Gallagher logic = Logic.find_by_iri iri
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher if logic.nil?
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher logic = Logic.new
558998ce664055a75595371118f818084d8f2b23Jan Cholasta logic.iri = iri
558998ce664055a75595371118f818084d8f2b23Jan Cholasta logic.name = key
9a3e40dc49c1e38bf58e45be5adff37615f3910bJan Cholasta logic.standardization_status = "Unofficial"
9a3e40dc49c1e38bf58e45be5adff37615f3910bJan Cholasta end
558998ce664055a75595371118f818084d8f2b23Jan Cholasta logic.description = nil
558998ce664055a75595371118f818084d8f2b23Jan Cholasta logic
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # Make a language singleton for a given key
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def make_language(key)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @languages[key] ||= begin
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher iri = "http://purl.net/dol/language/" + key
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher language = Language.find_by_iri iri
c737e1444fb186e349e59bfa9dac4995b720b4b1Jan Zeleny if language.nil?
f1828234a850dd28465425248a83a993f262918fPavel Březina language = Language.new
6ea6ec5cb7d9985e2730fb9d4657624d10aed4d8Nick Guay language.iri = iri
6ea6ec5cb7d9985e2730fb9d4657624d10aed4d8Nick Guay language.name = key
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher language.description = nil
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher language
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def make_support(logic_key, language_key)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @supports[logic_key] ||= {}
2827b0d03f7b6bafa504d22a5d7ca39cbda048b3Pavel Březina @supports[logic_key][language_key] ||= begin
2827b0d03f7b6bafa504d22a5d7ca39cbda048b3Pavel Březina logic = @logics[logic_key]
2827b0d03f7b6bafa504d22a5d7ca39cbda048b3Pavel Březina language = @languages[language_key]
9e80079370ff3b943832adc3c5ef430e64be0a0cJakub Hrozek support = Support.where(logic_id: logic, language_id: language).first
9e80079370ff3b943832adc3c5ef430e64be0a0cJakub Hrozek if support.nil?
9e80079370ff3b943832adc3c5ef430e64be0a0cJakub Hrozek support = Support.new
e7311aec8d691e5427317442387af1bc8fff3742Jan Cholasta support.logic = @logics[logic_key]
e7311aec8d691e5427317442387af1bc8fff3742Jan Cholasta support.language = @languages[language_key]
e7311aec8d691e5427317442387af1bc8fff3742Jan Cholasta end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher support
b9e5bd09a5ff7009537a18914dbebcf10498f592Sumit Bose end
b9e5bd09a5ff7009537a18914dbebcf10498f592Sumit Bose end
b9e5bd09a5ff7009537a18914dbebcf10498f592Sumit Bose
b9e5bd09a5ff7009537a18914dbebcf10498f592Sumit Bose # Parses the element opening tag
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def start_element(name, attributes)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @path << name
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher case name
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when ROOT
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher callback(:root, Hash[*[attributes]])
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when LOGIC
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher hash = Hash[*[attributes]]
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_logic = make_logic(hash['name'])
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_language = make_language(hash['name'])
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_support = make_support(hash['name'], hash['name'])
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher callback(:logic, @current_logic)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher callback(:language, @current_language)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher callback(:support, @current_support)
effcbdb12c7ef892f1fd92a745cb33a08ca4ba30Stephen Gallagher when COMORPHISM
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher hash = Hash[*[attributes]]
69aaef8719c5cf33ed1c4090fa313ba281bf8a02Jakub Hrozek @current_comorphism = make_mapping(hash['name'])
4dd615c01357b8715711aad6820ba9595d3ad377Stephen Gallagher if @path[-2] == SOURCE_SUBLOGIC
fe60346714a73ac3987f786731389320633dd245Pavel Březina # @current_comorphism.source = @current_source_sublogic
a6098862048d4bb469130b9ff21be3020d6f2c54Sumit Bose elsif @path[-2] == TARGET_SUBLOGIC
2d257ccf620ce1b611f89cec8f0a94c88c2f2881Sumit Bose # @current_comorphism.target = @current_target_sublogic
2d257ccf620ce1b611f89cec8f0a94c88c2f2881Sumit Bose else
e5e8252ec48bfdd4e7529debc705c8e090264b9aSumit Bose # Get attributes
e5e8252ec48bfdd4e7529debc705c8e090264b9aSumit Bose if hash['is_weakly_amalgamable'] == 'TRUE'
8359bf07a2e6c0181251ce8d5d9160dc57546c55Stephen Gallagher @current_comorphism.exactness = LogicMapping::EXACTNESSES[2]
e5e8252ec48bfdd4e7529debc705c8e090264b9aSumit Bose else
e5e8252ec48bfdd4e7529debc705c8e090264b9aSumit Bose @current_comorphism.exactness = LogicMapping::EXACTNESSES[0]
71e7918be3ca5d38794a16a17f6b4f19a24d51fcPavel Březina end
8359bf07a2e6c0181251ce8d5d9160dc57546c55Stephen Gallagher if hash['has_model_expansion'] == 'TRUE'
71e7918be3ca5d38794a16a17f6b4f19a24d51fcPavel Březina @current_comorphism.faithfulness = LogicMapping::FAITHFULNESSES[2]
71e7918be3ca5d38794a16a17f6b4f19a24d51fcPavel Březina else
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_comorphism.faithfulness = LogicMapping::FAITHFULNESSES[0]
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
150b76e13b7c4f3ccf1d709bf517ca2af6b2c9a2Jakub Hrozek if hash['source']
150b76e13b7c4f3ccf1d709bf517ca2af6b2c9a2Jakub Hrozek @current_comorphism.source = make_logic(hash['source'])
8359bf07a2e6c0181251ce8d5d9160dc57546c55Stephen Gallagher end
150b76e13b7c4f3ccf1d709bf517ca2af6b2c9a2Jakub Hrozek if hash['target']
150b76e13b7c4f3ccf1d709bf517ca2af6b2c9a2Jakub Hrozek @current_comorphism.target = make_logic(hash['target'])
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
f232789430a080384188d5da89b19d874cf17513Jakub Hrozek if !@current_comorphism.source.nil? && !@current_comorphism.target.nil?
150b76e13b7c4f3ccf1d709bf517ca2af6b2c9a2Jakub Hrozek callback(:logic_mapping, @current_comorphism)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when SOURCE_SUBLOGIC
bf5a808fa92007c325c3996e79694badfab201d4Stephen Gallagher hash = Hash[*[attributes]]
bf5a808fa92007c325c3996e79694badfab201d4Stephen Gallagher @current_source_sublogic = make_logic(hash['name'])
bf5a808fa92007c325c3996e79694badfab201d4Stephen Gallagher callback(:logic, @current_source_sublogic)
fa551077410019fb34460dc730950e93b62b2963Jakub Hrozek when TARGET_SUBLOGIC
fa551077410019fb34460dc730950e93b62b2963Jakub Hrozek hash = Hash[*[attributes]]
fa551077410019fb34460dc730950e93b62b2963Jakub Hrozek @current_target_sublogic = make_logic(hash['name'])
bf5a808fa92007c325c3996e79694badfab201d4Stephen Gallagher callback(:logic, @current_target_sublogic)
bf5a808fa92007c325c3996e79694badfab201d4Stephen Gallagher when DESCRIPTION
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when SERIALIZATION
bf5a808fa92007c325c3996e79694badfab201d4Stephen Gallagher hash = Hash[*[attributes]]
150b76e13b7c4f3ccf1d709bf517ca2af6b2c9a2Jakub Hrozek name = hash['name']
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher serialization = @current_language.serializations.create
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher serialization.name = name
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher serialization.extension = name
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher serialization.mimetype = name
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_language.serializations << serialization
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when PROVER
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when CONSERVATIVITY
effcbdb12c7ef892f1fd92a745cb33a08ca4ba30Stephen Gallagher when CONSISTENCY
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # Parses the element closing tag
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def end_element(name)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @path.pop
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher case name
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when ROOT
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when LOGIC
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher callback(:logic, @current_logic)
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose callback(:language, @current_language)
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose callback(:support, @current_support)
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose @current_logic = nil
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose @current_language = nil
90fd1bbd6035cdab46faa3a695a2fb2be6508b17Sumit Bose @current_support = nil
96453f402831275a39d5fb89c33c9776e148d03fStephen Gallagher when COMORPHISM
96453f402831275a39d5fb89c33c9776e148d03fStephen Gallagher if @path[-1] == SOURCE_SUBLOGIC
96453f402831275a39d5fb89c33c9776e148d03fStephen Gallagher elsif @path[-1] == TARGET_SUBLOGIC
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher else
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher callback(:logic_mapping, @current_comorphism)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
c7919a4fe41133cc466aa3d9431bfceee5784e7bJan Cholasta @current_comorphism = nil
b35f20cd8ecdc8308a3201e55752fb0443ec6ae4Jan Cholasta when SOURCE_SUBLOGIC
c7919a4fe41133cc466aa3d9431bfceee5784e7bJan Cholasta @current_axiom = nil
69aaef8719c5cf33ed1c4090fa313ba281bf8a02Jakub Hrozek when TARGET_SUBLOGIC
3b08dec5ee634f83ee18e1753d5ffe0ac5e3c458Jakub Hrozek @current_link = nil
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when DESCRIPTION
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when SERIALIZATION
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when PROVER
c7919a4fe41133cc466aa3d9431bfceee5784e7bJan Cholasta when CONSERVATIVITY
b35f20cd8ecdc8308a3201e55752fb0443ec6ae4Jan Cholasta when CONSISTENCY
c7919a4fe41133cc466aa3d9431bfceee5784e7bJan Cholasta end
69aaef8719c5cf33ed1c4090fa313ba281bf8a02Jakub Hrozek end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher # Parses a text node
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher def characters(text)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher case @path.last
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher when DESCRIPTION
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher if @current_logic
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_logic.description ||= ''
3b1df539835367cb81cd5ff0f9959947d5642e55Stephen Gallagher @current_logic.description << text
3b1df539835367cb81cd5ff0f9959947d5642e55Stephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher if @current_language
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @current_language.description ||= ''
96453f402831275a39d5fb89c33c9776e148d03fStephen Gallagher @current_language.description << text
96453f402831275a39d5fb89c33c9776e148d03fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
667db40da4db362d7ca0a1f7f1c4ba40fb71795aJakub Hrozek private
60e51fd2764291df2332f36ff478777627d92b57Sumit Bose
60e51fd2764291df2332f36ff478777627d92b57Sumit Bose def callback(name, args)
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher @callbacks[name].try :call, args
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher end
551aa6c36797ed720487f5974dcadabf19e6ff9fStephen Gallagher
1467daed400d6c186bd0c99c057c42e764309ff3Stephen Gallagherend
1467daed400d6c186bd0c99c057c42e764309ff3Stephen Gallagher