note

	description:

		"Fileset"

	library: "Gobo Eiffel Ant"
	copyright: "Copyright (c) 2001-2018, Sven Ehrke and others"
	license: "MIT License"
	date: "$Date$"
	revision: "$Revision$"

class GEANT_FILESET

inherit

	ANY

	KL_SHARED_FILE_SYSTEM
		export {NONE} all end

	KL_IMPORTED_STRING_ROUTINES

create

	make

feature {NONE} -- Initialization

	make (a_project: GEANT_PROJECT)
			-- Create a new fileset.
		local
			a_tester: UC_STRING_EQUALITY_TESTER
		do
			project := a_project
			create {DS_HASH_SET [GEANT_FILESET_ENTRY]} filenames.make_equal (20)
			create {DS_HASH_SET [STRING]} single_includes.make (20)
			create a_tester
			single_includes.set_equality_tester (a_tester)
			create {DS_HASH_SET [STRING]} single_excludes.make (20)
			single_excludes.set_equality_tester (a_tester)
			set_filename_variable_name ("fs.filename")
			set_mapped_filename_variable_name ("fs.mapped_filename")
			force := True
		ensure
			filename_variable_name_set: filename_variable_name.is_equal ("fs.filename")
			mapped_filename_variable_name_set: mapped_filename_variable_name.is_equal ("fs.mapped_filename")
			force_is_true: force = True
		end

feature -- Access

	project: GEANT_PROJECT
			-- Project to which Current belongs to

	dir_name: detachable STRING
			-- Current working directory for execution

	directory_name: detachable STRING
			-- Name of directory serving as root for recursive scanning

	include_wc_string: detachable STRING
			-- Wildcard against which filenames are matched for inclusion

	exclude_wc_string: detachable STRING
			-- Wildcard against which filenames are matched for exclusion

	convert_to_filesystem: BOOLEAN
			-- Are `item_filename' and `item_mapped_filename' in the format
			-- of the current filesystem?
			-- Note: Result = false implies that both features' Result is in unix format

	map: detachable GEANT_MAP
			-- Map for filenames

	has_map: BOOLEAN
			-- Does current fileset has a map?
		do
			Result := map /= Void
		ensure
			definition: Result = (map /= Void)
		end

	force: BOOLEAN
			-- Should all selected files be included in `filenames' regardless of their timestamp?
			-- True: all selected files are included in `filenames'.
			-- False: only those files are included in `filename' for which the timestamp is
			--   newer than the timestamp of their corresponding files specified by `map'.
			--   If `map' is Void the mapped filename and the source filename are the same
			--   which means no file is included.
			-- default value: False

	concat: BOOLEAN
			-- Should `directory_name' be prepended to matched filenames?

	filename_directory_name: detachable STRING
			-- Name of directory prepended to matched filenames

	mapped_filename_directory_name: detachable STRING
			-- Name of directory prepended to mapped filenames

	filename_variable_name: STRING
			-- Name of project variable to which `item_filename' is assigned to
			-- during iterations;
			-- default: 'fs.filename'

	mapped_filename_variable_name: STRING
			-- Name of project variable to which `item_mapped_filename' is assigned to
			-- during iterations;
			-- default: 'fs.mapped_filename'

	item_filename: STRING
			-- Filename at current cursor
		require
			not_off: not off
		do
			if not convert_to_filesystem then
				Result := filenames.item_for_iteration.filename
			else
				Result := filenames.item_for_iteration.filename_converted
			end
		ensure
			item_filename_not_void: Result /= Void
		end

	item_mapped_filename: STRING
			-- Mapped filename at current cursor
		require
			not_off: not off
		do
			if not convert_to_filesystem then
				Result := filenames.item_for_iteration.mapped_filename
			else
				Result := filenames.item_for_iteration.mapped_filename_converted
			end
		ensure
			item_mapped_filename_not_void: Result /= Void
		end

feature -- Status report

	is_executable: BOOLEAN
			-- Can element be executed?
		do
			Result := is_in_gobo_31_format or else is_in_gobo_32_format
			if Result then
				if is_in_gobo_31_format then
					Result := (attached directory_name as l_directory_name and then l_directory_name.count > 0)
					if not Result then
						project.log (<<"  [fileset] error: attribute 'directory' is mandatory">>)
					end
				end
			end
			if Result then
				Result := not attached include_wildcard as l_include_wildcard or else l_include_wildcard.is_compiled
				if not Result then
					project.log (<<"  [fileset] error: attribute 'include' is not valid">>)
				end
			end
			if Result then
				Result := not attached exclude_wildcard as l_exclude_wildcard or else l_exclude_wildcard.is_compiled
				if not Result then
					project.log (<<"  [fileset] error: attribute 'exclude' is not valid">>)
				end
			end
			if Result then
				Result := not attached map as l_map or else l_map.is_executable
				if not Result then
					project.log (<<"  [fileset] error: element 'map' is not defined correctly">>)
				end
			end
		ensure
			directory_name_not_void_and_not_empty: (Result and is_in_gobo_31_format) implies attached directory_name as l_directory_name and then l_directory_name.count > 0
			include_wildcard_compiled: Result implies (not attached include_wildcard as l_include_wildcard or else l_include_wildcard.is_compiled)
			exclude_wildcard_compiled: Result implies (not attached exclude_wildcard as l_exclude_wildcard or else l_exclude_wildcard.is_compiled)
			map_executable: Result implies (not attached map as l_map or else l_map.is_executable)
			correct_format: Result implies is_in_gobo_31_format or else is_in_gobo_32_format
		end

	is_in_gobo_31_format: BOOLEAN
			-- Is fileset setup for obsolete GOBO 3.1 format?
		do
			Result := directory_name /= Void and then filename_directory_name = Void and then
				mapped_filename_directory_name = Void and then dir_name = Void
		ensure
			definition: Result implies directory_name /= Void and then
				filename_directory_name = Void and then mapped_filename_directory_name = Void and then
				dir_name = Void
		end

	is_in_gobo_32_format: BOOLEAN
			-- Is fileset setup for GOBO 3.2 format?
		do
			Result := directory_name = Void and then not concat
		ensure
			definition: Result implies directory_name = Void and then not concat
		end

	are_project_variables_up_to_date: BOOLEAN
			-- If not `off' is project variable named `filename_variable_name' set to `item_filename' and
			-- project variable named `mapped_filename_variable_name' set to `item_mapped_filename'?
			-- And if `off' are project variables named `filename_variable_name' and
			-- `mapped_filename_variable_name' not existing?
		do
			if not off then
				Result := project.variables.has (filename_variable_name) and then
					STRING_.same_string (project.variables.item (filename_variable_name), item_filename) and then
					project.variables.has (mapped_filename_variable_name) and then
					STRING_.same_string (project.variables.item (mapped_filename_variable_name), item_mapped_filename)
			else
				Result := not (project.variables.has (filename_variable_name) or
					project.variables.has (mapped_filename_variable_name))
			end
		ensure
			filename_variable_name_exists: not off implies
				(Result implies project.variables.has (filename_variable_name))
			filename_variable_name_set: not off implies (Result implies
				STRING_.same_string (project.variables.item (filename_variable_name), item_filename))
			mapped_filename_variable_name_exists: not off implies
				(Result implies project.variables.has (mapped_filename_variable_name))
			mapped_filename_variable_name_set: not off implies (Result implies
				STRING_.same_string (project.variables.item (mapped_filename_variable_name), item_mapped_filename))
			filename_variable_name_not_exists: off implies
				(Result implies not project.variables.has (filename_variable_name))
			mapped_filename_variable_name_not_exists: off implies
				(Result implies not project.variables.has (mapped_filename_variable_name))
		end

	is_empty: BOOLEAN
			-- Is fileset empty?
		do
			Result := filenames.is_empty
		end

	after: BOOLEAN
			-- Is there no valid position to right of cursor?
		do
			Result := filenames.after
		end

	off: BOOLEAN
			-- Is there no item at internal cursor position?
		do
			Result := filenames.off
		end

feature -- Element change

	set_dir_name (a_dir_name: STRING)
			-- Set `dir_name' to `a_dir_name'.
		require
			dir_name_not_void: a_dir_name /= Void
		do
			dir_name := a_dir_name
		ensure
			dir_name_set: dir_name = a_dir_name
		end

	set_directory_name (a_directory_name: STRING)
			-- Set `directory_name' to `a_directory_name'.
		require
			directory_name_not_void: a_directory_name /= Void
		do
			directory_name := a_directory_name
		ensure
			directory_name_set: directory_name = a_directory_name
		end

	set_include_wc_string (a_include_wc_string: STRING)
			-- Set `include_wc_string' to `a_include_wc_string' and
			-- make a compiled version available in `include_wildcard'
		require
			a_include_wc_string_not_void : a_include_wc_string /= Void
			a_include_wc_string_not_empty: a_include_wc_string.count > 0
		local
			l_include_wildcard: like include_wildcard
		do
			include_wc_string := a_include_wc_string
				-- Setup wildcard for include patterns:
			create {LX_DFA_WILDCARD} l_include_wildcard.compile (a_include_wc_string, True)
			include_wildcard := l_include_wildcard
			if not l_include_wildcard.is_compiled then
				project.log (<<"  [fileset] error: invalid include wildcard: '", a_include_wc_string, "%'">>)
			end
		ensure
			include_wc_string_set: include_wc_string = a_include_wc_string
		end

	set_exclude_wc_string (a_exclude_wc_string: STRING)
			-- Set `exclude_wc_string' to `a_exclude_wc_string' and
			-- make a compiled version available in `exclude_wildcard'
		require
			a_exclude_wc_string_not_void : a_exclude_wc_string /= Void
			a_exclude_wc_string_not_empty: a_exclude_wc_string.count > 0
		local
			l_exclude_wildcard: like exclude_wildcard
		do
			exclude_wc_string := a_exclude_wc_string
				-- Setup wildcard for exclude patterns:
			create {LX_DFA_WILDCARD} l_exclude_wildcard.compile (a_exclude_wc_string, True)
			exclude_wildcard := l_exclude_wildcard
			if not l_exclude_wildcard.is_compiled then
				project.log (<<"  [fileset] error: invalid exclude wildcard: '", a_exclude_wc_string, "%'">>)
			end
		ensure
			exclude_wc_string_set: exclude_wc_string = a_exclude_wc_string
		end

	set_convert_to_filesystem (b: BOOLEAN)
			-- Set `convert_to_filesystem' to `b'.
		do
			convert_to_filesystem := b
		ensure
			convert_to_filesystem_set: convert_to_filesystem = b
		end

	set_map (a_map: like map)
			-- Set `map' to `a_map'.
		require
			a_map_not_void: a_map /= Void
		do
			map := a_map
		ensure
			map_set: map = a_map
		end

	set_force (b: BOOLEAN)
			-- Set `force' to `b'.
		do
			force := b
		ensure
			force_set: force = b
		end

	set_concat (b: BOOLEAN)
			-- Set `concat' to `b'.
		do
			concat := b
		ensure
			concat_set: concat = b
		end

	set_filename_directory_name (a_filename_directory_name: STRING)
			-- Set `filename_directory_name' to `a_filename_directory_name'.
		require
			filename_directory_name_not_void: a_filename_directory_name /= Void
		do
			filename_directory_name := a_filename_directory_name
		ensure
			filename_directory_name_set: filename_directory_name = a_filename_directory_name
		end

	set_mapped_filename_directory_name (a_mapped_filename_directory_name: STRING)
			-- Set `mapped_filename_directory_name' to `a_mapped_filename_directory_name'.
		require
			mapped_filename_directory_name_not_void: a_mapped_filename_directory_name /= Void
		do
			mapped_filename_directory_name := a_mapped_filename_directory_name
		ensure
			mapped_filename_directory_name_set: mapped_filename_directory_name = a_mapped_filename_directory_name
		end

	set_filename_variable_name (a_filename_variable_name: STRING)
			-- Set `filename_variable_name' to `a_filename_variable_name'.
		require
			a_filename_variable_name_not_void: a_filename_variable_name /= Void
			a_filename_variable_name_not_empty: a_filename_variable_name.count > 0
		do
			filename_variable_name := a_filename_variable_name
		ensure
			filename_variable_name_set: filename_variable_name = a_filename_variable_name
		end

	set_mapped_filename_variable_name (a_mapped_filename_variable_name: STRING)
			-- Set `mapped_filename_variable_name' to `a_mapped_filename_variable_name'.
		require
			a_mapped_filename_variable_name_not_void: a_mapped_filename_variable_name /= Void
			a_mapped_filename_variable_name_not_empty: a_mapped_filename_variable_name.count > 0
		do
			mapped_filename_variable_name := a_mapped_filename_variable_name
		ensure
			mapped_filename_variable_name_set: mapped_filename_variable_name = a_mapped_filename_variable_name
		end

feature -- Element change

	add_fileset_entry_if_necessary (a_filename: STRING)
			-- Add new GEANT_FILESET_ENTRY created from `a_filename'
			-- to `filenames'.
			-- If force is set to 'false' do this only if the file named
			--   `map.mapped_filename (a_filename)' (if map /= Void)
			--   `a_filename' (if map = Void)
			-- is older than the file name `a_filename'.
		require
			a_filename_not_void: a_filename /= Void
			a_filename_not_empty: a_filename.count > 0
		local
			a_entry: GEANT_FILESET_ENTRY
			an_filename: STRING
			an_mapped_filename: STRING
		do
			project.trace_debug (<<"  [*fileset] trying to add: '", a_filename, "%'">>)
			an_filename := a_filename
			if attached map as l_map then
				an_mapped_filename := l_map.mapped_filename (an_filename)
			else
				an_mapped_filename := an_filename
			end
				-- Remove support for 'gobo32_format' after obsolete period:
			if concat and attached directory_name as l_directory_name then
				an_mapped_filename := unix_file_system.pathname (l_directory_name, an_mapped_filename)
			end

			if attached filename_directory_name as l_filename_directory_name then
				an_filename := unix_file_system.pathname (l_filename_directory_name, an_filename)
			end
			if attached mapped_filename_directory_name as l_mapped_filename_directory_name then
				an_mapped_filename := unix_file_system.pathname (l_mapped_filename_directory_name,
					an_mapped_filename)
			end
			if force or else is_file_outofdate (an_filename, an_mapped_filename) then
				create a_entry.make (an_filename, an_mapped_filename)
				filenames.force_last (a_entry)
			end
		end

	remove_fileset_entry (a_filename: STRING)
			-- Remove entry with name equal to `a_filename' if existing.
		local
			a_entry: GEANT_FILESET_ENTRY
		do
			project.trace_debug (<<"  [*fileset] removing: '", a_filename, "%'">>)
			create a_entry.make (a_filename, a_filename)
			filenames.remove (a_entry)
		end

	add_single_include (a_filename: STRING)
			-- Add `a_filename' to list of single filenames to include into fileset.
		require
			a_filename_not_void: a_filename /= Void
		do
			single_includes.force_last (a_filename)
		end

	add_single_exclude (a_filename: STRING)
			-- Add `a_filename' to list of single filenames to exclude from fileset.
		require
			a_filename_not_void: a_filename /= Void
		do
			single_excludes.force_last (a_filename)
		end

feature -- Cursor movement

	start
			-- Move cursor to first position.
		do
			filenames.start
			if off then
				remove_project_variables
			else
				update_project_variables
			end
		ensure
			empty_behavior: is_empty implies after
			project_variables_up_to_date: are_project_variables_up_to_date
		end

	forth
	   		-- Move cursor to next position.
		require
			not_after: not after
		do
			filenames.forth
			if off then
				remove_project_variables
			else
				update_project_variables
			end
		ensure
			project_variables_up_to_date: are_project_variables_up_to_date
		end

	go_after
			-- Move cursor to `after' position.
		do
			remove_project_variables
			filenames.go_after
		ensure
			project_variables_up_to_date: are_project_variables_up_to_date
		end

feature -- Execution

	execute
			-- Populate `filenames'.
		local
			al_directory_name: STRING
			cs: DS_SET_CURSOR [STRING]
			a_old_cwd: STRING
		do
			remove_project_variables

			a_old_cwd := file_system.current_working_directory
				-- Change to directory `dir_name' if specified:
			if attached dir_name as l_dir_name then
				project.trace_debug (<<"  [*fileset] dir: '", l_dir_name, "%'">>)
				project.trace_debug (<<"  [*fileset] changing to directory: '", l_dir_name, "%'">>)
				file_system.set_current_working_directory (l_dir_name)
			end
			if attached directory_name as l_directory_name then
				project.trace_debug (<<"  [*fileset] directory_name: ", l_directory_name>>)
			end
			if attached include_wc_string as l_include_wc_string then
				project.trace_debug (<<"  [*fileset] include_wc_string: ", l_include_wc_string>>)
			end
			if attached filename_directory_name as l_filename_directory_name then
				project.trace_debug (<<"  [*fileset] filename_directory: ", l_filename_directory_name>>)
			end
			if attached mapped_filename_directory_name as l_mapped_filename_directory_name then
				project.trace_debug (<<"  [*fileset] mapped_filename_directory: ", l_mapped_filename_directory_name>>)
			end
			if attached directory_name as l_directory_name then
				al_directory_name := unix_file_system.canonical_pathname (l_directory_name)
			else
				create al_directory_name.make_from_string (".")
			end
				-- Add entries from filesystem scan:
			scan_internal (al_directory_name, al_directory_name)
				-- Add single includes:
			cs := single_includes.new_cursor
			from cs.start until cs.after loop
				add_fileset_entry_if_necessary (cs.item)
				cs.forth
			end
				-- Remove single excludes:
			cs := single_excludes.new_cursor
			from cs.start until cs.after loop
				remove_fileset_entry (cs.item)
				cs.forth
			end
			if project.options.debug_mode then
				from start until after loop
					project.trace_debug (<<"  [*fileset] entry: [", item_filename, ", ", item_mapped_filename, "]">>)
					forth
				end
			end
				-- Change back to previous directory:
			project.trace_debug (<<"  [*fileset] changing to directory: '", a_old_cwd, "%'">>)
			file_system.set_current_working_directory (a_old_cwd)
		end

	include_wildcard: detachable LX_WILDCARD
			-- Expression defining filenames for inclusion

	exclude_wildcard: detachable LX_WILDCARD
			-- Expression defining filenames for exclusion

feature {NONE} -- Implementation/Access

	filenames: DS_SET [GEANT_FILESET_ENTRY]
			-- Files underneath directory named `directory_name' (if `is_in_gobo_31_format')
			-- Files underneath current working directory (if `is_in_gobo_32_format')
			-- matching expressions in `include_wc_string' and not matching
			-- expressions in `exclude_wc_string' with their corresponding
			-- mapped filename if `has_map';
			-- available after execute has been performed.

	single_includes: DS_SET [STRING]
			-- Filenames to be included in `filenames'

	single_excludes: DS_SET [STRING]
			-- Filenames to be excluded from `filenames'

feature {NONE} -- Implementation/Processing

	scan_internal (a_directory_name, a_root_directory_name: STRING)
			-- Scan directory named `a_directory_name' recursivley;
			-- put filenames found matching `include_wildcard' and not matching `exclude_wildcard'
			-- into `filenames';
		require
			a_directory_name_not_void: a_directory_name /= Void
			a_root_directory_name_not_void: a_root_directory_name /= Void
		local
			a_dir: KL_DIRECTORY
			a_name: STRING
			s: STRING
			smatch: STRING
		do
			create a_dir.make (a_directory_name)
			a_dir.open_read
			if a_dir.is_open_read then
				from a_dir.read_entry until a_dir.end_of_input loop
					a_name := a_dir.last_entry
					if
						not STRING_.same_string (a_name, file_system.relative_current_directory) and
						not STRING_.same_string (a_name, file_system.relative_parent_directory)
					then
						s := unix_file_system.pathname (a_directory_name, a_name)
							-- Recurse for directories:
						if file_system.is_directory_readable (s) then
							scan_internal (s, a_root_directory_name)
						else
								-- Handle files:
--!!							project.trace_debug (<<"filename: ", s, "%N">>)
							if is_in_gobo_31_format then
								smatch := s.substring (a_root_directory_name.count + 2, s.count)	-- 2 because of '/'
							else
								smatch := s.substring (3, s.count)	-- 3 because of './'
							end
--!!							project.trace_debug (<<"  trying to match: ", smatch, "%N">>)
							if attached include_wildcard as l_include_wildcard and then l_include_wildcard.recognizes (smatch) then
								add_fileset_entry_if_necessary (smatch)
							end
							if attached exclude_wildcard as l_exclude_wildcard and then l_exclude_wildcard.recognizes (smatch) then
								remove_fileset_entry (smatch)
							end
						end
					end
					a_dir.read_entry
				end
				a_dir.close
			end
		end

	is_file_outofdate (a_first_filename, a_second_filename: STRING): BOOLEAN
			-- Is timestamp of file named `a_second_filename' older than
			-- timestamp of file named `a_first_filename' or doesn't exist at all?
		require
			a_first_filename_not_void: a_first_filename /= Void
			a_second_filename_not_void: a_second_filename /= Void
			-- first_file_exists: file_system.file_exists (first_filename)
		local
			a_first_time: INTEGER
			a_second_time: INTEGER
		do
			if not file_system.file_exists (a_second_filename) then
				Result := True
			else
				a_first_time := file_system.file_time_stamp (a_first_filename)
				a_second_time := file_system.file_time_stamp (a_second_filename)
				Result := a_second_time < a_first_time
			end
		end

	update_project_variables
			-- Set project variable with name `filename_variable_name' to `item_filename' and
			-- project variable with name `mapped_filename_variable_name' to `item_mapped_filename'.
		require
			not_off: not off
		do
			project.variables.set_variable_value (filename_variable_name, item_filename)
			project.variables.set_variable_value (mapped_filename_variable_name, item_mapped_filename)
		ensure
			project_variables_set: are_project_variables_up_to_date
		end

	remove_project_variables
			-- Remove project variable with name `filename_variable_name' and
			-- project variable with name `mapped_filename_variable_name'.
		do
			project.trace_debug (<<"  [*fileset] removing project variables '",
				filename_variable_name, "' and '", mapped_filename_variable_name, "'">>)
			project.variables.remove (filename_variable_name)
			project.variables.remove (mapped_filename_variable_name)
		ensure
			project_variables_removed: not project.variables.has (filename_variable_name) and
				not project.variables.has (mapped_filename_variable_name)
		end

invariant

	filename_variable_name_not_void: filename_variable_name /= Void
	filename_variable_name_not_empty: filename_variable_name.count > 0
	mapped_filename_variable_name_not_void: mapped_filename_variable_name /= Void
	mapped_filename_variable_name_not_empty: mapped_filename_variable_name.count > 0

end