fmpvc 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +88 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/run_current +5 -0
- data/bin/setup +7 -0
- data/exe/fmpvc +71 -0
- data/fmpvc.gemspec +29 -0
- data/lib/fmpvc.rb +16 -0
- data/lib/fmpvc/DDR.rb +107 -0
- data/lib/fmpvc/FMPReport.rb +538 -0
- data/lib/fmpvc/configuration.rb +18 -0
- data/lib/fmpvc/version.rb +3 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b3a390e3cfe02d80a7f15cf4751e8772f3da314d
|
4
|
+
data.tar.gz: a89b88d4365f3279ec46d66a93e1ef5440db736d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cb2cabdb3e496445f15f4b9748284643e8f8a12e03eb3b455175af704e1b7562c4e1040d09934f13e983068dc49aa456704cb840cbe7c921f3b53be6bcacd421
|
7
|
+
data.tar.gz: d8c928f7b5e7f38ff02d79f4133008ff74a5a9ea19feda60d68bf859e1e47e94174ca914f59e837db66c97e65a19c0beca8171f83d47fa8c32ea321eb4bb6929
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
fmversioning
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.1.0
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Martin S. Boswell
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# FMPVC
|
2
|
+
|
3
|
+
`FMPVC` is a tool to help FileMaker developers by creating a set of text files which represent design objects in their databases (e.g. scripts, custom functions, layouts, etc.). `fmpvc` has no access to database content. The command, `fmpvc`, parses a Database Design Report (DDR) produced by FileMaker Pro Advanced and creates text files for each of the primary FileMaker objects described in the DDR. With those files the developer may:
|
4
|
+
|
5
|
+
1. use a version control system to track changes to databases
|
6
|
+
1. diff current objects with objects from previous versions (e.g. compare different versions of a script)
|
7
|
+
2. perform full text searches of a set of FileMaker datases (e.g. find all uses of a custom function in a solution)
|
8
|
+
3. obtain text representations of FileMaker objects (e.g. create a list of fields in a table)
|
9
|
+
|
10
|
+
DDR parsing is a one-way process, and there is currently no way to re-create a FileMaker file from DDR, Therefore, there is no direct way to restore, for instance, an old version of a FileMaker Script to your current working FileMaker file. The best we can do is retrieve the previous text version and examine it to recreate the FileMaker script manually. It is recommended that developers save clones of the FileMaker databases with each version control commit so that older versions of some of the items may be copied from the clone and pasted into latest versions (or, of course, entire databases may be restored).
|
11
|
+
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
`FMPVC` is a ruby gem and may be installed as follows:
|
16
|
+
|
17
|
+
$ gem install fmpvc
|
18
|
+
|
19
|
+
`FMPVC` requires both Nokogiri and ActiveSupport gems.
|
20
|
+
|
21
|
+
`FMPVC` requires ruby 2.0 or later. FMPVC has only been tested on Mac OS X, and in it's current state, it is unlikely to work properly in a Windows ruby environment (due to line endings, file paths, etc.). `fmpvc` should run fine on a Linux machine, but of course, the DDR generation requires FileMaker Pro Advanced, which is only available on Mac OS X and Windows.
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
By default the `fmpvc` command looks for a `Summary.xml` file in a directory called `fmp_ddr` in the current working directory. It reads the contents of that file and then processes each of the referenced report files (there is one for each FileMaker file included in the DDR). It produces a set of text files and directories representing each database inside of the directory, `fmp_text`. Example output looks like this:
|
26
|
+
|
27
|
+
├── fmp_clone/
|
28
|
+
│ └── FMServer_Sample Clone.fmp12
|
29
|
+
├── fmp_ddr/
|
30
|
+
│ ├── FMServer_Sample_fmp12.xml
|
31
|
+
│ └── Summary.xml
|
32
|
+
├── fmp_text/
|
33
|
+
│ └── FMServer_Sample_fmp12.xml/
|
34
|
+
│ ├── Accounts.txt
|
35
|
+
│ ├── CustomFunctions/
|
36
|
+
│ │ └── GetWorkDays (id 1).txt
|
37
|
+
...etc.
|
38
|
+
|
39
|
+
Usage in brief:
|
40
|
+
|
41
|
+
- change directory to the location where you'd like to save the DDR and clones and produce the text files
|
42
|
+
- create a directory, `fmp_ddr`, to hold DDR
|
43
|
+
- from FileMaker Pro Advanced, choose "Database Design Report..." from the Tools menu.
|
44
|
+
- choose project database files
|
45
|
+
- include all tables for each file
|
46
|
+
- include all DDR sections
|
47
|
+
- choose XML output
|
48
|
+
- save in the folder created above with the default name, `Summary`
|
49
|
+
- optionally, save clones of the same databases in a folder named, `fmp_clone` (this is not required)
|
50
|
+
- run the command `fmpvc`
|
51
|
+
|
52
|
+
Command-line options:
|
53
|
+
|
54
|
+
-h Show help message
|
55
|
+
-b, --base-dir <directory> Path to base directory (contains fmp_ddr/).
|
56
|
+
-d, --ddr-dir <directory> Look for DDR files in directory named <directory>.
|
57
|
+
-D, --text-dir <directory> Write text files in directory named <directory>.
|
58
|
+
-q, --quiet Suppress progress messages.
|
59
|
+
-s, --summary-file <filename> Look for Summary file named <filename>.
|
60
|
+
-t, --tree-file <filename> Set tree file name.
|
61
|
+
-T, --no-tree Don't create a tree file.
|
62
|
+
-Y, --no-yaml Suppress YAML in text files.
|
63
|
+
|
64
|
+
### YAML
|
65
|
+
|
66
|
+
By default, `fmpvc` appends a [YAML](http://www.yaml.org/spec/1.2/spec.html) representation of the FileMaker element to each text file. `fmpvc` doesn't capture all of the details of every FileMaker object, and even when it does, there are cases where there isn't an easy way to represent the object that is more concise or clear than the YAML description. In cases where `fmpvc` doesn't describe an aspect of a FileMaker element, by including a full YAML representation, changes will not be missed in a diff. The YAML is typically more human-readable and easier to diff than the original XML from the DDR.
|
67
|
+
|
68
|
+
YAML generation can be suppressed with the `-Y` command-line option.
|
69
|
+
|
70
|
+
### tree command
|
71
|
+
|
72
|
+
The tree command creates a textual representation of a directory and its filesystem objects. By default, `fmpvc` searches for `tree` in the shell's path and if it finds, one, uses it to create the file, `./fmp_text/tree.txt`.
|
73
|
+
|
74
|
+
`tree` is not available by default on Mac OS X, but can be easily installed with a package manager such as [homebrew](http://brew.sh/). Most Linux installations include the `tree` command.
|
75
|
+
|
76
|
+
## Development
|
77
|
+
|
78
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
79
|
+
|
80
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
1. Fork it ( https://github.com/MartinBoswell/fmpvc/fork )
|
85
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
86
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
87
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
88
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "fmpvc"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/run_current
ADDED
data/bin/setup
ADDED
data/exe/fmpvc
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Requires: Summary.xml, run from main directory (which includes fmp_{clone,ddr,text}/)
|
3
|
+
|
4
|
+
require 'fmpvc'
|
5
|
+
require 'optparse'
|
6
|
+
include FMPVC
|
7
|
+
|
8
|
+
FMPVC.configure {}
|
9
|
+
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
# opts.banner = "Usage: #{$0} [options] [base_directory_path]"
|
12
|
+
opts.on('-h', 'Show help message') do
|
13
|
+
puts opts
|
14
|
+
exit
|
15
|
+
end
|
16
|
+
opts.on('-b', '--base-dir <directory>', 'Path to base directory (contains fmp_ddr/).') do |b|
|
17
|
+
FMPVC.configuration.ddr_basedir = b
|
18
|
+
end
|
19
|
+
opts.on('-d', '--ddr-dir <directory>', 'Look for DDR files in directory named <directory>.') do |d|
|
20
|
+
FMPVC.configuration.ddr_dirname = d
|
21
|
+
end
|
22
|
+
opts.on('-D', '--text-dir <directory>', 'Write text files in directory named <directory>.') do |d|
|
23
|
+
FMPVC.configuration.text_dirname = d
|
24
|
+
end
|
25
|
+
opts.on('-q', '--quiet', 'Suppress progress messages.') do
|
26
|
+
FMPVC.configuration.quiet = true
|
27
|
+
end
|
28
|
+
opts.on('-s', '--summary-file <filename>', 'Look for Summary file named <filename>.') do |s|
|
29
|
+
FMPVC.configuration.ddr_filename = s
|
30
|
+
end
|
31
|
+
opts.on('-t', '--tree-file <filename>', 'Set tree file name.') do |t|
|
32
|
+
FMPVC.configuration.tree_filename = t
|
33
|
+
end
|
34
|
+
opts.on('-T', '--no-tree', "Don't create a tree file.") do
|
35
|
+
FMPVC.configuration.tree_filename = nil
|
36
|
+
end
|
37
|
+
opts.on('-Y', '--no-yaml', 'Suppress YAML in text files.') do
|
38
|
+
FMPVC.configuration.yaml = false
|
39
|
+
end
|
40
|
+
end.parse!
|
41
|
+
|
42
|
+
time_start = Time.new
|
43
|
+
|
44
|
+
ddr_dir = [FMPVC.configuration.ddr_basedir, FMPVC.configuration.ddr_dirname].join('/')
|
45
|
+
ddr = DDR.new(ddr_dir)
|
46
|
+
ddr.process_reports
|
47
|
+
ddr.write_summary
|
48
|
+
ddr.write_reports
|
49
|
+
|
50
|
+
# create a tree file, if available
|
51
|
+
unless ( FMPVC.configuration.tree_filename.nil? || FMPVC.configuration.tree_filename == '' )
|
52
|
+
require 'mkmf'
|
53
|
+
module MakeMakefile::Logging
|
54
|
+
# don't let MakeMake leave extraneous logs
|
55
|
+
@logfile = File::NULL
|
56
|
+
end
|
57
|
+
|
58
|
+
tree_command = find_executable 'tree'
|
59
|
+
if tree_command
|
60
|
+
puts "Creating tree.txt." unless FMPVC.configuration.quiet
|
61
|
+
`#{tree_command} -F > #{FMPVC.configuration.tree_filename}`
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
time_end = Time.new
|
66
|
+
unless FMPVC.configuration.quiet
|
67
|
+
puts "Processing complete."
|
68
|
+
puts "Elapsed time: #{(time_end - time_start).round(0)} seconds."
|
69
|
+
puts
|
70
|
+
end
|
71
|
+
|
data/fmpvc.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'fmpvc/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "fmpvc"
|
9
|
+
spec.version = FMPVC::VERSION
|
10
|
+
spec.authors = ["Martin S. Boswell"]
|
11
|
+
spec.email = ["mboswell@me.com"]
|
12
|
+
|
13
|
+
spec.summary = %q{Create a text version of the design elements of a FileMaker database.}
|
14
|
+
spec.description = %q{Process FileMaker Pro Advanced's Database Design Report (DDR) to produce textual representations of the design objects for use with version control systems, text editors, etc.}
|
15
|
+
spec.homepage = "http://rubygems.org/gems/fmpvc"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.9'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
|
26
|
+
spec.add_dependency 'nokogiri', '~> 1.6.6'
|
27
|
+
spec.add_dependency 'activesupport', '~> 4.2'
|
28
|
+
|
29
|
+
end
|
data/lib/fmpvc.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "fmpvc/version"
|
2
|
+
require "fmpvc/ddr"
|
3
|
+
require "fmpvc/fmpreport"
|
4
|
+
require "fmpvc/configuration"
|
5
|
+
|
6
|
+
module FMPVC
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :configuration
|
10
|
+
end
|
11
|
+
def self.configure
|
12
|
+
@configuration = Configuration.new
|
13
|
+
yield(configuration)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/lib/fmpvc/DDR.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module FMPVC
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
# for xml2yaml
|
6
|
+
require 'active_support/core_ext/hash/conversions'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
class DDR
|
10
|
+
|
11
|
+
attr_reader :content, :type, :fmp_files, :xml_files, :base_dir_ddr, :base_dir_text_path, :reports, :fmpa_version, :creation_time, :creation_date, :reports
|
12
|
+
|
13
|
+
def initialize(summary_directory, summary_filename = FMPVC.configuration.ddr_filename)
|
14
|
+
|
15
|
+
@summary_filename = summary_filename
|
16
|
+
@base_dir_ddr = File.expand_path(summary_directory) ; raise(RuntimeError, "Error: can't find the DDR directory, #{@base_dir_ddr}") unless File.readable?(@base_dir_ddr)
|
17
|
+
summary_file_path = "#{@base_dir_ddr}/#{summary_filename}" ; raise(RuntimeError, "Error: can't find the DDR Summary.xml file, #{summary_file_path}") unless File.readable?(summary_file_path)
|
18
|
+
@base_dir_text_path = @base_dir_ddr.gsub(%r{#{FMPVC.configuration.ddr_dirname}}, FMPVC.configuration.text_dirname)
|
19
|
+
@summary_text_path = "#{@base_dir_text_path}/#{summary_filename.gsub(%r{\.xml}, '.txt')}"
|
20
|
+
|
21
|
+
@content = IO.read(summary_file_path)
|
22
|
+
@reports = Array.new
|
23
|
+
self.parse
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse
|
28
|
+
post_notification('Summary', 'Parsing')
|
29
|
+
summary = Nokogiri::XML(@content)
|
30
|
+
attrs = summary.xpath("//FMPReport").first # "there can be only one"
|
31
|
+
@type = attrs["type"] ; raise RuntimeError, "Incorrect file type: not a DDR Summary.xml file!" unless @type == "Summary"
|
32
|
+
@fmpa_version = attrs["version"]
|
33
|
+
@creation_time = attrs["creationTime"]
|
34
|
+
@creation_date = attrs["creationDate"]
|
35
|
+
@summary_yaml = element2yaml(summary)
|
36
|
+
|
37
|
+
fmp_reports = summary.xpath("//FMPReport/File")
|
38
|
+
@reports = fmp_reports.map do |a_report|
|
39
|
+
{
|
40
|
+
:name => a_report['name'],
|
41
|
+
:link => a_report['link'],
|
42
|
+
:path => a_report['path'],
|
43
|
+
:attrs => Hash[ a_report.xpath("./*").map {|v| [v.name, v['count']]} ]
|
44
|
+
}
|
45
|
+
end
|
46
|
+
@xml_files = fmp_reports.collect {|node| node['link']}
|
47
|
+
@fmp_files = fmp_reports.collect {|node| node['name']}
|
48
|
+
end
|
49
|
+
|
50
|
+
def process_reports
|
51
|
+
@reports.each do |r|
|
52
|
+
# $stdout.puts
|
53
|
+
post_notification(r[:link].gsub(%r{\./+},''), 'Processing')
|
54
|
+
r[:report] = FMPReport.new(r[:link], self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def post_notification(object, verb = 'Updating')
|
59
|
+
$stdout.puts [verb, object].join(" ") unless FMPVC.configuration.quiet
|
60
|
+
end
|
61
|
+
|
62
|
+
def stringer(n, str = " ")
|
63
|
+
n.times.map {str}.join
|
64
|
+
end
|
65
|
+
|
66
|
+
def write_summary
|
67
|
+
post_notification('Summary file', 'Writing')
|
68
|
+
FileUtils.mkdir(@base_dir_text_path) unless File.directory?(@base_dir_text_path)
|
69
|
+
summary_format = "%25s %-512s\n"
|
70
|
+
# report_params = ["BaseTables", "Tables", "Relationships", "Privileges", "ExtendedPrivileges", "FileAccess", "Layouts", "Scripts", "ValueLists", "CustomFunctions", "FileReferences", "CustomMenuSets", "CustomMenus"]
|
71
|
+
report_params = @reports.first[:attrs].keys # better to get the keys dynamically than a fixed list
|
72
|
+
params_label = report_params.map {|p| "%-2s" + stringer(p.length) }.join()
|
73
|
+
report_format = "%25s " + params_label
|
74
|
+
header = stringer(25 - "Report".length) + "Report" + " " + report_params.join(' ')
|
75
|
+
separator = header.gsub(%r{\w}, '-')
|
76
|
+
File.open(@summary_text_path, 'w') do |f|
|
77
|
+
f.write format(summary_format, "Summary file:", @summary_filename)
|
78
|
+
f.write format(summary_format, "Summary path:", @base_dir_ddr)
|
79
|
+
f.write format(summary_format, "FileMaker Pro version:", @fmpa_version)
|
80
|
+
f.write format(summary_format, "Creation Date:", @creation_date)
|
81
|
+
f.write format(summary_format, "Creation Time:", @creation_time)
|
82
|
+
f.puts
|
83
|
+
f.puts header
|
84
|
+
f.puts separator
|
85
|
+
@reports.each do |r|
|
86
|
+
f.puts format(report_format, r[:name] + ' ', *report_params.map { |p| r[:attrs][p] })
|
87
|
+
end
|
88
|
+
f.puts
|
89
|
+
f.puts @summary_yaml
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def write_reports
|
94
|
+
self.process_reports if @reports.first[:report].nil?
|
95
|
+
@reports.each { |r| r[:report].write_all_objects }
|
96
|
+
end
|
97
|
+
|
98
|
+
def element2yaml(xml_element)
|
99
|
+
return '' unless FMPVC.configuration.yaml
|
100
|
+
element_xml = xml_element.to_xml({:encoding => 'UTF-8'}) # REMEMBER: the encoding
|
101
|
+
element_hash = Hash.from_xml(element_xml)
|
102
|
+
element_yaml = element_hash.to_yaml
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,538 @@
|
|
1
|
+
module FMPVC
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
# for xml2yaml
|
7
|
+
require 'active_support/core_ext/hash/conversions'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
NEWLINE = "\n"
|
11
|
+
|
12
|
+
class FMPReport
|
13
|
+
|
14
|
+
attr_reader :content, :type, :text_dir, :text_filename, :report_dirpath, :named_objects
|
15
|
+
|
16
|
+
def initialize(report_filename, ddr)
|
17
|
+
report_dirpath = "#{ddr.base_dir_ddr}/#{report_filename}" # location of the fmpfilename.xml file
|
18
|
+
raise(RuntimeError, "Error: can't find the report file, #{report_dirpath}") unless File.readable?(report_dirpath)
|
19
|
+
|
20
|
+
@content = IO.read(report_dirpath, mode: 'rb:UTF-16:UTF-8') # transcode is specifically for a spec content match
|
21
|
+
@text_dir = "#{ddr.base_dir_ddr}/../#{FMPVC.configuration.text_dirname}"
|
22
|
+
@text_filename = fs_sanitize(report_filename)
|
23
|
+
@report_dirpath = "#{@text_dir}/#{@text_filename}"
|
24
|
+
|
25
|
+
self.define_content_procs
|
26
|
+
|
27
|
+
self.parse
|
28
|
+
self.clean_dir
|
29
|
+
self.write_dir
|
30
|
+
|
31
|
+
### hierarchical folder structure
|
32
|
+
@scripts = parse_fmp_obj( "/FMPReport/File/ScriptCatalog", "/*[name()='Group' or name()='Script']", @script_content )
|
33
|
+
@layouts = parse_fmp_obj( "/FMPReport/File/LayoutCatalog", "/*[name()='Group' or name()='Layout']", @layouts_content )
|
34
|
+
### single folder with files
|
35
|
+
@value_lists = parse_fmp_obj( "/FMPReport/File/ValueListCatalog", "/*[name()='ValueList']", @value_list_content )
|
36
|
+
@tables = parse_fmp_obj( "/FMPReport/File/BaseTableCatalog", "/*[name()='BaseTable']", @table_content )
|
37
|
+
@custom_functions = parse_fmp_obj( "/FMPReport/File/CustomFunctionCatalog", "/*[name()='CustomFunction']", @custom_function_content )
|
38
|
+
@menu_sets = parse_fmp_obj( "/FMPReport/File/CustomMenuSetCatalog", "/*[name()='CustomMenuSet']", @menu_sets_content )
|
39
|
+
@custom_menus = parse_fmp_obj( "/FMPReport/File/CustomMenuCatalog", "/*[name()='CustomMenu']", @custom_menus_content )
|
40
|
+
### single file output
|
41
|
+
@accounts = parse_fmp_obj( "/FMPReport/File/AccountCatalog", "/*[name()='Account']", @accounts_content, true )
|
42
|
+
@privileges = parse_fmp_obj( "/FMPReport/File/PrivilegesCatalog", "/*[name()='PrivilegeSet']", @privileges_content, true )
|
43
|
+
@extended_privileges = parse_fmp_obj( "/FMPReport/File/ExtendedPrivilegeCatalog", "/*[name()='ExtendedPrivilege']", @extended_priviledge_content, true )
|
44
|
+
@relationships = parse_fmp_obj( "/FMPReport/File/RelationshipGraph", "/RelationshipList/*[name()='Relationship']", @relationships_content, true )
|
45
|
+
@file_access = parse_fmp_obj( "/FMPReport/File/AuthFileCatalog", '', @file_access_content, true )
|
46
|
+
@external_sources = parse_fmp_obj( "/FMPReport/File/ExternalDataSourcesCatalog", '', @external_sources_content, true )
|
47
|
+
@file_options = parse_fmp_obj( "/FMPReport/File/Options", '', @file_options_content, true )
|
48
|
+
@themes = parse_fmp_obj( "/FMPReport/File/ThemeCatalog", "/*[name()='Theme']", @themes_content, true )
|
49
|
+
|
50
|
+
@named_objects = [
|
51
|
+
{ :content => @scripts, :disk_path => "/Scripts" },
|
52
|
+
{ :content => @layouts, :disk_path => "/Layouts" },
|
53
|
+
{ :content => @value_lists, :disk_path => "/ValueLists" },
|
54
|
+
{ :content => @tables, :disk_path => "/Tables" },
|
55
|
+
{ :content => @custom_functions, :disk_path => "/CustomFunctions" },
|
56
|
+
{ :content => @menu_sets, :disk_path => "/CustomMenuSets" },
|
57
|
+
{ :content => @custom_menus, :disk_path => "/CustomMenus" },
|
58
|
+
{ :content => @accounts, :disk_path => "/Accounts.txt" },
|
59
|
+
{ :content => @privileges, :disk_path => "/PrivilegeSets.txt" },
|
60
|
+
{ :content => @extended_privileges, :disk_path => "/ExtendedPrivileges.txt" },
|
61
|
+
{ :content => @relationships, :disk_path => "/Relationships.txt" },
|
62
|
+
{ :content => @file_access, :disk_path => "/FileAccess.txt" },
|
63
|
+
{ :content => @external_sources, :disk_path => "/ExternalDataSources.txt" },
|
64
|
+
{ :content => @file_options, :disk_path => "/Options.txt" },
|
65
|
+
{ :content => @themes, :disk_path => "/Themes.txt" }
|
66
|
+
]
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
def write_all_objects()
|
71
|
+
post_notification('report files', 'Writing')
|
72
|
+
@named_objects.each { |obj| write_obj_to_disk(obj[:content], @report_dirpath + obj[:disk_path])}
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse
|
76
|
+
@report = Nokogiri::XML(@content)
|
77
|
+
@type = @report.xpath("//FMPReport").first["type"]
|
78
|
+
raise RuntimeError, "Incorrect file type: not an FMPReport Report file" unless @type == "Report"
|
79
|
+
end
|
80
|
+
|
81
|
+
def fs_sanitize(text_string)
|
82
|
+
safe_name = text_string.gsub(%r{\A [\/\.]+ }mx, '') # remove leading dir symbols: . /
|
83
|
+
safe_name.gsub(%r{[\/]}, '_') # just remove [ / ] for now.
|
84
|
+
end
|
85
|
+
|
86
|
+
def fs_id(fs_name, id)
|
87
|
+
fs_name + " (id #{id})"
|
88
|
+
end
|
89
|
+
|
90
|
+
# e.g. /FMPReport/File/ScriptCatalog , /FMPReport/File/ScriptCatalog/Group[1]/Group
|
91
|
+
# return: "/Actors/Actor Triggers"
|
92
|
+
def disk_path_from_base(object_base, object_xpath, path = '')
|
93
|
+
return "#{path}" if object_xpath == object_base
|
94
|
+
curent_node_filename = @report.xpath("#{object_xpath}").first['name']
|
95
|
+
current_node_id = @report.xpath("#{object_xpath}").first['id']
|
96
|
+
parent_node_xpath = @report.xpath("#{object_xpath}/..").first.path
|
97
|
+
disk_path_from_base(object_base, parent_node_xpath, "/#{fs_id(curent_node_filename, current_node_id)}" + "#{path}" )
|
98
|
+
end
|
99
|
+
|
100
|
+
def write_dir
|
101
|
+
FileUtils.mkdir_p(@report_dirpath)
|
102
|
+
end
|
103
|
+
|
104
|
+
def clean_dir
|
105
|
+
FileUtils.rm_rf(@report_dirpath)
|
106
|
+
end
|
107
|
+
|
108
|
+
def element2yaml(xml_element)
|
109
|
+
return '' unless FMPVC.configuration.yaml
|
110
|
+
element_xml = xml_element.to_xml({:encoding => 'UTF-8'}) # REMEMBER: the encoding
|
111
|
+
element_hash = Hash.from_xml(element_xml)
|
112
|
+
element_yaml = element_hash.to_yaml
|
113
|
+
end
|
114
|
+
|
115
|
+
def post_notification(object, verb = 'Updating')
|
116
|
+
$stdout.puts [verb, object].join(" ") unless FMPVC.configuration.quiet
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def parse_fmp_obj(object_base, object_nodes, obj_content, one_file = false)
|
121
|
+
post_notification(object_base.gsub(%r{\/FMPReport\/File\/},''), ' Parsing')
|
122
|
+
objects_parsed = Array.new
|
123
|
+
objects = @report.xpath("#{object_base}#{object_nodes}")
|
124
|
+
objects.each do |an_obj|
|
125
|
+
obj_id = an_obj['id']
|
126
|
+
if one_file
|
127
|
+
sanitized_obj_name_id_ext = nil
|
128
|
+
else
|
129
|
+
obj_name = an_obj['name']
|
130
|
+
sanitized_obj_name = fs_sanitize(obj_name)
|
131
|
+
sanitized_obj_name_id = fs_id(sanitized_obj_name, obj_id)
|
132
|
+
sanitized_obj_name_id_ext = sanitized_obj_name_id + '.txt'
|
133
|
+
end
|
134
|
+
|
135
|
+
obj_parsed = {
|
136
|
+
:name => sanitized_obj_name_id_ext \
|
137
|
+
, :type => :file \
|
138
|
+
, :xpath => an_obj.path \
|
139
|
+
}
|
140
|
+
|
141
|
+
# if it's a Group, then make a directory for it, else make a file
|
142
|
+
if an_obj.name == 'Group'
|
143
|
+
obj_parsed[:type] = :dir
|
144
|
+
obj_parsed[:name] = sanitized_obj_name_id
|
145
|
+
obj_parsed[:children] = parse_fmp_obj(an_obj.path, object_nodes, obj_content)
|
146
|
+
else
|
147
|
+
obj_parsed[:content] = one_file ? obj_content.call(objects) : obj_content.call(an_obj)
|
148
|
+
obj_parsed[:yaml] = one_file ? element2yaml(@report.xpath(object_base)) : element2yaml(an_obj)
|
149
|
+
end
|
150
|
+
|
151
|
+
objects_parsed.push(obj_parsed)
|
152
|
+
break if one_file == true
|
153
|
+
end
|
154
|
+
objects_parsed
|
155
|
+
end
|
156
|
+
|
157
|
+
def write_obj_to_disk(objs, full_path)
|
158
|
+
post_notification(full_path.gsub(%r{.*#{FMPVC.configuration.text_dirname}/},''), ' Writing')
|
159
|
+
if full_path =~ %r{\.txt}
|
160
|
+
# single file objects
|
161
|
+
File.open(full_path, 'w') do |f|
|
162
|
+
unless objs.empty?
|
163
|
+
f.write(objs.first[:content] + NEWLINE) unless objs.first[:content] == ''
|
164
|
+
f.write(NEWLINE + objs.first[:yaml])
|
165
|
+
end
|
166
|
+
end
|
167
|
+
else
|
168
|
+
# multi-file objects in directory
|
169
|
+
FileUtils.mkdir_p(full_path) unless File.directory?(full_path)
|
170
|
+
objs.each do |obj|
|
171
|
+
if obj[:type] == :file
|
172
|
+
File.open("#{full_path}/#{obj[:name]}", 'w') do |f|
|
173
|
+
f.write(obj[:content] + NEWLINE) unless obj[:content] == ''
|
174
|
+
f.write(NEWLINE + obj[:yaml])
|
175
|
+
end
|
176
|
+
elsif obj[:type] == :dir
|
177
|
+
write_obj_to_disk(obj[:children], full_path + "/#{obj[:name]}")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def define_content_procs
|
184
|
+
|
185
|
+
@script_content = Proc.new do |a_script|
|
186
|
+
content = ''
|
187
|
+
a_script.xpath("./StepList/Step/StepText").each {|t| content += t.text.gsub(%r{\n},'') + "\n" } # remove \n from middle of steps
|
188
|
+
content
|
189
|
+
end
|
190
|
+
|
191
|
+
@layouts_content = Proc.new do |a_layout|
|
192
|
+
content = ''
|
193
|
+
layout_name = a_layout['name']
|
194
|
+
layout_id = a_layout['id']
|
195
|
+
layout_table = a_layout.xpath('./Table').first['name']
|
196
|
+
layout_theme = a_layout.xpath('./Theme').first['name']
|
197
|
+
layout_format = "%18s %-25s\n"
|
198
|
+
object_format = " %-16s %-35s\n"
|
199
|
+
content += format(layout_format, "Layout name: ", layout_name)
|
200
|
+
content += format(layout_format, "id: ", layout_id)
|
201
|
+
content += format(layout_format, "Table: ", layout_table)
|
202
|
+
content += format(layout_format, "Theme: ", layout_theme)
|
203
|
+
content += NEWLINE
|
204
|
+
content += format(layout_format, "Objects: ", '')
|
205
|
+
layout_objects = a_layout.xpath("./*[name()='Object']") # find all objects
|
206
|
+
layout_objects_types = layout_objects.map { |o| o['type']} # list of 'types'
|
207
|
+
if !layout_objects_types.empty? # [].uniq! => nil - don't do that
|
208
|
+
layout_objects_types.uniq!
|
209
|
+
content += format(object_format, "Type", "'Name'" )
|
210
|
+
content += format(object_format, "----", "------" )
|
211
|
+
end
|
212
|
+
layout_objects_types.each do |a_type|
|
213
|
+
selected_objects = layout_objects.select { |o| o['type'] == a_type } # get all the objects of a given type
|
214
|
+
selected_objects.each do |type_obj| # collect all objects of type
|
215
|
+
content += format(object_format, type_obj['type'], type_obj.xpath('./*/Name').text) unless type_obj['type'] == "Text"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
content
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
@value_list_content = Proc.new do |a_value_list|
|
224
|
+
content = ''
|
225
|
+
source_type = a_value_list.xpath("./Source").first['value']
|
226
|
+
if source_type == "Custom"
|
227
|
+
a_value_list.xpath("./CustomValues/Text").each {|t| content += t.text}
|
228
|
+
end
|
229
|
+
content
|
230
|
+
end
|
231
|
+
|
232
|
+
@table_content = Proc.new do |a_table|
|
233
|
+
content = ''
|
234
|
+
table_format = "%6d %-25s %-15s %-15s %-50s\n"
|
235
|
+
table_header_format = table_format.gsub(%r{d}, 's')
|
236
|
+
content += format(table_header_format, "id", "Field Name", "Data Type", "Field Type", "Comment")
|
237
|
+
content += format(table_header_format, "--", "----------", "---------", "----------", "-------")
|
238
|
+
a_table.xpath("//BaseTable[@name='#{a_table['name']}']/FieldCatalog/*[name()='Field']").each do |t|
|
239
|
+
t_comment = t.xpath("./Comment").text
|
240
|
+
content += format(table_format, t['id'], t['name'], t['dataType'], t['fieldType'], t_comment)
|
241
|
+
end
|
242
|
+
content
|
243
|
+
end
|
244
|
+
|
245
|
+
@custom_function_content = Proc.new do |a_custom_function|
|
246
|
+
content = ''
|
247
|
+
content += a_custom_function.xpath("./Calculation").map {|t| t.text}.join(NEWLINE)
|
248
|
+
content
|
249
|
+
end
|
250
|
+
|
251
|
+
@menu_sets_content = Proc.new do |a_menu_set|
|
252
|
+
content = ''
|
253
|
+
menu_set_format = "%6d %-35s\n"
|
254
|
+
menu_set_header_format = menu_set_format.gsub(%r{d}, 's')
|
255
|
+
content += format(menu_set_header_format, "id", "Menu")
|
256
|
+
content += format(menu_set_header_format, "--", "----")
|
257
|
+
a_menu_set.xpath("./CustomMenuList/*[name()='CustomMenu']").each do |a_menu|
|
258
|
+
content += format(menu_set_format, a_menu['id'], a_menu['name'])
|
259
|
+
end
|
260
|
+
|
261
|
+
content
|
262
|
+
end
|
263
|
+
|
264
|
+
@custom_menus_content = Proc.new do |a_menu|
|
265
|
+
content = ''
|
266
|
+
menu_name = a_menu['name']
|
267
|
+
menu_id = a_menu['id']
|
268
|
+
menu_base = a_menu.xpath('./BaseMenu').first['name']
|
269
|
+
menu_comment = a_menu.xpath('./Comment').text
|
270
|
+
menu_format = "%17s %-35s\n"
|
271
|
+
|
272
|
+
content += format(menu_format, "Menu name:", menu_name)
|
273
|
+
content += format(menu_format, "id:", menu_id)
|
274
|
+
content += format(menu_format, "Base menu:", menu_base)
|
275
|
+
content += format(menu_format, "Comment:", menu_comment)
|
276
|
+
content += NEWLINE
|
277
|
+
menu_items = a_menu.xpath("./MenuItemList/*[name()='MenuItem']")
|
278
|
+
menu_items.each do |an_item|
|
279
|
+
an_item.xpath('./Command').each { |c| content += " #{c['name']}\n"}
|
280
|
+
end
|
281
|
+
|
282
|
+
content
|
283
|
+
end
|
284
|
+
|
285
|
+
|
286
|
+
@accounts_content = Proc.new do |accounts|
|
287
|
+
content = ''
|
288
|
+
accounts_format = "%6d %-25s %-10s %-12s %-20s %-12s %-12s %-50s"
|
289
|
+
accounts_header_format = accounts_format.gsub(%r{d}, 's')
|
290
|
+
content += format(accounts_header_format, "id", "Name", "Status", "Management", "Privilege Set", "Empty Pass?", "Change Pass?", "Description") + NEWLINE
|
291
|
+
content += format(accounts_header_format, "--", "----", "------", "----------", "-------------", "-----------", "------------", "-----------") + NEWLINE
|
292
|
+
content += accounts.map do |an_account|
|
293
|
+
account_name = an_account['name']
|
294
|
+
account_id = an_account['id']
|
295
|
+
account_privilegeSet = an_account['privilegeSet']
|
296
|
+
account_emptyPassword = an_account['emptyPassword']
|
297
|
+
account_changePasswordOnNextLogin = an_account['changePasswordOnNextLogin']
|
298
|
+
account_managedBy = an_account['managedBy']
|
299
|
+
account_status = an_account['status']
|
300
|
+
account_Description = an_account.xpath('./Description').text
|
301
|
+
format(
|
302
|
+
accounts_format \
|
303
|
+
, account_id \
|
304
|
+
, account_name \
|
305
|
+
, account_status \
|
306
|
+
, account_managedBy \
|
307
|
+
, account_privilegeSet \
|
308
|
+
, account_emptyPassword \
|
309
|
+
, account_changePasswordOnNextLogin \
|
310
|
+
, account_Description
|
311
|
+
)
|
312
|
+
end.join(NEWLINE)
|
313
|
+
|
314
|
+
content
|
315
|
+
end
|
316
|
+
|
317
|
+
@privileges_content = Proc.new do |privileges|
|
318
|
+
content = ''
|
319
|
+
privileges_format = "%6d %-25s %-8s %-10s %-15s %-12s %-12s %-12s %-8s %-18s %-11s %-10s %-12s %-10s %-16s %-10s %-70s"
|
320
|
+
privileges_header_format = privileges_format.gsub(%r{d}, 's')
|
321
|
+
content += format(privileges_header_format, "id", "Name", "Print?", "Export?", "Manage Ext'd?", "Override?", "Disconnect?", "Password?", "Menus", "Records", "Layouts", "(Creation)", "ValueLists", "(Creation)", "Scripts", "(Creation)", "Description") + NEWLINE
|
322
|
+
content += format(privileges_header_format, "--", "----", "------", "-------", "-------------", "---------", "-----------", "---------", "-----", "-------", "-------", "----------", "----------", "----------", "-------", "----------", "-----------") + NEWLINE
|
323
|
+
privileges.each do |a_privilege_set|
|
324
|
+
privilege_set_id = a_privilege_set['id']
|
325
|
+
privilege_set_name = a_privilege_set['name']
|
326
|
+
privilege_set_comment = a_privilege_set['comment']
|
327
|
+
privilege_set_printing = a_privilege_set['printing']
|
328
|
+
privilege_set_exporting = a_privilege_set['exporting']
|
329
|
+
privilege_set_managedExtended = a_privilege_set['managedExtended']
|
330
|
+
privilege_set_overrideValidationWarning = a_privilege_set['overrideValidationWarning']
|
331
|
+
privilege_set_idleDisconnect = a_privilege_set['idleDisconnect']
|
332
|
+
privilege_set_allowModifyPassword = a_privilege_set['allowModifyPassword']
|
333
|
+
privilege_set_menu = a_privilege_set['menu']
|
334
|
+
|
335
|
+
privilege_set_records_value = a_privilege_set.xpath('./Records').first['value']
|
336
|
+
privilege_set_layouts_value = a_privilege_set.xpath('./Layouts').first['value']
|
337
|
+
privilege_set_layouts_creation = a_privilege_set.xpath('./Layouts').first['allowCreation']
|
338
|
+
privilege_set_valuelists_value = a_privilege_set.xpath('./ValueLists').first['value']
|
339
|
+
privilege_set_valuelists_creation = a_privilege_set.xpath('./ValueLists').first['allowCreation']
|
340
|
+
privilege_set_scripts_value = a_privilege_set.xpath('./Scripts').first['value']
|
341
|
+
privilege_set_scripts_creation = a_privilege_set.xpath('./Scripts').first['allowCreation']
|
342
|
+
|
343
|
+
content += format(
|
344
|
+
privileges_format \
|
345
|
+
, privilege_set_id \
|
346
|
+
, privilege_set_name \
|
347
|
+
, privilege_set_printing \
|
348
|
+
, privilege_set_exporting \
|
349
|
+
, privilege_set_managedExtended \
|
350
|
+
, privilege_set_overrideValidationWarning \
|
351
|
+
, privilege_set_idleDisconnect \
|
352
|
+
, privilege_set_allowModifyPassword \
|
353
|
+
, privilege_set_menu \
|
354
|
+
, privilege_set_records_value \
|
355
|
+
, privilege_set_layouts_value \
|
356
|
+
, privilege_set_layouts_creation \
|
357
|
+
, privilege_set_valuelists_value \
|
358
|
+
, privilege_set_valuelists_creation \
|
359
|
+
, privilege_set_scripts_value \
|
360
|
+
, privilege_set_scripts_creation \
|
361
|
+
, privilege_set_comment \
|
362
|
+
) + NEWLINE
|
363
|
+
end
|
364
|
+
content
|
365
|
+
end
|
366
|
+
|
367
|
+
@extended_priviledge_content = Proc.new do |ext_privileges|
|
368
|
+
content = ''
|
369
|
+
ext_privilege_format = "%6d %-20s %-85s %-150s\n"
|
370
|
+
ext_privilege_header_format = ext_privilege_format.gsub(%r{d}, 's')
|
371
|
+
content += format(ext_privilege_header_format, "id", "Name", "Description", "Privilege Sets")
|
372
|
+
content += format(ext_privilege_header_format, "--", "----", "-----------", "--------------")
|
373
|
+
ext_privileges.each do |an_ext_privilege|
|
374
|
+
ext_privilege_id = an_ext_privilege['id']
|
375
|
+
ext_privilege_name = an_ext_privilege['name']
|
376
|
+
ext_privilege_comment = an_ext_privilege['comment']
|
377
|
+
ext_privilege_sets = an_ext_privilege.xpath('./PrivilegeSetList/*[name()="PrivilegeSet"]').map {|s| s['name']}.join(", ")
|
378
|
+
|
379
|
+
content += format(
|
380
|
+
ext_privilege_format \
|
381
|
+
, ext_privilege_id \
|
382
|
+
, ext_privilege_name \
|
383
|
+
, ext_privilege_comment \
|
384
|
+
, ext_privilege_sets \
|
385
|
+
)
|
386
|
+
end
|
387
|
+
content
|
388
|
+
end
|
389
|
+
|
390
|
+
@relationships_content = Proc.new do |relationships|
|
391
|
+
content = ''
|
392
|
+
|
393
|
+
tables = @report.xpath("/FMPReport/File/RelationshipGraph/TableList/*[name()='Table']")
|
394
|
+
table_format = " %-25s %-25s"
|
395
|
+
content +="Tables\n"
|
396
|
+
content += NEWLINE
|
397
|
+
content +=format(table_format, "Base Table (id)", "Table occurrence (id)") + NEWLINE
|
398
|
+
content +=format(table_format, "---------------", "---------------------") + NEWLINE
|
399
|
+
content += NEWLINE
|
400
|
+
tables.each do |a_table|
|
401
|
+
table_id = a_table['id']
|
402
|
+
table_name = a_table['name']
|
403
|
+
basetable_id = a_table['baseTableId']
|
404
|
+
basetable_name = a_table['baseTable']
|
405
|
+
content +=format(table_format, "#{basetable_name} (#{basetable_id})", "#{table_name} (#{table_id})") + NEWLINE
|
406
|
+
end
|
407
|
+
content += NEWLINE
|
408
|
+
|
409
|
+
relationship_format = " %-35s %-15s %-35s"
|
410
|
+
content +="Relationships" + NEWLINE
|
411
|
+
relationships.each do |a_relationship|
|
412
|
+
content += NEWLINE
|
413
|
+
content += format(" Relationship: %-4d", a_relationship['id']) + NEWLINE
|
414
|
+
predicates = a_relationship.xpath('./JoinPredicateList/*[name()="JoinPredicate"]')
|
415
|
+
predicates.each do |a_predicate|
|
416
|
+
predicate_type = a_predicate['type']
|
417
|
+
|
418
|
+
left_field = a_predicate.xpath('./LeftField/*[name()="Field"]').first
|
419
|
+
left_table = left_field['table']
|
420
|
+
left_field_name = left_field['name']
|
421
|
+
|
422
|
+
right_field = a_predicate.xpath('./RightField/*[name()="Field"]').first
|
423
|
+
right_table = right_field['table']
|
424
|
+
right_field_name = right_field['name']
|
425
|
+
content += format(relationship_format, "#{left_table}::#{left_field_name}", "#{predicate_type}", "#{right_table}::#{right_field_name}") + NEWLINE
|
426
|
+
end
|
427
|
+
end
|
428
|
+
content
|
429
|
+
end
|
430
|
+
|
431
|
+
@file_access_content = Proc.new do |file_access|
|
432
|
+
content = ''
|
433
|
+
inbound_access = file_access.xpath("./Inbound/*[name()='InboundAuthorization']")
|
434
|
+
outbound_access = file_access.xpath("./Outbound/*[name()='OutboundAuthorization']")
|
435
|
+
access_format = " %6d %-25s %-25s %-25s"
|
436
|
+
access_format_header = access_format.gsub(%r{d}, 's')
|
437
|
+
|
438
|
+
auth_requirement = file_access.first['requireAuthorization']
|
439
|
+
content += "Authorization required: #{auth_requirement}" + NEWLINE
|
440
|
+
if auth_requirement == "True"
|
441
|
+
content += NEWLINE
|
442
|
+
content += format(access_format_header, "id", "Timestamp", "Account", "Filenames") + NEWLINE
|
443
|
+
content += format(access_format_header, "--", "---------", "-------", "---------") + NEWLINE
|
444
|
+
content += format("%12s", "Inbound:") + NEWLINE
|
445
|
+
inbound_access.each do |i|
|
446
|
+
content += format(access_format, i['id'], i['date'], i['user'], i['filenames']) + NEWLINE
|
447
|
+
end
|
448
|
+
content += format("%12s", "Outbound:") + NEWLINE
|
449
|
+
outbound_access.each do |o|
|
450
|
+
content += format(access_format, o['id'], o['date'], o['user'], o['filenames']) + NEWLINE
|
451
|
+
end
|
452
|
+
end
|
453
|
+
content += NEWLINE
|
454
|
+
|
455
|
+
content
|
456
|
+
end
|
457
|
+
|
458
|
+
@external_sources_content = Proc.new do |data_sources|
|
459
|
+
content = ''
|
460
|
+
file_references = data_sources.xpath("./*[name()='FileReference']")
|
461
|
+
odbc_sources = data_sources.xpath("./*[name()='OdbcDataSource']")
|
462
|
+
file_references_format = " %6d %-25s %-25s\n"
|
463
|
+
file_references_header_format = file_references_format.gsub(%r{d},'s')
|
464
|
+
odbc_source_format = " %6d %-25s %-25s %-25s\n"
|
465
|
+
odbc_source_header_format = odbc_source_format.gsub(%r{d},'s')
|
466
|
+
|
467
|
+
content += format(file_references_header_format, "id", "File Reference", "Path List")
|
468
|
+
content += format(file_references_header_format, "--", "--------------", "---------")
|
469
|
+
file_references.each do |r|
|
470
|
+
content += format(file_references_format, r['id'], r['name'], r['pathList'])
|
471
|
+
end
|
472
|
+
content += NEWLINE
|
473
|
+
content += format(odbc_source_header_format, "id", "ODBC Source", "DSN", "Link")
|
474
|
+
content += format(odbc_source_header_format, "--", "-----------", "---", "----")
|
475
|
+
odbc_sources.each do |s|
|
476
|
+
content += format(odbc_source_format, s['id'], s['name'], s['DSN'], s['link'])
|
477
|
+
end
|
478
|
+
|
479
|
+
content
|
480
|
+
end
|
481
|
+
|
482
|
+
@file_options_content = Proc.new do |file_options|
|
483
|
+
content = ''
|
484
|
+
file_options_format = " %-27s %-30s\n"
|
485
|
+
trigger_format = " %-23s %-30s\n"
|
486
|
+
|
487
|
+
# optional <FMPReport><File><Options>, see DDR_grammar doc, p. 5
|
488
|
+
open_account_search = file_options.xpath('./OnOpen/Account')
|
489
|
+
open_account = (open_account_search.size > 0 ? open_account_search.first['name']: "")
|
490
|
+
open_layout_search = file_options.xpath('./OnOpen/Layout')
|
491
|
+
open_layout = ( open_layout_search.size > 0 ? open_layout_search.first['name'] : "" )
|
492
|
+
|
493
|
+
encryption_type = file_options.xpath('./Encryption').first['type']
|
494
|
+
encryption_note = case encryption_type
|
495
|
+
when "0"
|
496
|
+
"no encryption"
|
497
|
+
when "1"
|
498
|
+
"AES256 encrypted"
|
499
|
+
end
|
500
|
+
|
501
|
+
content += "File Options\n"
|
502
|
+
content += "------------\n"
|
503
|
+
content += NEWLINE
|
504
|
+
content += format(file_options_format, "Encryption:", "#{encryption_type} (#{encryption_note})")
|
505
|
+
content += NEWLINE
|
506
|
+
content += format(file_options_format, "Minimum Allowed Version:", file_options.xpath('./OnOpen/MinimumAllowedVersion').first['name'])
|
507
|
+
content += format(file_options_format, "Account:", open_account)
|
508
|
+
content += format(file_options_format, "Layout:", open_layout)
|
509
|
+
content += NEWLINE
|
510
|
+
content += format(file_options_format, "Default Custom Menu Set:", file_options.xpath('./DefaultCustomMenuSet/CustomMenuSet').first['name'])
|
511
|
+
content += NEWLINE
|
512
|
+
content += " Triggers\n"
|
513
|
+
file_options.xpath('./WindowTriggers/*').each do |t|
|
514
|
+
content += format(trigger_format, t.name, t.xpath('./Script').first['name'])
|
515
|
+
end
|
516
|
+
|
517
|
+
content
|
518
|
+
end
|
519
|
+
|
520
|
+
@themes_content = Proc.new do |themes|
|
521
|
+
content = ''
|
522
|
+
theme_format = " %6s %-20s %-20s %-10s %-10s %-20s\n"
|
523
|
+
content += format(theme_format, "id", "Name", "Group", "Version", "Locale", "Internal Name")
|
524
|
+
content += format(theme_format, "--", "----", "-----", "-------", "------", "-------------")
|
525
|
+
themes.each do |a_theme|
|
526
|
+
content += format(theme_format, a_theme['id'], a_theme['name'], a_theme['group'], a_theme['version'], a_theme['locale'], a_theme['internalName'])
|
527
|
+
end
|
528
|
+
|
529
|
+
content
|
530
|
+
end
|
531
|
+
|
532
|
+
end
|
533
|
+
|
534
|
+
|
535
|
+
end
|
536
|
+
|
537
|
+
end
|
538
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module FMPVC
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
attr_accessor :quiet, :yaml, :ddr_dirname, :ddr_filename, :ddr_basedir, :text_dirname, :tree_filename
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
# set default config settings
|
8
|
+
@quiet = false # don't print progress to stdout
|
9
|
+
@yaml = true # append full YAML to text files
|
10
|
+
@ddr_filename = 'Summary.xml' # name of primary DDR file to open
|
11
|
+
@ddr_dirname = 'fmp_ddr' # directory containing DDR
|
12
|
+
@ddr_basedir = './' # base directory (containing fmp_ddr, fmp_text)
|
13
|
+
@text_dirname = 'fmp_text' # text file base directory
|
14
|
+
@tree_filename = 'tree.txt' # set to nil to disable tree file generation
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fmpvc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Martin S. Boswell
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: nokogiri
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.6.6
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.6.6
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.2'
|
69
|
+
description: Process FileMaker Pro Advanced's Database Design Report (DDR) to produce
|
70
|
+
textual representations of the design objects for use with version control systems,
|
71
|
+
text editors, etc.
|
72
|
+
email:
|
73
|
+
- mboswell@me.com
|
74
|
+
executables:
|
75
|
+
- fmpvc
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- ".gitignore"
|
80
|
+
- ".rspec"
|
81
|
+
- ".ruby-gemset"
|
82
|
+
- ".ruby-version"
|
83
|
+
- ".travis.yml"
|
84
|
+
- CODE_OF_CONDUCT.md
|
85
|
+
- Gemfile
|
86
|
+
- LICENSE.txt
|
87
|
+
- README.md
|
88
|
+
- Rakefile
|
89
|
+
- bin/console
|
90
|
+
- bin/run_current
|
91
|
+
- bin/setup
|
92
|
+
- exe/fmpvc
|
93
|
+
- fmpvc.gemspec
|
94
|
+
- lib/fmpvc.rb
|
95
|
+
- lib/fmpvc/DDR.rb
|
96
|
+
- lib/fmpvc/FMPReport.rb
|
97
|
+
- lib/fmpvc/configuration.rb
|
98
|
+
- lib/fmpvc/version.rb
|
99
|
+
homepage: http://rubygems.org/gems/fmpvc
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
metadata: {}
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.2.1
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: Create a text version of the design elements of a FileMaker database.
|
123
|
+
test_files: []
|