aem_lookout 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,31 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ aem_lookout (0.0.1)
5
+ builder (~> 3.2.2)
6
+ rb-fsevent (~> 0.9.4)
7
+ vlt_wrapper (~> 2.4.18)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ builder (3.2.2)
13
+ diff-lcs (1.2.5)
14
+ rb-fsevent (0.9.4)
15
+ rspec (2.14.1)
16
+ rspec-core (~> 2.14.0)
17
+ rspec-expectations (~> 2.14.0)
18
+ rspec-mocks (~> 2.14.0)
19
+ rspec-core (2.14.7)
20
+ rspec-expectations (2.14.5)
21
+ diff-lcs (>= 1.1.3, < 2.0)
22
+ rspec-mocks (2.14.5)
23
+ vlt_wrapper (2.4.18)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ bundler (~> 1.5)
30
+ aem_lookout!
31
+ rspec (~> 2.14.1)
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jordan Raine
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # AemLookout
2
+
3
+ Automatically deploys code changes while developing.
4
+
5
+ A short loop between when you make changes and when you see those changes is an important step to productivity!
6
+
7
+ ## Install
8
+
9
+ ```
10
+ gem install aem_lookout
11
+ ```
12
+
13
+ or add the following to your Gemfile:
14
+
15
+ ```
16
+ gem 'aem_lookout'
17
+ ```
18
+
19
+ and run `bundle install` from your shell.
20
+
21
+ ## Usage
22
+
23
+ 1. Create a lookout.json file for your code base, similar to the (example config file)[https://github.com/jnraine/aem_lookout/blob/master/example_config.json].
24
+ 2. Run `lookout` from the command line. May need to run `bundle exec lookout` if the executable is not found.
25
+ 3. Edit files — .java, .css, .content.xml, whatever — and watch them deploy to your local instance.
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it ( http://github.com/<my-github-username>/aem_lookout/fork )
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'aem_lookout/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "aem_lookout"
8
+ spec.version = AemLookout::VERSION
9
+ spec.authors = ["Jordan Raine"]
10
+ spec.email = ["jnraine@gmail.com"]
11
+ spec.summary = %q{Speeds up iteration loop while developing for AEM/CQ.}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = ["aem_lookout", "lookout"]
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency "vlt_wrapper", "~> 2.4.18"
21
+ spec.add_runtime_dependency "builder", "~> 3.2.2"
22
+ spec.add_runtime_dependency "rb-fsevent", "~> 0.9.4"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.5"
25
+ spec.add_development_dependency "rspec", "~> 2.14.1"
26
+ end
data/bin/aem_lookout ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ echo "Use `lookout` instead"
data/bin/lookout ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.dirname(__FILE__) + "/lib"
4
+ require 'aem_lookout'
5
+ require 'rb-fsevent'
6
+ require 'optparse'
7
+ require 'json'
8
+
9
+ options = {}
10
+ option_parser = OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{File.basename(__FILE__)} [-p CQ_REPO_PATH]"
12
+
13
+ opts.on("-p", "--path CQ_REPO_PATH", "Path to source code repo") do |cq_repo_path|
14
+ options[:cq_repo_path] = cq_repo_path
15
+ end
16
+
17
+ opts.on_tail("-h", "--help", "Show this message") do
18
+ puts opts
19
+ exit
20
+ end
21
+ end
22
+
23
+ option_parser.parse!
24
+
25
+ begin
26
+ @cq_repo_path = options.fetch(:cq_repo_path, File.expand_path("."))
27
+ rescue KeyError
28
+ puts option_parser
29
+ exit 1
30
+ end
31
+
32
+ def config
33
+ template = File.read(@cq_repo_path + "/lookout.json")
34
+ result = ERB.new(template).result
35
+ JSON.parse(result)
36
+ end
37
+
38
+ AemLookout::Watcher.run(@cq_repo_path, config)
@@ -0,0 +1,21 @@
1
+ // You can use ERB tags in this!
2
+ {
3
+ // array of CQ/AEM dev instance hostnames with HTTP basic auth and port
4
+ "instances": [
5
+ "http://admin:<%= ENV["CQ_PASSWORD"] %>@localhost:4502"
6
+ ],
7
+ // array of relative paths to vault package roots (where jcr_root and META-INF live)
8
+ "jcrRootPaths": [
9
+ "jcr_content/src/content/jcr_root"
10
+ ],
11
+ // array of objects specifying filesystem to jcr path mappings. Haven't finished this yet.
12
+ "slingInitialContentPaths": [
13
+ {"filesystem": "java-core/src/main/resources/components", "jcr": "/apps/clf/components"},
14
+ {"filesystem": "java-core/src/main/resources/clientlibs", "jcr": "/etc/designs/clf/clientlibs"}
15
+ ],
16
+ // generic commands to do something when a specific
17
+ "commands": [
18
+ // watch a bundle for changes to classes and install to local instance when it happens
19
+ {"watch": "java-core/src/main/java", "pwd": "java-core", "command": "mvn install -P author-localhost"}
20
+ ]
21
+ }
@@ -0,0 +1,3 @@
1
+ module AemLookout
2
+ class LookoutError < Exception; end
3
+ end
@@ -0,0 +1,98 @@
1
+ require 'json'
2
+ require 'builder'
3
+
4
+ module AemLookout
5
+ class SlingInitialContentConverter
6
+ def self.convert(json_string)
7
+ node_data = JSON.parse(json_string)
8
+ builder = Builder::XmlMarkup.new
9
+ serialized_attributes, children = group_data(node_data)
10
+
11
+ builder.tag!("jcr:root", namespaces.merge(serialized_attributes)) do |b|
12
+ add_children(children, b)
13
+ end
14
+ rescue JSON::ParserError => e
15
+ raise SlingInitialContentConverterError, "A problem occurred while parsing JSON descriptor file: #{e.message}"
16
+ end
17
+
18
+ def self.convert_package(package_path)
19
+ json_files = json_files_within(package_path)
20
+ json_files.each do |json_file|
21
+ content_xml_path = generate_content_xml_path(json_file)
22
+ FileUtils.mkdir_p(content_xml_path.parent)
23
+ xml_string = convert(File.read(json_file))
24
+ File.open(content_xml_path, 'w') {|f| f.write(xml_string) }
25
+ FileUtils.rm(json_file)
26
+ end
27
+ end
28
+
29
+ def self.json_files_within(path)
30
+ glob_path = Pathname(path).to_s + "/**/*"
31
+ Dir.glob(glob_path.to_s).delete_if {|path| !path.match(/\.json$/) }
32
+ end
33
+
34
+ def self.generate_content_xml_path(json_path)
35
+ raise ArgumentError, "#{json_path} must end in .json" unless json_path.end_with?(".json")
36
+ node_name = File.basename(json_path, ".json")
37
+ Pathname(json_path).parent + node_name + ".content.xml"
38
+ end
39
+
40
+ def self.add_children(children, builder)
41
+ children.each do |name, node_data|
42
+ serialized_attributes, children = group_data(node_data)
43
+ builder.tag!(name, serialized_attributes) do |b|
44
+ add_children(children, b)
45
+ end
46
+ end
47
+ end
48
+
49
+ # Divide node data into serialized attributes and children
50
+ def self.group_data(node_data)
51
+ serialized_attributes = {}
52
+ children = {}
53
+ node_data.each do |key,value|
54
+ if !value.is_a?(Hash)
55
+ serialized_attributes[key] = convert_to_serialized_jcr_value(value)
56
+ else
57
+ children[key] = value
58
+ end
59
+ end
60
+
61
+ [default_attributes.merge(serialized_attributes), children]
62
+ end
63
+
64
+ def self.default_attributes
65
+ {
66
+ "jcr:primaryType" => "nt:unstructured"
67
+ }
68
+ end
69
+
70
+ # All known namespaces
71
+ def self.namespaces
72
+ {
73
+ "xmlns:cq" => "http://www.day.com/jcr/cq/1.0",
74
+ "xmlns:sling" => "http://sling.apache.org/jcr/sling/1.0",
75
+ "xmlns:jcr" => "http://www.jcp.org/jcr/1.0",
76
+ "xmlns:vlt" => "http://www.day.com/jcr/vault/1.0",
77
+ "xmlns:nt" => "http://www.jcp.org/jcr/nt/1.0"
78
+ }
79
+ end
80
+
81
+ def self.convert_to_serialized_jcr_value(value)
82
+ if value == true || value == false
83
+ "{Boolean}#{value}"
84
+ elsif value.is_a?(Array)
85
+ values = value.map {|el| convert_to_serialized_jcr_value(el) }
86
+ "[#{values.join(",")}]"
87
+ elsif value.is_a?(Time)
88
+ "{Date}#{value.strftime("%Y-%m-%dT%H:%M:%S.%L%:z")}"
89
+ elsif value.is_a?(String)
90
+ value
91
+ else
92
+ raise "Unknown type, cannot serialize #{value.class} value: #{value}"
93
+ end
94
+ end
95
+ end
96
+
97
+ class SlingInitialContentConverterError < LookoutError; end
98
+ end
@@ -0,0 +1,186 @@
1
+ require 'uri'
2
+ require 'pathname'
3
+ require 'erb'
4
+ require 'tmpdir'
5
+ require 'logger'
6
+
7
+ module AemLookout
8
+ class Sync
9
+ class Hostname
10
+ attr_accessor :url
11
+
12
+ def initialize(hostname)
13
+ @url = URI.parse(hostname)
14
+ end
15
+
16
+ def url_without_credentials
17
+ "#{url.scheme}://#{url.host}:#{url.port}/crx/-/jcr:root"
18
+ end
19
+
20
+ def credentials
21
+ "#{url.user}:#{url.password}"
22
+ end
23
+ end
24
+
25
+ def self.run(options)
26
+ self.new(options).run
27
+ end
28
+
29
+ attr_accessor :log, :hostnames, :filesystem_path, :jcr_path
30
+
31
+ def initialize(options)
32
+ default_log = Logger.new(options.fetch(:output, STDOUT))
33
+ @log = options.fetch(:log, default_log)
34
+ @hostnames = options.fetch(:hostnames).map {|hostname| Hostname.new(hostname) }
35
+ @sling_initial_content = options.fetch(:sling_initial_content, false)
36
+ @filesystem_path = options.fetch(:filesystem)
37
+ @jcr_path = options.fetch(:jcr)
38
+ rescue KeyError => e
39
+ raise ArgumentError.new("#{e.message} (missing a hash argument)")
40
+ end
41
+
42
+ # Locals on the local filesystem to copy into the package
43
+ def content_paths
44
+ if @content_paths.nil?
45
+ paths = if filesystem_path.end_with?(".content.xml")
46
+ Pathname(filesystem_path).parent.to_s
47
+ elsif sling_initial_content? and filesystem_path.end_with?(".json")
48
+ [filesystem_path, filesystem_path.gsub(/.json$/, "")].delete_if { |path| !File.exist?(path) }
49
+ else
50
+ filesystem_path
51
+ end
52
+
53
+ @content_paths = Array(paths)
54
+ end
55
+
56
+ @content_paths
57
+ end
58
+
59
+ # Paths in the package to install into the JCR
60
+ def filter_paths
61
+ if @filter_paths.nil?
62
+ paths = if jcr_path.end_with?(".content.xml")
63
+ Pathname(jcr_path).parent.to_s
64
+ elsif sling_initial_content? and jcr_path.end_with?(".json")
65
+ jcr_path.gsub(/.json$/, "")
66
+ else
67
+ jcr_path
68
+ end
69
+
70
+ @filter_paths = Array(paths)
71
+ end
72
+
73
+ @filter_paths
74
+ end
75
+
76
+ def run
77
+ start_timer
78
+ build_package
79
+ install_package
80
+ log_elapsed_time
81
+ end
82
+
83
+ def start_timer
84
+ @start_time = Time.now
85
+ end
86
+
87
+ def log_elapsed_time
88
+ elapsed_time = (Time.now.to_f - @start_time.to_f).round(2)
89
+ log.info "Elapsed time: #{elapsed_time} seconds"
90
+ end
91
+
92
+ def build_package
93
+ copy_content_to_package
94
+ create_settings
95
+ create_filter
96
+ SlingInitialContentConverter.convert_package(package_path) if sling_initial_content?
97
+ sleep 0.1 # just in case
98
+ end
99
+
100
+ def copy_content_to_package
101
+ FileUtils.mkdir_p(target_content_path_root)
102
+ content_paths.each do |content_path|
103
+ log.info "Copying content from #{content_path} to target content path root"
104
+ FileUtils.cp_r(content_path, target_content_path_root)
105
+ end
106
+ end
107
+
108
+ def create_settings
109
+ FileUtils.mkdir_p(vault_config_path)
110
+ File.open(vault_config_path + "settings.xml", 'w') {|f| f.write(settings_template) }
111
+ end
112
+
113
+ def create_filter
114
+ File.open(vault_config_path + "filter.xml", 'w') do |f|
115
+ paths = filter_paths
116
+ f.write(ERB.new(filter_template).result(binding))
117
+ end
118
+ end
119
+
120
+ def sling_initial_content_filter_paths_for(jcr_path)
121
+
122
+ end
123
+
124
+ def install_package
125
+ hostnames.map do |hostname|
126
+ install_package_to_hostname(hostname)
127
+ end
128
+ end
129
+
130
+ def install_package_multithreaded
131
+ threads = hostnames.map do |hostname|
132
+ Thread.new do
133
+ install_package_to_hostname(hostname)
134
+ end
135
+ end
136
+
137
+ threads.each {|thread| thread.join } # wait for threads
138
+ end
139
+
140
+ def install_package_to_hostname(hostname)
141
+ log.info "Installing package at #{package_path} to #{hostname.url_without_credentials}"
142
+ command = "#{AemLookout.vlt_executable} --credentials #{hostname.credentials} -v import #{hostname.url_without_credentials} #{package_path} /"
143
+ Terminal.new(log).execute_command(command)
144
+ end
145
+
146
+ def vault_config_path
147
+ package_path + "META-INF/vault"
148
+ end
149
+
150
+ def target_content_path_root
151
+ (jcr_root_path + jcr_path.gsub(/^\//, "")).parent
152
+ end
153
+
154
+ def package_path
155
+ @package_path ||= Pathname(Dir.mktmpdir("vlt-sync"))
156
+ end
157
+
158
+ def jcr_root_path
159
+ @jcr_root ||= package_path + "jcr_root"
160
+ end
161
+
162
+ def sling_initial_content?
163
+ @sling_initial_content
164
+ end
165
+
166
+ def settings_template
167
+ <<-EOF
168
+ <?xml version="1.0" encoding="UTF-8"?>
169
+ <vault version="0.1">
170
+ <ignore name=".svn"/>
171
+ <ignore name=".gitignore"/>
172
+ <ignore name=".DS_Store"/>
173
+ </vault>
174
+ EOF
175
+ end
176
+
177
+ def filter_template
178
+ <<-EOF
179
+ <?xml version="1.0" encoding="UTF-8"?>
180
+ <workspaceFilter vesion="0.1">
181
+ <% paths.each do |path| %> <filter root="<%= path %>" mode="replace"/>\n<% end %>
182
+ </workspaceFilter>
183
+ EOF
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,41 @@
1
+ require 'open3'
2
+
3
+ # A helper class for running terminal commands in the background while
4
+ # streaming to a Ruby logger.
5
+ module AemLookout
6
+ class Terminal
7
+ attr_reader :log
8
+
9
+ def initialize(log = Logger.new(STDOUT))
10
+ @log = log
11
+ end
12
+
13
+ def execute_command(command)
14
+ Open3.popen3(command) do |stdin, stdout, stderr, thread|
15
+ flush(stdout: stdout, stderr: stderr) until !thread.alive?
16
+ flush(stdout: stdout, stderr: stderr)
17
+ end
18
+ end
19
+
20
+ def flush(options = {})
21
+ stdout_thread = stream_to_log(options.fetch(:stdout))
22
+ stderr_thread = stream_to_log(options.fetch(:stderr), error: true)
23
+ sleep 0.1 until !stdout_thread.alive? and !stdout_thread.alive?
24
+ end
25
+
26
+ def stream_to_log(io, options = {})
27
+ options = options.merge({error: false})
28
+ thread = Thread.new do
29
+ while message = io.gets
30
+ if options.fetch(:error)
31
+ log.error message.chomp
32
+ else
33
+ log.info message.chomp
34
+ end
35
+
36
+ sleep 0.01 # to ensure line isn't still being written to
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module AemLookout
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,223 @@
1
+ module AemLookout
2
+ class Watcher
3
+ def self.run(repo_path, config)
4
+ self.new(repo_path, config).run
5
+ end
6
+
7
+ attr_accessor :repo_path, :config, :log
8
+
9
+ def initialize(repo_path, config, log = nil)
10
+ @repo_path = repo_path
11
+ @config = config
12
+ @log = log || Logger.new(STDOUT)
13
+ end
14
+
15
+ def run
16
+ threads = jcr_root_paths.map do |jcr_root_paths|
17
+ Thread.new { watch_vault_package(jcr_root_paths) }
18
+ end
19
+
20
+ threads += sling_initial_content_paths.map do |sling_initial_content_path|
21
+ Thread.new { watch_sling_initial_content(sling_initial_content_path) }
22
+ end
23
+
24
+ threads += command_configs.map do |command_config|
25
+ Thread.new { watch_command_config(command_config) }
26
+ end
27
+
28
+ wait_for(threads)
29
+ end
30
+
31
+ def wait_for(threads)
32
+ sleep 1 until Array(threads).map {|thread| thread.join(1) }.all? {|result| result }
33
+ end
34
+
35
+ def watch_vault_package(jcr_root)
36
+ if !File.exist?(jcr_root)
37
+ log.warn "jcr_root points to non-existing directory: #{jcr_root}"
38
+ return
39
+ end
40
+
41
+ options = {:latency => 0.1, :file_events => true}
42
+ fsevent = create_threaded_fsevent jcr_root.to_s, options do |paths|
43
+ sync_vault_package_paths(paths)
44
+ end
45
+
46
+ log.info "Watching jcr_root at #{jcr_root} for changes..."
47
+ fsevent.run
48
+ end
49
+
50
+ # Watch a given path with speified options and call action_block when a
51
+ # non-ignored path is modified. This also ensures that only one
52
+ # action_block is running at a time, killing any other running
53
+ # blocks before starting a new one.
54
+ def create_threaded_fsevent(watch_path, options, &action_block)
55
+ fsevent = FSEvent.new
56
+ running_jobs = Set.new
57
+
58
+ fsevent.watch watch_path, options do |paths|
59
+ paths.delete_if {|path| ignored?(path) }
60
+ log.warn "Detected change inside: #{paths.inspect}" unless paths.empty?
61
+
62
+ if running_jobs.length > 0
63
+ log.warn "A job is currently running for this watcher, killing..."
64
+ running_jobs.each {|thread| thread.kill }
65
+ else
66
+ log.warn "Phew, no running jobs: #{running_jobs}"
67
+ end
68
+
69
+ job = Thread.new do
70
+ action_block.call(paths)
71
+ Thread.exit
72
+ end
73
+
74
+ track_job_on_list(job, running_jobs)
75
+ end
76
+
77
+ fsevent
78
+ end
79
+
80
+ # Adds to running job list and removes from list when thread completes.
81
+ def track_job_on_list(job, running_jobs)
82
+ Thread.new do
83
+ running_jobs << job
84
+ log.warn "Waiting for #{job} to finish"
85
+ wait_for(job)
86
+ log.warn "#{job} job finished"
87
+ running_jobs.delete(job)
88
+ Thread.exit
89
+ end
90
+ end
91
+
92
+ def sync_vault_package_paths(paths)
93
+ paths.each do |path|
94
+ if !File.exist?(path)
95
+ log.warn "#{path} no longer exists, syncing parent instead"
96
+ path = File.dirname(path)
97
+ end
98
+
99
+ jcr_path = discover_jcr_path_from_file_in_vault_package(path)
100
+
101
+ AemLookout::Sync.new(
102
+ hostnames: hostnames,
103
+ filesystem: path,
104
+ jcr: jcr_path,
105
+ log: log
106
+ ).run
107
+ end
108
+ end
109
+
110
+ def watch_sling_initial_content(path)
111
+ filesystem_path = path.fetch("filesystem")
112
+ jcr_path = path.fetch("jcr")
113
+
114
+ if !File.exist?(filesystem_path)
115
+ log.warn "Filesystem path for Sling-Initial-Content points to non-existing directory: #{filesystem_path}"
116
+ return
117
+ end
118
+
119
+ options = {:latency => 0.1, :file_events => true}
120
+ fsevent = create_threaded_fsevent filesystem_path.to_s, options do |paths|
121
+ begin
122
+ handle_sling_initial_content_change(paths, filesystem_path, jcr_path)
123
+ rescue LookoutError => e
124
+ log.error "An error occurred while handling sling initial content change: #{e.message}"
125
+ end
126
+ end
127
+
128
+ log.info "Watching Sling-Initial-Content at #{filesystem_path} for changes..."
129
+ fsevent.run
130
+ end
131
+
132
+ def handle_sling_initial_content_change(paths, filesystem_path, jcr_path)
133
+ paths.each do |path|
134
+ if !File.exist?(path)
135
+ log.info "#{path} no longer exists, syncing parent instead"
136
+ path = File.dirname(path)
137
+ end
138
+
139
+ relative_jcr_path = path.gsub(/^.+#{filesystem_path}\//, "")
140
+
141
+ AemLookout::Sync.new(
142
+ hostnames: hostnames,
143
+ filesystem: path,
144
+ jcr: (Pathname(jcr_path) + relative_jcr_path).to_s,
145
+ log: log,
146
+ sling_initial_content: true
147
+ ).run
148
+ end
149
+ end
150
+
151
+ def watch_command_config(command_config)
152
+ watch_path = command_config.fetch("watch")
153
+ pwd = Pathname(repo_path) + command_config.fetch("pwd", "")
154
+ command = command_config.fetch("command")
155
+
156
+ options = {:latency => 1, :file_events => true}
157
+ fsevent = create_threaded_fsevent watch_path, options do |paths|
158
+ break if paths.empty?
159
+ log.info "Running command"
160
+ Terminal.new(log).execute_command("cd #{pwd} && #{command}")
161
+ end
162
+
163
+ log.info "Watching #{watch_path}, changes will run #{command.inspect}..."
164
+ fsevent.run
165
+ end
166
+
167
+ def jcr_root_paths
168
+ config.fetch("jcrRootPaths", []).map {|jcr_root_path| Pathname(repo_path) + jcr_root_path }.map(&:to_s)
169
+ end
170
+
171
+ def sling_initial_content_paths
172
+ config.fetch("slingInitialContentPaths", []).map do |sling_initial_content_path|
173
+ validate_sling_initial_content_path!(sling_initial_content_path)
174
+ sling_initial_content_path
175
+ end
176
+ end
177
+
178
+ # Something like this [{"watch" => "java-core/src/main/java", "pwd" => "java-core", "command" => "mvn install -P author-localhost"}]
179
+ def command_configs
180
+ config.fetch("commands", []).map do |command_config|
181
+ validate_command_config!(command_config)
182
+ command_config
183
+ end
184
+ end
185
+
186
+ # Ensures required keys are present. This is ugly.
187
+ def validate_command_config!(command_config)
188
+ required_keys = ["watch", "command"]
189
+ unless command_config.has_key?(required_keys.first) and command_config.has_key?(required_keys.last)
190
+ raise "commands entry is malformed (requires these keys: #{required_keys.join(", ")}): #{command_config.inspect}"
191
+ end
192
+ end
193
+
194
+ def validate_sling_initial_content_path!(path)
195
+ unless path.has_key?("filesystem") and path.has_key?("jcr")
196
+ raise "slingInitialContentPaths entry is malformed (requires \"filesystem\" and \"jcr\" entry): #{path.inspect}"
197
+ end
198
+ end
199
+
200
+ def hostnames
201
+ config.fetch("instances")
202
+ end
203
+
204
+ # Return true if file should not trigger a sync
205
+ def ignored?(file)
206
+ return true if File.extname(file) == ".tmp"
207
+ return true if file.match(/___$/)
208
+ return true if File.basename(file) == ".DS_Store"
209
+ return false
210
+ end
211
+
212
+ # Find the root of the package to determine the path used for the filter
213
+ def discover_jcr_path_from_file_in_vault_package(filesystem_path)
214
+ possible_jcr_root = Pathname(filesystem_path).parent
215
+ while !possible_jcr_root.root?
216
+ break if possible_jcr_root.basename.to_s == "jcr_root"
217
+ possible_jcr_root = possible_jcr_root.parent
218
+ end
219
+
220
+ filesystem_path.gsub(/^#{possible_jcr_root.to_s}/, "")
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,17 @@
1
+ require 'vlt_wrapper'
2
+
3
+ require 'aem_lookout/version'
4
+ require 'aem_lookout/lookout_error'
5
+ require 'aem_lookout/sling_initial_content_converter'
6
+ require 'aem_lookout/sync'
7
+ require 'aem_lookout/terminal'
8
+ require 'aem_lookout/watcher'
9
+
10
+
11
+ module AemLookout
12
+ def vlt_executable
13
+ VltWrapper.executable
14
+ end
15
+
16
+ extend self
17
+ end
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+
3
+ describe AemLookout::SlingInitialContentConverter do
4
+ it "converts a json file into a .content.xml file" do
5
+ node_example = {
6
+ "cq:isContainer" => false,
7
+ "jcr:description" => "A list of upcoming events pulled from a calendar",
8
+ "jcr:primaryType" => "cq:Component",
9
+ "jcr:title" => "Upcoming Events",
10
+ "componentGroup" => "Social"
11
+ }
12
+ content_xml = AemLookout::SlingInitialContentConverter.convert(node_example.to_json)
13
+ xml_exemplar = '<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:vlt="http://www.day.com/jcr/vault/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" cq:isContainer="{Boolean}false" jcr:description="A list of upcoming events pulled from a calendar" jcr:primaryType="cq:Component" jcr:title="Upcoming Events" componentGroup="Social"></jcr:root>'
14
+ expect(content_xml).to eq(xml_exemplar)
15
+ end
16
+
17
+ # xit "prints an example of a complex JSON desciptor file" do
18
+ # json = File.read("/Users/jnraine/projects/cq/java-core/src/main/resources/components/twitter-timeline.json")
19
+ # puts AemLookout::SlingInitialContentConverter.convert(json)
20
+ # end
21
+
22
+ describe ".convert_to_serialized_jcr_value" do
23
+ let(:klass) { AemLookout::SlingInitialContentConverter }
24
+
25
+ it "converts booleans to serialized JCR booleans" do
26
+ klass.convert_to_serialized_jcr_value(true).should == "{Boolean}true"
27
+ klass.convert_to_serialized_jcr_value(false).should == "{Boolean}false"
28
+ end
29
+
30
+ it "converts arrays to serialized JCR arrays" do
31
+ klass.convert_to_serialized_jcr_value(["foo", "bar", "baz"]).should == "[foo,bar,baz]"
32
+ end
33
+
34
+ it "converts dates to serialized JCR dates" do
35
+ # this only handles local time
36
+ local_time = Time.parse("2010-03-17T17:14:41.775")
37
+ klass.convert_to_serialized_jcr_value(local_time).should == "{Date}2010-03-17T17:14:41.775-07:00"
38
+ end
39
+ end
40
+
41
+
42
+ describe "#convert_json_descriptor_files" do
43
+ def build_fake_package
44
+ tmp_dir = Pathname(Dir.mktmpdir("sync-test"))
45
+ File.open(tmp_dir + "#{node_name}.json", 'w') {|f| f.write(properties.to_json) }
46
+ tmp_dir
47
+ end
48
+
49
+ let(:properties) do
50
+ {foo: "hello", bar: "world", baz: "ur nice"}
51
+ end
52
+
53
+ let(:node_name) { "foo" }
54
+ let(:fake_package_path) { build_fake_package }
55
+
56
+ it "takes any json files at a given path and converts them to .content.xml files" do
57
+ AemLookout::SlingInitialContentConverter.convert_package(fake_package_path)
58
+ content_xml_path = fake_package_path + "foo" + ".content.xml"
59
+ expect(File).to exist(content_xml_path)
60
+ expect(File.read(content_xml_path)).to match("foo=\"hello\" bar=\"world\" baz=\"ur nice\"")
61
+ end
62
+ end
63
+
64
+ describe ".json_files_within" do
65
+ it "returns all json files found within a directory structure recursively" do
66
+ # setup
67
+ tmp_dir = Pathname(Dir.mktmpdir("test"))
68
+ nested_dir = tmp_dir + "foo" + "bar" + "baz"
69
+ FileUtils.mkdir_p(nested_dir.to_s)
70
+ sleep 0.01
71
+ FileUtils.touch([tmp_dir + "my.json", nested_dir + "another.json"])
72
+ # execute and assert
73
+ json_files = AemLookout::SlingInitialContentConverter.json_files_within(tmp_dir)
74
+ expect(json_files.length).to eq(2)
75
+ json_files.each {|json_file| expect(json_file).to end_with(".json") }
76
+ end
77
+ end
78
+
79
+ describe ".generate_content_xml_path" do
80
+ it "creates a content XML path for the named node" do
81
+ content_xml_path = AemLookout::SlingInitialContentConverter.generate_content_xml_path("/foo/my_node.json")
82
+ expect(content_xml_path).to eq(Pathname("/foo/my_node/.content.xml"))
83
+ end
84
+
85
+ it "raises an error when passed a path to a non-json file" do
86
+ expect {
87
+ AemLookout::SlingInitialContentConverter.generate_content_xml_path("/foo/my_node.txt")
88
+ }.to raise_error(ArgumentError)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe AemLookout::Sync do
4
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe AemLookout do
4
+ it "knows where the vlt executable lives" do
5
+ vlt_executable = AemLookout.vlt_executable
6
+ expect(File.basename(vlt_executable)).to eq("vlt")
7
+ expect(File.executable?(vlt_executable)).to eq(true)
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ $: << File.expand_path("..", File.dirname(__FILE__))
2
+ require 'aem_lookout'
3
+
4
+ RSpec.configure do |config|
5
+ config.treat_symbols_as_metadata_keys_with_true_values = true
6
+ config.run_all_when_everything_filtered = true
7
+ config.filter_run :focus
8
+
9
+ config.order = 'random'
10
+ end
data/sync.sh ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+
3
+ VLT=/Users/jnraine/projects/cq/instances/author/crx-quickstart/opt/filevault/vault-cli-2.4.18/bin/vlt
4
+ WHERE_JCR_ROOT_AND_META_INF_IS=/tmp/playground
5
+ JCR_ROOT_PATH=/
6
+ CQ_HOST="http://localhost:4502"
7
+ CREDENTIALS="admin:cq4me"
8
+
9
+ $VLT --credentials $CREDENTIALS -v import $CQ_HOST/crx/-/jcr:root $WHERE_JCR_ROOT_AND_META_INF_IS $JCR_ROOT_PATH
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aem_lookout
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jordan Raine
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-03-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: vlt_wrapper
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.4.18
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.4.18
30
+ - !ruby/object:Gem::Dependency
31
+ name: builder
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 3.2.2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 3.2.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: rb-fsevent
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.9.4
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.9.4
62
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.5'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '1.5'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 2.14.1
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 2.14.1
94
+ description:
95
+ email:
96
+ - jnraine@gmail.com
97
+ executables:
98
+ - aem_lookout
99
+ - lookout
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - Gemfile
104
+ - Gemfile.lock
105
+ - LICENSE.txt
106
+ - README.md
107
+ - Rakefile
108
+ - aem_lookout.gemspec
109
+ - bin/aem_lookout
110
+ - bin/lookout
111
+ - example_config.json
112
+ - lib/aem_lookout.rb
113
+ - lib/aem_lookout/lookout_error.rb
114
+ - lib/aem_lookout/sling_initial_content_converter.rb
115
+ - lib/aem_lookout/sync.rb
116
+ - lib/aem_lookout/terminal.rb
117
+ - lib/aem_lookout/version.rb
118
+ - lib/aem_lookout/watcher.rb
119
+ - spec/lib/aem_lookout/sling_initial_content_converter_spec.rb
120
+ - spec/lib/aem_lookout/sync_spec.rb
121
+ - spec/lib/aem_lookout_spec.rb
122
+ - spec/spec_helper.rb
123
+ - sync.sh
124
+ homepage: ''
125
+ licenses:
126
+ - MIT
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 1.8.23
146
+ signing_key:
147
+ specification_version: 3
148
+ summary: Speeds up iteration loop while developing for AEM/CQ.
149
+ test_files:
150
+ - spec/lib/aem_lookout/sling_initial_content_converter_spec.rb
151
+ - spec/lib/aem_lookout/sync_spec.rb
152
+ - spec/lib/aem_lookout_spec.rb
153
+ - spec/spec_helper.rb
154
+ has_rdoc: