pompompom 1.0.0

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.
Files changed (69) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +11 -0
  3. data/LICENSE +20 -0
  4. data/README.mdown +32 -0
  5. data/Rakefile +7 -0
  6. data/TODO +0 -0
  7. data/bin/pompompom +17 -0
  8. data/lib/pom_pom_pom.rb +1 -0
  9. data/lib/pompompom/cli.rb +87 -0
  10. data/lib/pompompom/dependency.rb +55 -0
  11. data/lib/pompompom/downloader.rb +15 -0
  12. data/lib/pompompom/metadata.rb +26 -0
  13. data/lib/pompompom/pom.rb +234 -0
  14. data/lib/pompompom/resolver.rb +149 -0
  15. data/lib/pompompom/url_builder.rb +37 -0
  16. data/lib/pompompom.rb +10 -0
  17. data/pompompom.gemspec +116 -0
  18. data/spec/pompompom/cli_spec.rb +93 -0
  19. data/spec/pompompom/dependency_spec.rb +144 -0
  20. data/spec/pompompom/metadata_spec.rb +38 -0
  21. data/spec/pompompom/pom_spec.rb +221 -0
  22. data/spec/pompompom/resolver_spec.rb +205 -0
  23. data/spec/pompompom/url_builders_shared.rb +33 -0
  24. data/spec/resources/repository/aopalliance/aopalliance/1.0/aopalliance-1.0.jar +0 -0
  25. data/spec/resources/repository/aopalliance/aopalliance/1.0/aopalliance-1.0.pom +15 -0
  26. data/spec/resources/repository/aopalliance/aopalliance/maven-metadata.xml +5 -0
  27. data/spec/resources/repository/com/example/test/8.8/test-8.8.jar +0 -0
  28. data/spec/resources/repository/com/example/test/8.8/test-8.8.pom +11 -0
  29. data/spec/resources/repository/com/example/test/9.9/test-9.9.jar +0 -0
  30. data/spec/resources/repository/com/example/test/9.9/test-9.9.pom +11 -0
  31. data/spec/resources/repository/com/example/test/maven-metadata.xml +11 -0
  32. data/spec/resources/repository/com/example/test-abc/1.0/test-abc-1.0.jar +0 -0
  33. data/spec/resources/repository/com/example/test-abc/1.0/test-abc-1.0.pom +19 -0
  34. data/spec/resources/repository/com/example/test-abc/maven-metadata.xml +13 -0
  35. data/spec/resources/repository/com/example/test-child/1.0/test-child-1.0.pom +16 -0
  36. data/spec/resources/repository/com/example/test-def/1.0/test-def-1.0.jar +0 -0
  37. data/spec/resources/repository/com/example/test-def/1.0/test-def-1.0.pom +19 -0
  38. data/spec/resources/repository/com/example/test-exclusions/1.0/test-exclusions-1.0.jar +1 -0
  39. data/spec/resources/repository/com/example/test-exclusions/1.0/test-exclusions-1.0.pom +25 -0
  40. data/spec/resources/repository/com/example/test-optional/1.0/test-optional-1.0.jar +1 -0
  41. data/spec/resources/repository/com/example/test-optional/1.0/test-optional-1.0.pom +20 -0
  42. data/spec/resources/repository/com/example/test-parent/1.0/test-parent-1.0.pom +16 -0
  43. data/spec/resources/repository/com/google/google/1/google-1.pom +37 -0
  44. data/spec/resources/repository/com/google/inject/guice/2.0/guice-2.0.jar +1 -0
  45. data/spec/resources/repository/com/google/inject/guice/2.0/guice-2.0.pom +20 -0
  46. data/spec/resources/repository/com/google/inject/guice-parent/2.0/guice-parent-2.0.pom +70 -0
  47. data/spec/resources/repository/com/rabbitmq/amqp-client/1.8.0/amqp-client-1.8.0.jar +1 -0
  48. data/spec/resources/repository/com/rabbitmq/amqp-client/1.8.0/amqp-client-1.8.0.pom +98 -0
  49. data/spec/resources/repository/commons-cli/commons-cli/1.1/commons-cli-1.1.jar +1 -0
  50. data/spec/resources/repository/commons-cli/commons-cli/1.1/commons-cli-1.1.pom +165 -0
  51. data/spec/resources/repository/commons-io/commons-io/1.2/commons-io-1.2.jar +1 -0
  52. data/spec/resources/repository/commons-io/commons-io/1.2/commons-io-1.2.pom +235 -0
  53. data/spec/resources/repository/javax/servlet/servlet-api/2.5/servlet-api-2.5.jar +0 -0
  54. data/spec/resources/repository/javax/servlet/servlet-api/2.5/servlet-api-2.5.pom +6 -0
  55. data/spec/resources/repository/net/iconara/pompompom/1.0/pompompom-1.0.jar +0 -0
  56. data/spec/resources/repository/net/iconara/pompompom/1.0/pompompom-1.0.pom +11 -0
  57. data/spec/resources/repository/org/eclipse/jetty/jetty-continuation/7.1.4.v20100610/jetty-continuation-7.1.4.v20100610.pom +73 -0
  58. data/spec/resources/repository/org/eclipse/jetty/jetty-http/7.1.4.v20100610/jetty-http-7.1.4.v20100610.pom +73 -0
  59. data/spec/resources/repository/org/eclipse/jetty/jetty-io/7.1.4.v20100610/jetty-io-7.1.4.v20100610.pom +62 -0
  60. data/spec/resources/repository/org/eclipse/jetty/jetty-parent/15/jetty-parent-15.pom +419 -0
  61. data/spec/resources/repository/org/eclipse/jetty/jetty-project/7.1.4.v20100610/jetty-project-7.1.4.v20100610.pom +368 -0
  62. data/spec/resources/repository/org/eclipse/jetty/jetty-server/7.1.4.v20100610/jetty-server-7.1.4.v20100610.jar +0 -0
  63. data/spec/resources/repository/org/eclipse/jetty/jetty-server/7.1.4.v20100610/jetty-server-7.1.4.v20100610.pom +101 -0
  64. data/spec/resources/repository/org/eclipse/jetty/jetty-util/7.1.4.v20100610/jetty-util-7.1.4.v20100610.pom +80 -0
  65. data/spec/spec_helper.rb +5 -0
  66. data/tasks/gem.rake +18 -0
  67. data/tasks/rdoc.rake +13 -0
  68. data/tasks/spec.rake +17 -0
  69. metadata +142 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ /tmp
3
+ /.bundle
4
+ /pkg
5
+ /TODO
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source :rubygems
2
+
3
+ gem 'immutable_struct'
4
+ gem 'hpricot'
5
+
6
+ group :testing do
7
+ gem 'rspec'
8
+ gem 'sdoc'
9
+ gem 'jeweler'
10
+ gem 'rake'
11
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Theo Hultberg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.mdown ADDED
@@ -0,0 +1,32 @@
1
+ # Pom Pom Pom
2
+
3
+ Pom Pom Pom is a basic dependency manager for Maven repository artifacts.
4
+
5
+ The following code would download `amqp-client-1.8.0.jar` and its dependencies, `commons-cli-1.1.jar` and `commons-io-1.1.jar`, to the directory `lib`:
6
+
7
+ dependencies = %w(com.rabbitmq:amqp-client:1.8.0 com.google.inject:guice:2.0)
8
+ dependencies = dependencies.map { |d| Dependency.parse(d) }
9
+ repositories = %w(http://repo1.maven.org/maven2)
10
+ resolver = Resolver.new(repositories)
11
+ resolver.download('lib', true, *dependencies)
12
+
13
+ There will be less verbose ways of doing this in the future. The goal is to be able to use this with rake, like this:
14
+
15
+ task :dependencies do
16
+ pompompom :artifacts => %w(com.rabbitmq:amqp-client:1.8.0 com.google.inject:guice:2.0)
17
+ end
18
+
19
+ Which would do the same as the code above.
20
+
21
+ There is a command line tool that can be used to install artifacts:
22
+
23
+ pompompom com.rabbitmq:amqp-client:1.8.0 com.google.inject:guice:2.0
24
+
25
+ It will create a directory called `lib` and download JARs into it. In the future things may be configurable, and POMs will be cached to make things quicker.
26
+
27
+ ## Why another dependency management tool, why not use Maven, Buildr, sbt or Ivy?
28
+
29
+ Every time I look at Maven I cringe. Buildr is almost as bad, but hides some of the complexity. Ivy is better, but still too much noisy XML.
30
+
31
+ Pom Pom Pom is meant to do more or less what Ivy does, but in a way that doesn't make you want to tear out your eyes.
32
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ $: << File.expand_path('../lib', __FILE__)
2
+
3
+ require 'pompompom'
4
+
5
+ task :default => :spec
6
+
7
+ Dir[File.join(File.dirname(__FILE__), 'tasks', '*.rake')].each { |t| load t }
data/TODO ADDED
File without changes
data/bin/pompompom ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if File.symlink?(__FILE__)
4
+ $: << File.expand_path('../../lib', File.readlink(__FILE__))
5
+ else
6
+ $: << File.expand_path('../../lib', __FILE__)
7
+ end
8
+
9
+ require 'rubygems'
10
+
11
+ gem 'hpricot'
12
+ gem 'immutable_struct'
13
+
14
+ require 'pompompom'
15
+ require 'pompompom/cli'
16
+
17
+ exit PomPomPom::Cli.new(STDIN, STDOUT, STDERR).run!(*ARGV)
@@ -0,0 +1 @@
1
+ require 'pompompom'
@@ -0,0 +1,87 @@
1
+ module PomPomPom
2
+ class Cli
3
+ STANDARD_REPOSITORIES = [
4
+ 'http://repo1.maven.org/maven2/'
5
+ ]
6
+
7
+ DEFAULT_DESTINATION_DIR = 'lib'
8
+
9
+ def initialize(stdin, stdout, stderr)
10
+ @stdin, @stdout, @stderr = stdin, stdout, stderr
11
+ @status_logger = EchoLogger.new(@stderr)
12
+ end
13
+
14
+ def run!(*args)
15
+ if args.empty?
16
+ print_usage
17
+ return 1
18
+ end
19
+
20
+ resolver = create_resolver
21
+
22
+ create_lib_directory!
23
+
24
+ @status_logger.info("Determining transitive dependencies...")
25
+
26
+ dependencies = parse_dependencies(args)
27
+ dependencies = resolver.find_transitive_dependencies(*dependencies)
28
+ dependencies = dependencies.reject { |d| File.exists?(File.join(DEFAULT_DESTINATION_DIR, d.jar_file_name)) }
29
+
30
+ if dependencies.empty?
31
+ @status_logger.info('All dependencies are met')
32
+ else
33
+ dependencies.each do |dependency|
34
+ @status_logger.info(%(Downloading "#{dependency.to_dependency.to_s}"))
35
+ resolver.download!(DEFAULT_DESTINATION_DIR, false, dependency)
36
+ end
37
+ end
38
+
39
+ return 0
40
+ rescue => e
41
+ @status_logger.warn(%(Warning: #{e.message}))
42
+ return 1
43
+ end
44
+
45
+ private
46
+
47
+ def create_lib_directory!
48
+ return if File.directory?(DEFAULT_DESTINATION_DIR)
49
+ raise %(Cannot create destination, "#{DEFAULT_DESTINATION_DIR}" is a file!) if File.exists?(DEFAULT_DESTINATION_DIR)
50
+ Dir.mkdir(DEFAULT_DESTINATION_DIR)
51
+ end
52
+
53
+ def create_resolver
54
+ Resolver.new(STANDARD_REPOSITORIES, :logger => @status_logger)
55
+ end
56
+
57
+ def parse_dependencies(args)
58
+ args.map do |coordinate|
59
+ begin
60
+ Dependency.parse(coordinate)
61
+ rescue ArgumentError => e
62
+ @status_logger.warn(%(Warning: "#{coordinate}" is not a valid artifact coordinate))
63
+ nil
64
+ end
65
+ end.compact
66
+ end
67
+
68
+ def print_usage
69
+ @status_logger.info('Usage: pompompom <group_id:artifact_id:version> [<group_id:artifact_id:version>]')
70
+ end
71
+
72
+ class EchoLogger
73
+ def initialize(io)
74
+ @io = io
75
+ end
76
+
77
+ def debug(msg); end
78
+
79
+ def info(msg)
80
+ @io.puts(msg)
81
+ end
82
+
83
+ alias_method :warn, :info
84
+ alias_method :fatal, :info
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,55 @@
1
+ require 'immutable_struct'
2
+
3
+
4
+ module PomPomPom
5
+ class Dependency < ImmutableStruct.new(:group_id, :artifact_id, :version, :packaging, :classifier, :optional, :exclusions)
6
+ include UrlBuilder
7
+
8
+ def self.parse(artifact_coordinates)
9
+ raise ArgumentError, %(Malformed artifact coordinate: "#{artifact_coordinates}") if artifact_coordinates.nil? || artifact_coordinates.strip.length == 0
10
+ components = artifact_coordinates.strip.split(':')
11
+ raise ArgumentError, %(Malformed artifact coordinate: "#{artifact_coordinates}") if components.size < 3 || components.size > 5
12
+ case components.size
13
+ when 3
14
+ group_id, artifact_id, version = components
15
+ when 4
16
+ group_id, artifact_id, packaging, version = components
17
+ when 5
18
+ group_id, artifact_id, packaging, classifier, version = components
19
+ end
20
+ Dependency.new(group_id, artifact_id, version, packaging, classifier)
21
+ end
22
+
23
+ def exclusions
24
+ self[:exclusions] || []
25
+ end
26
+
27
+ def optional?
28
+ optional
29
+ end
30
+
31
+ def has_version?
32
+ !version.nil?
33
+ end
34
+
35
+ def same_artifact?(o)
36
+ o.artifact_id == self.artifact_id && o.group_id == self.group_id
37
+ end
38
+
39
+ def eql?(o)
40
+ o.to_s == to_s
41
+ end
42
+
43
+ def hash
44
+ to_s.hash
45
+ end
46
+
47
+ def clone(overrides={})
48
+ self.class.new(self.to_h.merge(overrides))
49
+ end
50
+
51
+ def to_s
52
+ "#{group_id}:#{artifact_id}:#{version}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ require 'open-uri'
2
+
3
+ module PomPomPom
4
+ class Downloader
5
+ def get(url)
6
+ open(url).read
7
+ end
8
+ end
9
+
10
+ class FilesystemDownloader
11
+ def get(path)
12
+ File.read(path) if File.exists?(path)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ module PomPomPom
2
+ class Metadata
3
+ def initialize(io)
4
+ @io = io
5
+ end
6
+
7
+ def parse!
8
+ doc = Hpricot.XML(@io)
9
+ versioning = doc.at('/metadata/versioning')
10
+ if versioning
11
+ release = versioning.at('release/text()')
12
+ if release
13
+ @latest_version = release.to_s
14
+ else
15
+ @latest_version = versioning.search('versions/version/text()').map(&:to_s).sort.last
16
+ end
17
+ else
18
+ @latest_version = doc.at('/metadata/version/text()').to_s
19
+ end
20
+ end
21
+
22
+ def latest_version
23
+ @latest_version
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,234 @@
1
+ require 'hpricot'
2
+
3
+
4
+ module PomPomPom
5
+ class Pom
6
+ include UrlBuilder
7
+
8
+ PROPERTIES = [:group_id, :artifact_id, :version, :name, :description, :url, :packaging]
9
+
10
+ attr_reader *PROPERTIES
11
+ attr_reader :parent
12
+
13
+ def initialize(io, defaults={})
14
+ @io = io
15
+ merge_defaults!(defaults)
16
+ end
17
+
18
+ def parse!
19
+ doc = Hpricot.XML(@io)
20
+ parse_properties!(doc)
21
+ parse_meta!(doc)
22
+ parse_parent!(doc)
23
+ parse_dependency_management!(doc)
24
+ parse_dependencies!(doc)
25
+ end
26
+
27
+ def dependencies(scope = :default)
28
+ @dependencies.fetch(scope, []).dup
29
+ end
30
+
31
+ def exclusions
32
+ []
33
+ end
34
+
35
+ def has_parent?
36
+ !!parent
37
+ end
38
+
39
+ def group_id
40
+ @group_id || parent.group_id
41
+ end
42
+
43
+ def artifact_id
44
+ @artifact_id || parent.artifact_id
45
+ end
46
+
47
+ def version
48
+ @version || parent.version
49
+ end
50
+
51
+ def properties
52
+ if has_parent? && @parent.respond_to?(:properties)
53
+ @parent.properties.merge(@properties)
54
+ else
55
+ @properties || {}
56
+ end
57
+ end
58
+
59
+ def to_dependency
60
+ Dependency.new(
61
+ :group_id => group_id,
62
+ :artifact_id => artifact_id,
63
+ :version => version,
64
+ :packaging => packaging,
65
+ :dependencies => dependencies
66
+ )
67
+ end
68
+
69
+ def merge(parent)
70
+ defaults = Hash[*PROPERTIES.map { |p| [p, self.send(p) || parent.send(p)] }.flatten]
71
+ defaults = defaults.merge(
72
+ :parent => parent,
73
+ :dependencies => @dependencies.dup,
74
+ :dependency_management => @dependency_management.dup,
75
+ :properties => @properties.dup
76
+ )
77
+ self.class.new(nil, defaults)
78
+ end
79
+
80
+ def to_s
81
+ to_dependency.to_s
82
+ end
83
+
84
+ protected
85
+
86
+ def resolve_version(dependency)
87
+ @dependency_management.find { |d| d.same_artifact?(dependency) } || dependency
88
+ end
89
+
90
+ private
91
+
92
+ def camelize(str)
93
+ if str.include?('_')
94
+ components = str.split('_')
95
+ components.first + components[1..-1].map { |s| s.capitalize }.join('')
96
+ else
97
+ str
98
+ end
99
+ end
100
+
101
+ def snakeify(str)
102
+ str.gsub(/([a-z])([A-Z])/) do |match|
103
+ "#{$1}_#{$2.downcase}"
104
+ end
105
+ end
106
+
107
+ def merge_defaults!(defaults)
108
+ @parent = defaults[:parent]
109
+ @dependency_management = defaults.fetch(:dependency_management, [])
110
+ @properties = defaults.fetch(:properties, [])
111
+
112
+ PROPERTIES.each do |p|
113
+ if defaults.has_key?(p)
114
+ value = defaults[p]
115
+ value = expand_properties(value) if value
116
+ instance_variable_set('@' + p.to_s, value)
117
+ end
118
+ end
119
+
120
+ default_dependencies = defaults.fetch(:dependencies, {})
121
+
122
+ @dependencies = default_dependencies.keys.inject({}) do |deps, scope|
123
+ deps[scope] = default_dependencies[scope].map do |dependency|
124
+ dependency = dependency.clone(
125
+ :group_id => expand_properties(dependency.group_id),
126
+ :artifact_id => expand_properties(dependency.artifact_id),
127
+ :version => expand_properties(dependency.version)
128
+ )
129
+
130
+ if dependency.has_version?
131
+ dependency
132
+ elsif @parent
133
+ @parent.resolve_version(dependency)
134
+ end
135
+ end
136
+ deps
137
+ end
138
+ end
139
+
140
+ def parse_properties!(doc)
141
+ @properties = {}
142
+ doc.search('/project/properties/*').each do |property_node|
143
+ if property_node.elem?
144
+ @properties[property_node.name] = property_node.inner_text.to_s
145
+ end
146
+ end
147
+ end
148
+
149
+ def parse_meta!(doc)
150
+ properties = PROPERTIES.map { |p| [p.to_s, camelize(p.to_s)] }
151
+ properties.each do |property, tag_name|
152
+ val = doc.at("/project/#{tag_name}/text()")
153
+ instance_variable_set('@' + property, expand_properties(val.to_s)) if val
154
+ end
155
+ end
156
+
157
+ def parse_parent!(doc)
158
+ parent_node = doc.at("/project/parent")
159
+ @parent = parse_dependency(parent_node) if parent_node
160
+ end
161
+
162
+ def parse_dependencies!(doc)
163
+ doc.search('/project/dependencies/dependency').each do |dep_node|
164
+ scope = parse_scope(dep_node)
165
+ @dependencies[scope] ||= []
166
+ @dependencies[scope] << parse_dependency(dep_node)
167
+ end
168
+ end
169
+
170
+ def parse_dependency_management!(doc)
171
+ doc.search('/project/dependencyManagement/dependencies/dependency').each do |dep_node|
172
+ @dependency_management << parse_dependency(dep_node)
173
+ end
174
+ end
175
+
176
+ def parse_dependency(dep_node)
177
+ Dependency.new(
178
+ :group_id => parse_attr(dep_node, 'groupId'),
179
+ :artifact_id => parse_attr(dep_node, 'artifactId'),
180
+ :version => parse_version(dep_node),
181
+ :optional => parse_attr(dep_node, 'optional').downcase == 'true',
182
+ :exclusions => parse_exclusions(dep_node)
183
+ )
184
+ end
185
+
186
+ def parse_attr(dep_node, attr_name)
187
+ str = expand_properties(dep_node.at("#{attr_name}/text()").to_s)
188
+ end
189
+
190
+ def expand_properties(str)
191
+ return nil if str.nil?
192
+ str.gsub(/\$\{([.\w]+)\}/) do |match|
193
+ property_value($1)
194
+ end
195
+ end
196
+
197
+ def property_value(property_name)
198
+ custom_properties = {'version' => @version}.merge(properties)
199
+ if custom_properties.has_key?(property_name)
200
+ custom_properties[property_name]
201
+ else
202
+ components = property_name.split('.')
203
+ if components.first == 'project' && components.size == 2 && self.respond_to?(snakeify(components[1]))
204
+ self.send(snakeify(components[1]))
205
+ elsif components.first == 'env' && components.size == 2
206
+ ENV[components[1]]
207
+ #elsif components.first == 'settings' # not yet supported
208
+ else
209
+ "${#{property_name}}"
210
+ end
211
+ end
212
+ end
213
+
214
+ def parse_version(dep_node)
215
+ v = parse_attr(dep_node, 'version')
216
+ if v.length == 0 then nil else v end
217
+ end
218
+
219
+ def parse_scope(dep_node)
220
+ scope = parse_attr(dep_node, 'scope')
221
+ scope = 'default' if scope.nil? || scope.strip.length == 0
222
+ scope.to_sym
223
+ end
224
+
225
+ def parse_exclusions(dep_node)
226
+ dep_node.search('exclusions/exclusion').map do |excl_node|
227
+ Dependency.new(
228
+ parse_attr(excl_node, 'groupId'),
229
+ parse_attr(excl_node, 'artifactId')
230
+ )
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,149 @@
1
+ module PomPomPom
2
+ class Resolver
3
+ class JarNotFoundError < StandardError; end
4
+ class DependencyNotFoundError < StandardError; end
5
+
6
+ def initialize(repositories, options={})
7
+ @repositories, @logger, @downloader = repositories, (options[:logger] || NullLogger.new), (options[:downloader] || Downloader.new)
8
+ raise ArgumentError, 'No repositories given!' if repositories.nil? || repositories.empty?
9
+ end
10
+
11
+ def download!(target_dir, transitive, *dependencies)
12
+ create_target_directory(target_dir)
13
+ all_dependencies = if transitive then find_transitive_dependencies(*dependencies) else dependencies end
14
+ all_dependencies.each do |pom|
15
+ destination = File.join(target_dir, "#{pom.artifact_id}-#{pom.version}.jar")
16
+ JarDownloader.new(pom, destination, @repositories, @downloader, @logger).download!
17
+ end
18
+ end
19
+
20
+ def find_transitive_dependencies(*dependencies)
21
+ resolver = PomResolver.new(dependencies, @repositories, @downloader, @logger)
22
+ filter_newest(group_by_artifact(resolver.all_poms))
23
+ end
24
+
25
+ private
26
+
27
+ def filter_newest(pom_groups)
28
+ pom_groups.map do |id, poms|
29
+ if poms.size > 1
30
+ newest = poms.sort { |a, b| a.version <=> b.version }.reverse.first
31
+ @logger.warn(%(Warning: multiple versions of #{id} were required, using the newest required version (#{newest.version})))
32
+ newest
33
+ else
34
+ poms.first
35
+ end
36
+ end.flatten
37
+ end
38
+
39
+ def group_by_artifact(poms)
40
+ poms.inject({}) do |acc, pom|
41
+ id = "#{pom.group_id}:#{pom.artifact_id}"
42
+ acc[id] ||= []
43
+ acc[id] << pom
44
+ acc
45
+ end
46
+ end
47
+
48
+ def create_target_directory(target_dir)
49
+ return if File.directory?(target_dir)
50
+ raise 'Cannot create target directory because it already exists (but is not a directory)' if File.exists?(target_dir)
51
+ Dir.mkdir(target_dir)
52
+ end
53
+
54
+ def args_and_options(*args)
55
+ if Hash === args.last
56
+ options = args.pop
57
+ end
58
+ [args, options || {}]
59
+ end
60
+
61
+ class NullLogger
62
+ def debug(msg); end
63
+ def info(msg); end
64
+ def warn(msg); end
65
+ end
66
+
67
+ class PomResolver
68
+ def initialize(dependencies, repositories, downloader, logger)
69
+ @dependencies, @repositories, @downloader, @logger = dependencies, repositories, downloader, logger
70
+ end
71
+
72
+ def all_poms
73
+ @dependencies.map { |d| resolve_dependencies(d) }.flatten
74
+ end
75
+
76
+ private
77
+
78
+ def resolve_dependencies(dependency)
79
+ pom = nil
80
+ @repositories.detect do |repository|
81
+ if dependency.has_version?
82
+ d = dependency
83
+ else
84
+ d = find_latest(dependency, repository)
85
+ end
86
+ pom = get_pom(repository, d)
87
+ pom
88
+ end
89
+ raise DependencyNotFoundError, "Could not find POM for #{dependency} in any repository" unless pom
90
+ transitive_dependencies = pom.dependencies.reject do |d|
91
+ d.optional? || dependency.exclusions.any? { |dd| dd.artifact_id == d.artifact_id }
92
+ end
93
+ [pom] + transitive_dependencies.map { |d| resolve_dependencies(d) }
94
+ end
95
+
96
+ def find_latest(dependency, repository)
97
+ @logger.info(%(Finding latest version for #{dependency.group_id}:#{dependency.artifact_id}))
98
+ url = dependency.metadata_url(repository)
99
+ metadata = Metadata.new(@downloader.get(url))
100
+ metadata.parse!
101
+ dependency.clone(:version => metadata.latest_version)
102
+ rescue => e
103
+ @logger.warn(%(Could not donwload "#{url}": #{e.message}))
104
+ end
105
+
106
+ def get_pom(repository, dependency)
107
+ url = dependency.pom_url(repository)
108
+ @logger.debug(%(Loading POM from "#{url}"))
109
+ data = @downloader.get(url)
110
+ if data
111
+ pom = Pom.new(StringIO.new(data))
112
+ pom.parse!
113
+ if pom.has_parent?
114
+ parent = get_pom(repository, pom.parent)
115
+ pom = pom.merge(parent)
116
+ end
117
+ pom
118
+ else
119
+ nil
120
+ end
121
+ rescue => e
122
+ @logger.debug(%(Could not download "#{url}": #{e.message}))
123
+ nil
124
+ end
125
+ end
126
+
127
+ class JarDownloader
128
+ def initialize(pom, local_path, repositories, downloader, logger)
129
+ @pom, @local_path, @repositories, @downloader, @logger = pom, local_path, repositories, downloader, logger
130
+ end
131
+
132
+ def download!
133
+ data = nil
134
+ @repositories.detect do |repository|
135
+ url = @pom.jar_url(repository)
136
+ @logger.debug(%(Loading JAR from "#{url}"))
137
+ begin
138
+ data = @downloader.get(url)
139
+ rescue => e
140
+ @logger.debug(%(Could not download "#{url}": #{e.message}))
141
+ end
142
+ data
143
+ end
144
+ raise JarNotFoundError, "Could not download JAR for #{@pom.to_dependency} in any repository" unless data
145
+ File.open(@local_path, 'w') { |f| f.write(data) }
146
+ end
147
+ end
148
+ end
149
+ end