pompompom 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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