jinx 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +27 -0
  6. data/History.md +6 -0
  7. data/LEGAL +5 -0
  8. data/LICENSE +22 -0
  9. data/README.md +44 -0
  10. data/Rakefile +41 -0
  11. data/examples/family/README.md +10 -0
  12. data/examples/family/ext/build.xml +35 -0
  13. data/examples/family/ext/src/family/Address.java +68 -0
  14. data/examples/family/ext/src/family/Child.java +24 -0
  15. data/examples/family/ext/src/family/DomainObject.java +26 -0
  16. data/examples/family/ext/src/family/Household.java +36 -0
  17. data/examples/family/ext/src/family/Parent.java +48 -0
  18. data/examples/family/ext/src/family/Person.java +42 -0
  19. data/examples/family/lib/family.rb +15 -0
  20. data/examples/family/lib/family/address.rb +6 -0
  21. data/examples/family/lib/family/domain_object.rb +6 -0
  22. data/examples/family/lib/family/household.rb +6 -0
  23. data/examples/family/lib/family/parent.rb +16 -0
  24. data/examples/family/lib/family/person.rb +6 -0
  25. data/examples/model/README.md +25 -0
  26. data/examples/model/ext/build.xml +35 -0
  27. data/examples/model/ext/src/domain/Child.java +192 -0
  28. data/examples/model/ext/src/domain/Dependent.java +29 -0
  29. data/examples/model/ext/src/domain/DomainObject.java +26 -0
  30. data/examples/model/ext/src/domain/Independent.java +83 -0
  31. data/examples/model/ext/src/domain/Parent.java +129 -0
  32. data/examples/model/ext/src/domain/Person.java +14 -0
  33. data/examples/model/lib/model.rb +13 -0
  34. data/examples/model/lib/model/child.rb +13 -0
  35. data/examples/model/lib/model/domain_object.rb +6 -0
  36. data/examples/model/lib/model/independent.rb +11 -0
  37. data/examples/model/lib/model/parent.rb +17 -0
  38. data/jinx.gemspec +22 -0
  39. data/lib/jinx.rb +3 -0
  40. data/lib/jinx/active_support/README.txt +2 -0
  41. data/lib/jinx/active_support/core_ext/string.rb +7 -0
  42. data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
  43. data/lib/jinx/active_support/inflections.rb +55 -0
  44. data/lib/jinx/active_support/inflector.rb +398 -0
  45. data/lib/jinx/cli/application.rb +36 -0
  46. data/lib/jinx/cli/command.rb +214 -0
  47. data/lib/jinx/helpers/array.rb +108 -0
  48. data/lib/jinx/helpers/boolean.rb +42 -0
  49. data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
  50. data/lib/jinx/helpers/class.rb +149 -0
  51. data/lib/jinx/helpers/collection.rb +33 -0
  52. data/lib/jinx/helpers/collections.rb +11 -0
  53. data/lib/jinx/helpers/collector.rb +20 -0
  54. data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
  55. data/lib/jinx/helpers/enumerable.rb +242 -0
  56. data/lib/jinx/helpers/enumerate.rb +35 -0
  57. data/lib/jinx/helpers/error.rb +15 -0
  58. data/lib/jinx/helpers/file_separator.rb +65 -0
  59. data/lib/jinx/helpers/filter.rb +52 -0
  60. data/lib/jinx/helpers/flattener.rb +38 -0
  61. data/lib/jinx/helpers/hash.rb +12 -0
  62. data/lib/jinx/helpers/hashable.rb +502 -0
  63. data/lib/jinx/helpers/inflector.rb +36 -0
  64. data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
  65. data/lib/jinx/helpers/lazy_hash.rb +44 -0
  66. data/lib/jinx/helpers/log.rb +106 -0
  67. data/lib/jinx/helpers/math.rb +12 -0
  68. data/lib/jinx/helpers/merge.rb +60 -0
  69. data/lib/jinx/helpers/module.rb +18 -0
  70. data/lib/jinx/helpers/multi_enumerator.rb +31 -0
  71. data/lib/jinx/helpers/options.rb +92 -0
  72. data/lib/jinx/helpers/os.rb +19 -0
  73. data/lib/jinx/helpers/partial_order.rb +37 -0
  74. data/lib/jinx/helpers/pretty_print.rb +207 -0
  75. data/lib/jinx/helpers/set.rb +8 -0
  76. data/lib/jinx/helpers/stopwatch.rb +76 -0
  77. data/lib/jinx/helpers/transformer.rb +24 -0
  78. data/lib/jinx/helpers/transitive_closure.rb +55 -0
  79. data/lib/jinx/helpers/uniquifier.rb +50 -0
  80. data/lib/jinx/helpers/validation.rb +33 -0
  81. data/lib/jinx/helpers/visitor.rb +370 -0
  82. data/lib/jinx/import/class_path_modifier.rb +77 -0
  83. data/lib/jinx/import/java.rb +337 -0
  84. data/lib/jinx/importer.rb +240 -0
  85. data/lib/jinx/metadata.rb +155 -0
  86. data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
  87. data/lib/jinx/metadata/dependency.rb +244 -0
  88. data/lib/jinx/metadata/id_alias.rb +23 -0
  89. data/lib/jinx/metadata/introspector.rb +179 -0
  90. data/lib/jinx/metadata/inverse.rb +170 -0
  91. data/lib/jinx/metadata/java_property.rb +169 -0
  92. data/lib/jinx/metadata/propertied.rb +500 -0
  93. data/lib/jinx/metadata/property.rb +401 -0
  94. data/lib/jinx/metadata/property_characteristics.rb +114 -0
  95. data/lib/jinx/resource.rb +862 -0
  96. data/lib/jinx/resource/copy_visitor.rb +36 -0
  97. data/lib/jinx/resource/inversible.rb +90 -0
  98. data/lib/jinx/resource/match_visitor.rb +180 -0
  99. data/lib/jinx/resource/matcher.rb +20 -0
  100. data/lib/jinx/resource/merge_visitor.rb +73 -0
  101. data/lib/jinx/resource/mergeable.rb +185 -0
  102. data/lib/jinx/resource/reference_enumerator.rb +49 -0
  103. data/lib/jinx/resource/reference_path_visitor.rb +38 -0
  104. data/lib/jinx/resource/reference_visitor.rb +55 -0
  105. data/lib/jinx/resource/unique.rb +35 -0
  106. data/lib/jinx/version.rb +3 -0
  107. data/spec/defaults_spec.rb +30 -0
  108. data/spec/definitions/model/alias/child.rb +5 -0
  109. data/spec/definitions/model/base/child.rb +5 -0
  110. data/spec/definitions/model/base/domain_object.rb +5 -0
  111. data/spec/definitions/model/base/independent.rb +5 -0
  112. data/spec/definitions/model/defaults/child.rb +5 -0
  113. data/spec/definitions/model/dependency/child.rb +5 -0
  114. data/spec/definitions/model/dependency/parent.rb +6 -0
  115. data/spec/definitions/model/inverse/child.rb +5 -0
  116. data/spec/definitions/model/inverse/independent.rb +5 -0
  117. data/spec/definitions/model/inverse/parent.rb +5 -0
  118. data/spec/definitions/model/mandatory/child.rb +6 -0
  119. data/spec/dependency_spec.rb +47 -0
  120. data/spec/family_spec.rb +64 -0
  121. data/spec/inverse_spec.rb +53 -0
  122. data/spec/mandatory_spec.rb +43 -0
  123. data/spec/metadata_spec.rb +68 -0
  124. data/spec/resource_spec.rb +30 -0
  125. data/spec/spec_helper.rb +3 -0
  126. data/spec/support/model.rb +19 -0
  127. data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
  128. data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
  129. data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
  130. data/test/fixtures/mixed/ext/build.xml +35 -0
  131. data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
  132. data/test/helper.rb +7 -0
  133. data/test/lib/jinx/command_test.rb +41 -0
  134. data/test/lib/jinx/helpers/boolean_test.rb +27 -0
  135. data/test/lib/jinx/helpers/class_test.rb +60 -0
  136. data/test/lib/jinx/helpers/collections_test.rb +402 -0
  137. data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
  138. data/test/lib/jinx/helpers/inflector_test.rb +11 -0
  139. data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
  140. data/test/lib/jinx/helpers/module_test.rb +24 -0
  141. data/test/lib/jinx/helpers/options_test.rb +66 -0
  142. data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
  143. data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
  144. data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
  145. data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
  146. data/test/lib/jinx/helpers/visitor_test.rb +288 -0
  147. data/test/lib/jinx/import/java_test.rb +78 -0
  148. data/test/lib/jinx/import/mixed_case_test.rb +16 -0
  149. metadata +272 -0
