aem_lookout 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: