jinx 2.1.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.
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