@@ -0,0 +1,337 @@
1
+ require 'java'
2
+ require 'ftools'
3
+ require 'date'
4
+ require 'jinx/helpers/class'
5
+ require 'jinx/helpers/inflector'
6
+ require 'jinx/helpers/collections'
7
+ require 'jinx/import/class_path_modifier'
8
+
9
+ module Java
10
+ # the class path modifier for expand_to_class_path
11
+ @cp_mod = Jinx::ClassPathModifier.new
12
+
13
+ # @see ClassPathModifier#expand_to_class_path
14
+ def self.expand_to_class_path(path)
15
+ @cp_mod.expand_to_class_path(path)
16
+ end
17
+
18
+ # @param [String] name the properties file name
19
+ # @return [Hash, nil] the properties content, or nil if the file is not on the classpath
20
+ def self.load_properties(name)
21
+ url = JRuby.runtime.jruby_class_loader.findResource(name) || return
22
+ props = JavaUtil::Properties.new
23
+ props.load(url.openStream)
24
+ props
25
+ end
26
+
27
+ module JavaUtil
28
+ # Aliases Java Collection methods with the standard Ruby Set counterpart, e.g. +delete+ for +remove+.
29
+ module Collection
30
+ def to_a
31
+ inject(Array.new) { |array, item| array << item }
32
+ end
33
+
34
+ # Removes the given item from this collection.
35
+ def delete(item)
36
+ # can't alias delete to remove, since a Java interface doesn't implement any methods
37
+ remove(item)
38
+ end
39
+
40
+ # Removes the items from this collection for which the block given to this method returns a non-nil, non-false value.
41
+ def delete_if
42
+ removeAll(select { |item| yield item })
43
+ self
44
+ end
45
+ end
46
+
47
+ # Aliases Java List methods with the standard Ruby Array counterpart, e.g. +merge+ for +addAll+.
48
+ module List
49
+ # Returns whether this List has the same content as the other Java List or Ruby Array.
50
+ def ==(other)
51
+ Array === other ? to_a == other : equals(other)
52
+ end
53
+
54
+ # Removes the given item from this collection.
55
+ def delete(item)
56
+ remove(item)
57
+ end
58
+ end
59
+
60
+ module Map
61
+ # Returns whether this Set has the same content as the other Java Map or Ruby Hash.
62
+ def ==(other)
63
+ ::Hash === other ? (size == other.size and other.all? { |key, value| get(key) == value }) : equals(other)
64
+ end
65
+
66
+ # Merges the other Java Map or Ruby Hash into this Map. Returns this modified Map.
67
+ #
68
+ # If a block is given to this method, then the block determines the mapped value
69
+ # as specified in the Ruby Hash merge method documentation.
70
+ def merge(other)
71
+ other.each do |key, value|
72
+ value = yield(key, get(key), value) if block_given? and containsKey(key)
73
+ put(key, value)
74
+ end
75
+ self
76
+ end
77
+
78
+ alias :merge! :merge
79
+ end
80
+
81
+ module Set
82
+ # Returns whether this Set has the same content as the other Java Set or Ruby Set.
83
+ def ==(other)
84
+ ::Set === other ? (size == other.size and all? { |item| other.include?(item) }) : equals(other)
85
+ end
86
+
87
+ # Merges the other Enumerable into this Set. Returns this modified Set.
88
+ #
89
+ # This method conforms to the Ruby Set merge contract rather than the Ruby List and Hash
90
+ # merge contract. Ruby Set merge modifies the Set in-place, whereas Ruby List and Hash
91
+ # merge return a new collection.
92
+ def merge(other)
93
+ return self if other.nil?
94
+ Jinx.fail(ArgumentError, "Merge argument must be enumerable: #{other}") unless Enumerable === other
95
+ other.each { |item| self << item }
96
+ self
97
+ end
98
+
99
+ alias :merge! :merge
100
+ end
101
+
102
+ class HashSet
103
+ alias :base__clear :clear
104
+ private :base__clear
105
+ def clear
106
+ base__clear
107
+ self
108
+ end
109
+ end
110
+
111
+ class TreeSet
112
+ alias :base__first :first
113
+ private :base__first
114
+ # Fixes the jRuby {TreeSet#first} to return nil on an empty set rather than raise a Java exception.
115
+ def first
116
+ empty? ? nil : base__first
117
+ end
118
+ end
119
+
120
+ class ArrayList
121
+ alias :base__clear :clear
122
+ private :base__clear
123
+ def clear
124
+ base__clear
125
+ self
126
+ end
127
+ end
128
+
129
+ class Date
130
+ # The millisecond-to-day conversion factor.
131
+ MILLIS_PER_DAY = (60 * 60 * 1000) * 24
132
+
133
+ # Converts this Java Date to a Ruby DateTime.
134
+ #
135
+ # @return [DateTime] the Ruby date
136
+ def to_ruby_date
137
+ calendar = java.util.Calendar.instance
138
+ calendar.setTime(self)
139
+ secs = calendar.timeInMillis.to_f / 1000
140
+ # millis since epoch
141
+ time = Time.at(secs)
142
+ # convert UTC timezone millisecond offset to Rational fraction of a day
143
+ offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
144
+ if offset_millis.zero? then
145
+ offset = 0
146
+ else
147
+ offset_days = offset_millis / MILLIS_PER_DAY
148
+ offset_fraction = 1 / offset_days
149
+ offset = Rational(1, offset_fraction)
150
+ end
151
+ # convert to DateTime
152
+ DateTime.civil(time.year, time.mon, time.day, time.hour, time.min, time.sec, offset)
153
+ end
154
+
155
+ # Converts a Ruby Date or DateTime to a Java Date.
156
+ #
157
+ # @param [::Date, DateTime] date the Ruby date
158
+ # @return [Date] the Java date
159
+ def self.from_ruby_date(date)
160
+ return if date.nil?
161
+ # DateTime has time attributes, Date doesn't
162
+ if DateTime === date then
163
+ hour, min, sec = date.hour, date.min, date.sec
164
+ else
165
+ hour = min = sec = 0
166
+ end
167
+ # the Ruby time
168
+ rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, nil, nil)
169
+ # millis since epoch
170
+ millis = (rtime.to_f * 1000).truncate
171
+ # the Java date factory
172
+ calendar = java.util.Calendar.instance
173
+ calendar.setTimeInMillis(millis)
174
+ jtime = calendar.getTime
175
+ # the daylight time flag
176
+ isdt = calendar.timeZone.inDaylightTime(jtime)
177
+ return jtime unless isdt
178
+ # adjust the Ruby time for DST
179
+ rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, isdt, nil)
180
+ millis = (rtime.to_f * 1000).truncate
181
+ calendar.setTimeInMillis(millis)
182
+ calendar.getTime
183
+ end
184
+ end
185
+ end
186
+
187
+ def self.now
188
+ JavaUtil::Date.from_ruby_date(DateTime.now)
189
+ end
190
+
191
+ # @param [Class, String] the JRuby class or the full Java class name
192
+ # @return (String, String] the package and base for the given name
193
+ def self.split_class_name(name_or_class)
194
+ name = Class === name_or_class ? name_or_class.java_class.name : name_or_class
195
+ match = NAME_SPLITTER_REGEX.match(name)
196
+ match ? match.captures : [nil, name]
197
+ end
198
+
199
+ private
200
+
201
+ NAME_SPLITTER_REGEX = /^([\w.]+)\.(\w+)$/
202
+ end
203
+
204
+ class Class
205
+ # Returns whether this is a Java wrapper class.
206
+ def java_class?
207
+ method_defined?(:java_class)
208
+ end
209
+
210
+ # Returns the Ruby class for the given class, as follows:
211
+ # * If the given class is already a Ruby class, then return the class.
212
+ # * If the class argument is a Java class or a Java class name, then
213
+ # the Ruby class is the JRuby wrapper for the Java class.
214
+ #
215
+ # @param [Class, String] class_or_name the class or class name
216
+ # @return [Class] the corresponding Ruby class
217
+ def self.to_ruby(class_or_name)
218
+ case class_or_name
219
+ when Class then class_or_name
220
+ when String then eval to_ruby_name(class_or_name)
221
+ else to_ruby(class_or_name.name)
222
+ end
223
+ end
224
+
225
+ # @return [Boolean] whether this is a wrapper for an abstract Java class
226
+ def abstract?
227
+ java_class? and Java::JavaLangReflect::Modifier.isAbstract(java_class.modifiers)
228
+ end
229
+
230
+ # Returns whether the given PropertyDescriptor pd corresponds to a transient field in this class, or nil if there is no such field.
231
+ def transient?(pd)
232
+ begin
233
+ field = java_class.declared_field(pd.name)
234
+ rescue Exception
235
+ # should occur only if a property is not a field; not an error
236
+ return
237
+ end
238
+ Java::JavaLangReflect::Modifier.isTransient(field.modifiers) if field
239
+ end
240
+
241
+ # Returns this class's readable and writable Java PropertyDescriptors, or an empty Array if none.
242
+ # If the hierarchy flag is set to +false+, then only this class's properties
243
+ # will be introspected.
244
+ def property_descriptors(hierarchy=true)
245
+ return Array::EMPTY_ARRAY unless java_class?
246
+ info = hierarchy ? Java::JavaBeans::Introspector.getBeanInfo(java_class) : Java::JavaBeans::Introspector.getBeanInfo(java_class, java_class.superclass)
247
+ info.propertyDescriptors.select { |pd| pd.write_method and property_read_method(pd) }
248
+ end
249
+
250
+ # Redefines the reserved method corresponding to the given Java property descriptor
251
+ # back to the Object implementation, if necessary.
252
+ # If both this class and Object define a method with the given property name,
253
+ # then a new method is defined with the same body as the previous method.
254
+ # Returns the new method symbol, or nil if the property name is not an occluded
255
+ # Object instance method.
256
+ #
257
+ # This method undoes the JRuby clobbering of Object methods by Java property method
258
+ # wrappers. The method is renamed as follows:
259
+ # * +id+ is changed to :identifier
260
+ # * +type+ is prefixed by the underscore subject class name, e.g. +Specimen.type => :specimen_type+,
261
+ # If the property name is +type+ and the subject class name ends in 'Type', then the property
262
+ # symbol is the underscore subject class name, e.g. +HistologicType.type => :histologic_type+.
263
+ #
264
+ # Raises ArgumentError if symbol is not an Object method.
265
+ def unocclude_reserved_method(pd)
266
+ oldname = pd.name.underscore
267
+ return unless OBJ_INST_MTHDS.include?(oldname)
268
+ oldsym = oldname.to_sym
269
+ undeprecated = case oldsym
270
+ when :id then :object_id
271
+ when :type then :class
272
+ else oldsym
273
+ end
274
+ rsvd_mth = Object.instance_method(undeprecated)
275
+ base = self.qp.underscore
276
+ newname = if oldname == 'id' then
277
+ 'identifier'
278
+ elsif base[-oldname.length..-1] == oldname then
279
+ base
280
+ else
281
+ "#{base}_#{oldname}"
282
+ end
283
+ newsym = newname.to_sym
284
+ rdr = property_read_method(pd).name.to_sym
285
+ alias_method(newsym, rdr)
286
+ # alias the writers
287
+ wtr = pd.write_method.name.to_sym
288
+ alias_method("#{newsym}=".to_sym, wtr)
289
+ # alias a camel-case Java-style method if necessary
290
+ altname = newname.camelize
291
+ unless altname == newname then
292
+ alias_method(altname.to_sym, oldsym)
293
+ alias_method("#{altname}=".to_sym, wtr)
294
+ end
295
+ # restore the old method to Object
296
+ define_method(oldsym) { |*args| rsvd_mth.bind(self).call(*args) }
297
+ newsym
298
+ end
299
+
300
+ # @quirk Java +java.lang.Boolean+ +is+_name_ is not introspected as a read method,
301
+ # since the type must be primitive, i.e. +boolean is+_name_. The work-around is
302
+ # to explicitly look for the +is+_name_ method in the case of a +java.lang.Boolean+
303
+ # property rather than rely on the Java introspector.
304
+ #
305
+ # @return [Symbol] the property descriptor pd introspected or discovered Java read Method
306
+ def property_read_method(pd)
307
+ return pd.read_method if pd.read_method
308
+ return unless pd.get_property_type == Java::JavaLang::Boolean.java_class
309
+ rdr = java_class.java_method("is#{pd.name.capitalize_first}") rescue nil
310
+ logger.debug { "Discovered #{qp} #{pd.name} property non-introspected reader method #{rdr.name}." } if rdr
311
+ rdr
312
+ end
313
+
314
+ private
315
+
316
+ OBJ_INST_MTHDS = Object.instance_methods
317
+
318
+ # @param [String] jname the fully-qualified Java class or interface name
319
+ # @return [String] the JRuby class or module name
320
+ # @example
321
+ # Java.to_ruby_class_name('com.test.Sample') #=> Java::ComTest::Sample
322
+ def self.to_ruby_name(jname)
323
+ path = jname.split('.')
324
+ return "Java::#{jname}" if path.size == 1
325
+ cname = path[-1]
326
+ pkg = path[0...-1].map { |s| s.capitalize_first }.join
327
+ "Java::#{pkg}::#{cname}"
328
+ end
329
+ end
330
+
331
+ class Array
332
+ alias :equal__base :==
333
+ # Overrides the standard == to compare a Java List with a Ruby Array.
334
+ def ==(other)
335
+ Java::JavaUtil::List === other ? other == self : equal__base(other)
336
+ end
337
+ end
@@ -0,0 +1,240 @@
1
+ require 'jinx/helpers/transitive_closure'
2
+ require 'jinx/metadata'
3
+
4
+ module Jinx
5
+ # Importer extends a module with Java class import support. Importer is an aspect
6
+ # of a {Metadata} module. Including +Metadata+ in an application domain module
7
+ # extends that module with Importer capability.
8
+ #
9
+ # The Importer module imports a Java class or interface on demand by referencing
10
+ # the class name in the context of the module.
11
+ # The imported class {Metadata} is introspected.
12
+ #
13
+ # Import on demand is induced by a reference to the class.
14
+ # The +family+ example illustrates a domain package extended
15
+ # with metadata capability. The first non-definition reference to +Family::Parent+
16
+ # imports the Java class +family.Parent+ into the JRuby class wrapper
17
+ # +Family+ and introspects the Java property meta-data.
18
+ module Importer
19
+ # Declares that the given {Resource} classes will be dynamically modified.
20
+ # This method introspects the classes, if necessary.
21
+ #
22
+ # @param [<Class>] classes the classes to augment
23
+ def shims(*classes)
24
+ # Nothing to do, since all this method does is ensure that the arguments are
25
+ # introspected when they are referenced.
26
+ end
27
+
28
+ # Imports a Java class constant on demand. If the class does not already
29
+ # include this module's mixin, then the mixin is included in the class.
30
+ #
31
+ # @param [Symbol, String] sym the missing constant
32
+ # @return [Class] the imported class
33
+ # @raise [NameError] if the symbol is not an importable Java class
34
+ def const_missing(sym)
35
+ # Load the class definitions in the source directory, if necessary.
36
+ # If a load is performed as a result of referencing the given symbol,
37
+ # then dereference the class constant again after the load, since the class
38
+ # might have been loaded or referenced during the load.
39
+ unless defined? @introspected then
40
+ configure_importer
41
+ load_definitions
42
+ return const_get(sym)
43
+ end
44
+
45
+ # Append the symbol to the package to make the Java class name.
46
+ logger.debug { "Detecting whether #{sym} is a #{self} Java class..." }
47
+ klass = @packages.detect_value do |pkg|
48
+ begin
49
+ java_import "#{pkg}.#{sym}"
50
+ rescue NameError
51
+ nil
52
+ end
53
+ end
54
+ if klass.nil? then
55
+ # Not a Java class; print a log message and pass along the error.
56
+ logger.debug { "#{sym} is not recognized as a #{self} Java class." }
57
+ super
58
+ end
59
+
60
+ # Introspect the Java class meta-data, if necessary.
61
+ unless @introspected.include?(klass) then
62
+ add_metadata(klass)
63
+ # Print the class meta-data.
64
+ logger.info(klass.pp_s)
65
+ end
66
+
67
+ klass
68
+ end
69
+
70
+ # @param [String] the module name to resolve in the context of this module
71
+ # @return [Module] the corresponding module
72
+ def module_for_name(name)
73
+ begin
74
+ # Incrementally resolve the module.
75
+ name.split('::').inject(self) { |ctxt, mod| ctxt.const_get(mod) }
76
+ rescue NameError
77
+ # If the application domain module set the parent module i.v.. then continue
78
+ # the look-up in that parent importer.
79
+ raise unless @parent_importer
80
+ mod = @parent_importer.module_for_name(name)
81
+ if mod then
82
+ logger.debug { "Module #{name} found in #{qp} parent module #{@parent_importer}." }
83
+ end
84
+ mod
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ # Initializes this importer on demand. This method is called the first time a class
91
+ # is referenced.
92
+ def configure_importer
93
+ # The default package conforms to the JRuby convention for mapping a package name
94
+ # to a module name.
95
+ @packages ||= [name.split('::').map { |n| n.downcase }.join('.')]
96
+ @packages.each do |pkg|
97
+ begin
98
+ eval "java_package Java::#{pkg}"
99
+ rescue Exception => e
100
+ Jinx.fail(ArgumentError, "#{self} Java package #{pkg} not found - #{$!}")
101
+ end
102
+ end
103
+ # The introspected classes.
104
+ @introspected = Set.new
105
+ end
106
+
107
+ # Sets the package names. The default package conforms to the JRuby convention for
108
+ # mapping a package name to a module name, e.g. the +MyApp::Domain+ default package
109
+ # is +myapp.domain+. Clients set the package if it differs from the default.
110
+ #
111
+ # @param [<String>] name the package names
112
+ def packages(*names)
113
+ @packages = names
114
+ end
115
+
116
+ # Alias for the common case of a single package.
117
+ alias :package :packages
118
+
119
+ # @param [<String>] directories the Ruby class definitions directories
120
+ def definitions(*directories)
121
+ @definitions = directories
122
+ end
123
+
124
+ def load_definitions
125
+ return if @definitions.nil_or_empty?
126
+ # Load the class definitions in the source directories.
127
+ @definitions.each { |dir| load_dir(File.expand_path(dir)) }
128
+ # Print each introspected class's content.
129
+ @introspected.sort { |k1, k2| k1.name <=> k2.name }.each { |klass| logger.info(klass.pp_s) }
130
+ true
131
+ end
132
+
133
+ # Loads the Ruby source files in the given directory.
134
+ #
135
+ # @param [String] dir the source directory
136
+ def load_dir(dir)
137
+ logger.debug { "Loading the class definitions in #{dir}..." }
138
+ # Import the classes.
139
+ srcs = sources(dir)
140
+ # Introspect and load the classes in reverse class order, i.e. superclass before subclass.
141
+ klasses = srcs.keys.transitive_closure { |k| [k.superclass] }.select { |k| srcs[k] }.reverse
142
+ # Introspect the classes if necessary.
143
+ klasses.each { |klass| add_metadata(klass) unless @introspected.include?(klass) }
144
+ # Load the classes.
145
+ klasses.each do |klass|
146
+ file = srcs[klass]
147
+ logger.debug { "Loading #{klass.qp} definition #{file}..." }
148
+ require file
149
+ logger.debug { "Loaded #{klass.qp} definition #{file}." }
150
+ end
151
+ logger.debug { "Loaded the class definitions in #{dir}." }
152
+ end
153
+
154
+ # @param [String] dir the source directory
155
+ # @return [{Class => String}] the source class => file hash
156
+ def sources(dir)
157
+ # the domain class definitions
158
+ files = Dir.glob(File.join(dir, "*.rb"))
159
+ # Infer each class symbol from the file base name.
160
+ # Ignore files which do not resolve to a class.
161
+ files.to_compact_hash do |file|
162
+ name = File.basename(file, ".rb").camelize
163
+ resolve_class(name)
164
+ end.invert
165
+ end
166
+
167
+ # @param [String] name the demodulized class name
168
+ # @param [String] package the Java package, or nil for all packages
169
+ # @return [Class, nil] the {Resource} class imported into this module,
170
+ # or nil if the class cannot be resolved
171
+ def resolve_class(name, package=nil)
172
+ if const_defined?(name) then
173
+ return const_get(name)
174
+ end
175
+ if package.nil? then
176
+ return @packages.detect_value { |pkg| resolve_class(name, pkg) }
177
+ end
178
+ # Append the class name to the package to make the Java class name.
179
+ full_name = "#{package}.#{name}"
180
+ # If the class is already imported, then java_import returns nil. In that case,
181
+ # evaluate the Java class.
182
+ begin
183
+ java_import(full_name)
184
+ rescue
185
+ module_eval("Java::#{full_name}") rescue nil
186
+ end
187
+ end
188
+
189
+ # Introspects the given class meta-data.
190
+ #
191
+ # @param [Class] klass the Java class or interface to introspect
192
+ def add_metadata(klass)
193
+ logger.debug("Adding #{self}::#{klass.qp} metadata...")
194
+ # Mark the class as introspected. Do this first to preclude a recursive loop back
195
+ # into this method when the references are introspected below.
196
+ @introspected << klass
197
+ # Add the superclass meta-data if necessary.
198
+ if Class === klass then
199
+ sc = klass.superclass
200
+ unless @introspected.include?(sc) or sc == Java::java.lang.Object then
201
+ add_metadata(sc)
202
+ end
203
+ end
204
+ # Include this resource module into the class.
205
+ unless klass < self then
206
+ m = self
207
+ klass.class_eval { include m }
208
+ end
209
+ # Add introspection capability to the class.
210
+ md_mod = @metadata_module || Metadata
211
+
212
+ logger.debug { "Extending #{self}::#{klass.qp} with #{md_mod.name}..." }
213
+ klass.extend(md_mod)
214
+
215
+ # Introspect the Java properties.
216
+ introspect(klass)
217
+ klass.add_attribute_value_initializer if Class === klass
218
+ # Set the class domain module.
219
+ klass.domain_module = self
220
+ # Add referenced domain class metadata as necessary.
221
+ mod = klass.parent_module
222
+ klass.each_property do |prop|
223
+ ref = prop.type
224
+ if ref.nil? then Jinx.fail(MetadataError, "#{self} #{prop} domain type is unknown.") end
225
+ unless @introspected.include?(ref) or ref.parent_module != mod then
226
+ logger.debug { "Introspecting #{qp} #{prop} reference #{ref.qp}..." }
227
+ add_metadata(ref)
228
+ end
229
+ end
230
+ logger.debug("#{self}::#{klass.qp} metadata added.")
231
+ end
232
+
233
+ # Introspects the given class.
234
+ #
235
+ # @param [Class] the domain class to introspect
236
+ def introspect(klass)
237
+ klass.introspect
238
+ end
239
+ end
240
+ end