kodekopelli 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +32 -0
  3. data/README +78 -0
  4. data/Rakefile +87 -0
  5. data/bin/kodekopelli +5 -0
  6. data/examples/faqomatic/config.xml +40 -0
  7. data/examples/faqomatic/content.rhtml +376 -0
  8. data/examples/faqomatic/generated/README.txt +1 -0
  9. data/examples/faqomatic/includes/shampoo_answer.txt +1 -0
  10. data/examples/faqomatic/templates/faqs.txt +22 -0
  11. data/examples/faqomatic/templates/great_big.txt +3 -0
  12. data/examples/faqomatic/templates/html_wrapper.txt +9 -0
  13. data/examples/thankyounotes/config.xml +18 -0
  14. data/examples/thankyounotes/content.rhtml +528 -0
  15. data/examples/thankyounotes/generated/README.txt +1 -0
  16. data/examples/thankyounotes/includes/poem.txt +3 -0
  17. data/examples/thankyounotes/templates/thanks.txt +14 -0
  18. data/examples/treemenu/config.xml +110 -0
  19. data/examples/treemenu/content.rhtml +324 -0
  20. data/examples/treemenu/generated/README.txt +1 -0
  21. data/examples/treemenu/templates/authorized.txt +1 -0
  22. data/examples/treemenu/templates/menu_category.txt +4 -0
  23. data/examples/treemenu/templates/menu_item.txt +3 -0
  24. data/examples/treemenu/templates/simple_menu.txt +3 -0
  25. data/lib/kodekopelli.rb +56 -0
  26. data/lib/kodekopelli/expandable_properties.rb +63 -0
  27. data/lib/kodekopelli/file_generator.rb +726 -0
  28. data/lib/kodekopelli/frozen_key_hash.rb +67 -0
  29. data/lib/kodekopelli/minimal_logger.rb +50 -0
  30. data/lib/kodekopelli/properties_file.rb +41 -0
  31. data/lib/kodekopelli/util.rb +30 -0
  32. data/rakefile +87 -0
  33. data/test/abstract_unit.rb +4 -0
  34. data/test/fixtures/definitions/invalid/empty +0 -0
  35. data/test/fixtures/definitions/invalid/gibberish.txt +6 -0
  36. data/test/fixtures/definitions/kodekopelli.xsd +95 -0
  37. data/test/fixtures/definitions/valid/empty_files.xml +49 -0
  38. data/test/fixtures/definitions/valid/kodekopelli_rocks_files.xml +359 -0
  39. data/test/fixtures/definitions/valid/simplest.xml +8 -0
  40. data/test/fixtures/includes/bang.txt +1 -0
  41. data/test/fixtures/includes/c.txt +1 -0
  42. data/test/fixtures/includes/d.txt +1 -0
  43. data/test/fixtures/includes/e.txt +1 -0
  44. data/test/fixtures/includes/empty.txt +0 -0
  45. data/test/fixtures/includes/i.txt +1 -0
  46. data/test/fixtures/includes/k.txt +1 -0
  47. data/test/fixtures/includes/k_upper.txt +1 -0
  48. data/test/fixtures/includes/kodekopelli_rocks.txt +1 -0
  49. data/test/fixtures/includes/l.txt +1 -0
  50. data/test/fixtures/includes/o.txt +1 -0
  51. data/test/fixtures/includes/p.txt +1 -0
  52. data/test/fixtures/includes/r.txt +1 -0
  53. data/test/fixtures/includes/s.txt +1 -0
  54. data/test/fixtures/includes/space.txt +1 -0
  55. data/test/fixtures/properties/comments.properties +17 -0
  56. data/test/fixtures/properties/empty.properties +0 -0
  57. data/test/fixtures/properties/expandable.properties +2 -0
  58. data/test/fixtures/properties/none_valid.properties +1 -0
  59. data/test/fixtures/properties/simple.properties +5 -0
  60. data/test/fixtures/templates/anything_att.txt +1 -0
  61. data/test/fixtures/templates/anything_att_prop.txt +1 -0
  62. data/test/fixtures/templates/anything_prop.txt +1 -0
  63. data/test/fixtures/templates/anything_prop_att.txt +1 -0
  64. data/test/fixtures/templates/blank.txt +0 -0
  65. data/test/fixtures/templates/child_output.txt +1 -0
  66. data/test/fixtures/templates/kodekopelli_blanks +1 -0
  67. data/test/tc_expandable_properties.rb +106 -0
  68. data/test/tc_file_generator.rb +20 -0
  69. data/test/tc_frozen_key_hash.rb +34 -0
  70. data/test/tc_properties_file.rb +52 -0
  71. data/test/tc_util.rb +55 -0
  72. data/test/ts_all_tests.rb +9 -0
  73. data/test/ts_functional.rb +86 -0
  74. metadata +133 -0
@@ -0,0 +1 @@
1
+ This folder contains all content generated by Kodekopelli processing.
@@ -0,0 +1 @@
1
+ <authz:authorize ifAnyGranted="<%= att('roles') %>"><%= child_output %></authz:authorize>
@@ -0,0 +1,4 @@
1
+ <li><% unless att('url',nil).nil? %><a href="<%= att('url') %>"
2
+ target="<%= att('target') %>"
3
+ ><% end %><%= h att('text') %><% unless att('url',nil).nil? %></a><% end %><br />
4
+ <%= child_output %></li>
@@ -0,0 +1,3 @@
1
+ <li><% unless att('url',nil).nil? %><a href="<%= att('url') %>"
2
+ target="<%= att('target') %>"
3
+ ><% end %><%= h att('text') %><% unless att('url',nil).nil? %></a><% end %></li>
@@ -0,0 +1,3 @@
1
+ <ul><% child_iterator { |current_child, text| %>
2
+ <%= text %><% } %>
3
+ </ul>
@@ -0,0 +1,56 @@
1
+ begin
2
+ require 'erb'
3
+ require 'rexml/document'
4
+
5
+ rescue LoadError
6
+ # require 'rubygems'
7
+ # require_gem 'somethin', '>=1.1.1'
8
+ end
9
+
10
+ require File.dirname(__FILE__) + '/kodekopelli/expandable_properties'
11
+ require File.dirname(__FILE__) + '/kodekopelli/file_generator'
12
+ require File.dirname(__FILE__) + '/kodekopelli/frozen_key_hash'
13
+ require File.dirname(__FILE__) + '/kodekopelli/minimal_logger'
14
+ require File.dirname(__FILE__) + '/kodekopelli/properties_file'
15
+ require File.dirname(__FILE__) + '/kodekopelli/util'
16
+
17
+ module Kodekopelli
18
+ def Kodekopelli.logo # :nodoc:
19
+ buf = "\n"
20
+ buf += " K \n"
21
+ buf += " ODE KO \n"
22
+ buf += " PEL LI K \n"
23
+ buf += " ODEKOP EL LI KO \n"
24
+ buf += " DEK O PE LL \n"
25
+ buf += " IK OD EK OP \n"
26
+ buf += " ELLIKODE KO PELL IK \n"
27
+ buf += " O DEK OPELLIK ODE \n"
28
+ buf += " KOPELLIKODEK \n"
29
+ buf += " OPELLIKODEK \n"
30
+ buf += " OPELLIKODEKOPELLIKOD \n"
31
+ buf += " EKOPELLIKODEKOPELLIKOD \n"
32
+ buf += " EKOPELLIKODE KOPELLIKOD \n"
33
+ buf += " EKOPELLIKODEK OPELLI \n"
34
+ buf += " KODEKOPELLIKOD EK \n"
35
+ buf += " OPELLIKODEKOPELL IKO \n"
36
+ buf += " DEKOPELLIKODEKOPE LLI \n"
37
+ buf += " KODEPELLIKODE KOPELLIKODE \n"
38
+ buf += " KOPELLIKODEKOPEL LI \n"
39
+ buf += " KODEKOPE LLIKODEKOPELLIK \n"
40
+ buf += " ODEKOPELL IK \n"
41
+ buf += " ODEKOPELL IK \n"
42
+ buf += " ODEKOPELLI KOD \n"
43
+ buf += " EKOPELLIKODE KO \n"
44
+ buf += " PELLIKODEKOPELLIK OD \n"
45
+ buf += " EKOPELLIKODEKOPEL \n"
46
+ buf += " LIKODEKOPEL LI \n"
47
+ buf += " KODEKOP ELLIKOD \n"
48
+ buf += " EKOPE LLIKO \n"
49
+ buf += " DEK \n"
50
+ buf += " OPELL TM \n"
51
+ buf += " IKODEKOP \n"
52
+ buf += " ELLIKOD \n"
53
+ buf += "\n"
54
+ buf
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ module Kodekopelli # :nodoc:
2
+ # This module encapsulates functionality related to
3
+ # manipulating expandable properties. In this context,
4
+ # <em>expandable properties</em> refer to those properties
5
+ # that may contain as substrings other properties
6
+ # to be substituted from an arbitrary property
7
+ # store, like a _Hash_.
8
+ #
9
+ # === Example Properties
10
+ # property.not.expandable=world
11
+ #
12
+ # property.expandable=Hello, ${property.not.expandable}! # => Hello, world!
13
+ #
14
+ # property.nested.expandables=Not another ${property.expandable}.
15
+ # # => Not another Hello, world!
16
+ #
17
+ # Please note that the order of the properties above is very important.
18
+ # For example, if the property keyed by <em>property.expandable</em>
19
+ # had been listed before property <em>property.not.expandable</em>,
20
+ # its value would have remained
21
+ # <code>Hello, ${property.not.expandable}!</code> with no
22
+ # <em>expansions</em> being performed.
23
+ module ExpandableProperties #
24
+
25
+ # Given a property value that may or may not contain expansion directives of the form ${property key}
26
+ # and a hash containing pre-existing properties, this function returns a string with property
27
+ # values substituted for the expansion directives.
28
+ #
29
+ # :call-seq:
30
+ # ExpandableProperties.replace_expandables(string, hash) -> string
31
+ #
32
+ def ExpandableProperties.replace_expandables(property_value, property_hash)
33
+ if(property_value.nil? || property_hash.nil?)
34
+ raise "Both a property value and property hash must be provided."
35
+ end
36
+
37
+ modified_value = String.new(property_value)
38
+
39
+ expandables_in_property_value(modified_value).each {|current_expandable|
40
+ expand_if_present!(current_expandable, modified_value, property_hash[current_expandable])
41
+ }
42
+
43
+ modified_value
44
+ end
45
+
46
+ private
47
+
48
+ # Given a string, returns an array of those keys that should
49
+ # be used for expansion,
50
+ def ExpandableProperties.expandables_in_property_value(anystring)
51
+ anystring.scan(/\$\{[^\}]*\}/).each {|current| current.chop!.slice!("${")}
52
+ end
53
+
54
+ # Given the name of an expandable name, a property value containing the expandable, and a replacement value,
55
+ # modifies the property value by substituting the replacement valuable for the expandable.
56
+ def ExpandableProperties.expand_if_present!(expandable_name, property_value, replacement_value)
57
+ unless replacement_value.nil? || replacement_value.empty?
58
+ property_value.gsub!("${#{expandable_name}}", replacement_value)
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,726 @@
1
+ module Kodekopelli # :nodoc:
2
+ # The _FileGenerator_ class coordinates the necessary
3
+ # actions to generate output files from one or more definitions
4
+ # files.
5
+ class FileGenerator #
6
+ require 'optparse'
7
+ require 'webrick'
8
+ include WEBrick
9
+ require 'erb'
10
+ include ERB::Util
11
+
12
+ attr_accessor :logger
13
+
14
+ # Returns a new _FileGenerator_.
15
+ #
16
+ # :call-seq:
17
+ # FileGenerator.new( properties_hash=nil ) -> file generator
18
+ #
19
+ def initialize(properties_hash = nil)
20
+ # Stores properties for property and attribute value expansions
21
+ # and system configuration
22
+ @property_hash = Kodekopelli::FrozenKeyHash.new
23
+
24
+ unless(properties_hash.nil?)
25
+ properties_hash.each_pair { |key,value|
26
+ @property_hash[key] = value
27
+ }
28
+ end
29
+
30
+ # Tracks attributes defined in the definitions
31
+ # file(s); also provides for nested contexts.
32
+ @context_stack = []
33
+
34
+ # Allows for node-specific context creation
35
+ @node_stack = []
36
+
37
+ # Context containing those attributes that
38
+ # are relevant to the node currently being
39
+ # processed.
40
+ @current_context = {}
41
+
42
+ @logger = MinimalLogger.new
43
+ end
44
+
45
+ # Element names
46
+ $ELEMENT_COMPOSITE = 'composite'
47
+ $ELEMENT_FILE = 'file'
48
+ $ELEMENT_FILE_GROUP = 'filegroup'
49
+ $ELEMENT_ATTRIBUTE = 'attribute'
50
+ $ELEMENT_INCLUDE = 'include'
51
+ $ELEMENT_TEMPLATE = 'template'
52
+ $ELEMENT_PROPERTY = 'property'
53
+ $ELEMENT_ROOT = 'kodekopelli'
54
+
55
+ # Attribute names
56
+ $ATTRIBUTE_ENVIRONMENT = 'environment'
57
+ $ATTRIBUTE_FILE = 'file'
58
+ $ATTRIBUTE_LOCATION = 'location'
59
+ $ATTRIBUTE_NAME = 'name'
60
+ $ATTRIBUTE_TEMPLATE = 'template'
61
+ $ATTRIBUTE_VALUE = 'value'
62
+
63
+ # Property names
64
+ $KODEKOPELLI_VERSION = "0.8.0"
65
+ $KODEKOPELLI_VERSION_PROPERTY = 'kodekopelli.version'
66
+ $KODEKOPELLI_START_TIME_PROPERTY = 'kodekopelli.start.time'
67
+
68
+ def run(runtime_args) # :nodoc:
69
+ properties_file = nil
70
+
71
+ opts = OptionParser.new
72
+
73
+ opts.banner = "Usage: kodekopelli [options] [configfile [configfile2 [configfile3] ...]]"
74
+ opts.separator ""
75
+ opts.separator "Core Options:"
76
+
77
+ opts.on("-h", "--help", "print this message") {
78
+ puts opts.to_s
79
+ return
80
+ }
81
+
82
+ opts.on("--version", "print the version information and exit") {
83
+ puts "Kodekopelli version #{$KODEKOPELLI_VERSION}"
84
+ return
85
+ }
86
+
87
+ opts.on("-q", "--quiet", "be extra quiet in log messages") {
88
+ @logger.silent = true
89
+ }
90
+
91
+ opts.on("-v", "--verbose", "be extra verbose in log messages") {
92
+ @logger.verbose = true
93
+ }
94
+
95
+ opts.on("-l", "--logo", "print the Kodekopelli logo and exit") {
96
+ puts Kodekopelli.logo
97
+ return
98
+ }
99
+
100
+ opts.on("--props k1=v1,k2=v2,...", Array,
101
+ "use the property key/value pair(s)", "specified") do |properties|
102
+ properties.each { |current_property|
103
+ first_delim_index = current_property.index('=')
104
+ if(first_delim_index > 0)
105
+ # Don't bother processing properties that don't have
106
+ # delimiters or in which no property name was
107
+ # specified.
108
+ current_property_key = current_property[0,first_delim_index]
109
+ current_property_value = current_property[first_delim_index + 1, current_property.length-1]
110
+
111
+ @property_hash[current_property_key]= current_property_value
112
+
113
+ end
114
+ }
115
+ end
116
+
117
+ opts.on("--propsfile file", "load all properties from file with --props", "taking precedence") do |propsfile|
118
+ properties_file = propsfile
119
+ end
120
+
121
+ opts.separator ""
122
+ opts.separator "Other Options:"
123
+
124
+ opts.on("--server [port]", Integer, "launch an HTTP server with the current",
125
+ "folder as the context root using,", "optionally, the port specified",
126
+ "(defaults to port 80)") do |port|
127
+ port_number = port || 80
128
+
129
+ s = HTTPServer.new(
130
+ :Port => port_number,
131
+ :DocumentRoot => Dir::pwd
132
+ )
133
+
134
+ trap("INT") {
135
+ @logger.info("Shutting down HTTP server listening on port [#{port_number}]...")
136
+ s.shutdown
137
+ }
138
+
139
+ @logger.info("Starting HTTP server listening on port [#{port_number}]...")
140
+ @logger.info("Context root location: [#{Dir::pwd}]")
141
+ s.start
142
+
143
+ return
144
+ end
145
+
146
+ opts.on("--system", "display all system properties available to", "Kodekopelli and exit") {
147
+ puts "*** SYSTEM PROPERTIES (#{Time.now.to_s}) ***"
148
+ puts sprintf("%-30s %s", "<KEY>", "<VALUE>")
149
+ ENV.to_hash.each_pair {|key,value|
150
+ puts sprintf("%-30s => %s", key, value)
151
+ }
152
+ return
153
+ }
154
+
155
+ # Perform a destructive parse, leaving only the
156
+ # configuration files, if any were specified.
157
+ begin
158
+ opts.parse!(runtime_args)
159
+ rescue => cl_err
160
+ puts "Unable to process command line options: #{cl_err.to_s}"
161
+ puts opts.to_s
162
+ exit(1)
163
+ end
164
+
165
+ unless(properties_file.nil?)
166
+ digest_properties_file(properties_file)
167
+ end
168
+
169
+ if(runtime_args.size > 0)
170
+ begin
171
+ process(runtime_args)
172
+ rescue => err
173
+ @logger.always("Root cause: #{err.to_s}")
174
+ @logger.always("Build failed.")
175
+ exit(1)
176
+ end
177
+ else
178
+ puts "No Kodekopelli configuration files specified."
179
+ puts opts.to_s
180
+ end
181
+ end
182
+ # Processes one or more Kodekopelli definitions files,
183
+ # given an array of file locations.
184
+ #
185
+ # :call-seq:
186
+ # process(array)
187
+ #
188
+ def process(files)
189
+ @logger.raw(Kodekopelli.logo)
190
+ files.each { |current_file|
191
+ process_definitions(current_file)
192
+ }
193
+ end
194
+
195
+ protected
196
+
197
+ def att(key, default = '')
198
+ @current_context[key] || default
199
+ end
200
+
201
+ def prop(key, default = '')
202
+ @property_hash[key] || default
203
+ end
204
+
205
+ def prop_att(key, default = '')
206
+ prop(key, nil) || att(key, nil) || default
207
+ end
208
+
209
+ def att_prop(key, default = '')
210
+ att(key, nil) || prop(key, nil) || default
211
+ end
212
+
213
+ def j(javascript)
214
+ Kodekopelli::Util.escape_javascript(javascript)
215
+ end
216
+
217
+ def include_file(filename, process=true, ignore_if_missing=false)
218
+ # The location of the properties file may contain expandable property directives. Replace any
219
+ # that exist prior to attempting its retrieval.
220
+ location = Kodekopelli::ExpandableProperties.replace_expandables(filename, @property_hash)
221
+
222
+ contents = Kodekopelli::Util.file_contents(location, ignore_if_missing)
223
+
224
+ if(process)
225
+ contents = process_erb(contents)
226
+ end
227
+
228
+ contents
229
+ end
230
+
231
+ def node_att(node, name, default = '')
232
+ value = node.attributes[name] || default
233
+ # The child attribute may contain expandable property directives. Replace
234
+ # any that exist prior to returning.
235
+ Kodekopelli::ExpandableProperties.replace_expandables(value, @property_hash)
236
+ end
237
+
238
+ def child_iterator
239
+ current = current_node
240
+ @logger.debug("Processing node [" + current.name + "]...")
241
+ result = ''
242
+
243
+ current.elements.each { |node|
244
+ text = ''
245
+ if(node.name == $ELEMENT_INCLUDE)
246
+ text = process_include(node)
247
+ result += yield(node, text)
248
+ else
249
+ if(node.name == $ELEMENT_COMPOSITE)
250
+ text = process_composite(node)
251
+ else
252
+ text = process_template(node)
253
+ end
254
+ result += yield(node, text)
255
+ end
256
+ }
257
+ result
258
+ end
259
+
260
+ def child_output
261
+ result = ''
262
+ child_iterator { |node, text|
263
+ result += text
264
+ }
265
+ result
266
+ end
267
+
268
+ private
269
+
270
+ # Given a reference to an XML representation of the
271
+ # definitions file, initializes the properties to be used during
272
+ # its processing.
273
+ def initialize_properties(doc)
274
+ @logger.debug("Initializing properties...")
275
+
276
+ # Iterate over the <property ... /> elements in the document, performing fine-grained
277
+ # processing (via a call to process_property_element) for each.
278
+ doc.elements.each("#{$ELEMENT_ROOT}/#{$ELEMENT_PROPERTY}") { |current_element|
279
+ process_property_element(current_element)
280
+ }
281
+
282
+ # Always add the system (environment) properties to the property
283
+ # hash
284
+ expose_system_properties
285
+
286
+ @logger.debug("Finished initializing properties.")
287
+ end
288
+
289
+ def expose_system_properties(prefix='')
290
+ ENV.to_hash.each_pair { |key, value|
291
+ @property_hash[prefix + key] = Kodekopelli::ExpandableProperties.replace_expandables(value, @property_hash)
292
+ }
293
+ end
294
+
295
+ # Performs fine-grained processing on a <property ... /> element, updating the
296
+ # property hash based upon the attributes provided.
297
+ def process_property_element(property_element)
298
+
299
+ # Retrieve the value of the "environment" attribute, if it was specified
300
+ environment_attribute_value = property_element.attributes[$ATTRIBUTE_ENVIRONMENT]
301
+
302
+ # If the "environment" attribute was specified... #
303
+ unless(environment_attribute_value.nil?)
304
+ # The attribute value may or may not end with a period ('.'). If it does, remove the period;
305
+ # then, regardless, concatenate another period to serve as a separator between the user-specified
306
+ # prefix and the environment attribute name suffix.
307
+ prefix = environment_attribute_value.chomp(".") + "."
308
+
309
+
310
+ expose_system_properties(prefix)
311
+
312
+ # No additional work is required for the current element.
313
+ return
314
+ end
315
+
316
+ # Retrieve the value of the "file" attribute, if it was specified
317
+ file_attribute_value = property_element.attributes[$ATTRIBUTE_FILE]
318
+
319
+ # If the "file" attribute was specified...
320
+ unless(file_attribute_value.nil?)
321
+ digest_properties_file(file_attribute_value)
322
+
323
+ # No additional work is required for the current element.
324
+ return
325
+ end
326
+
327
+ # The "name" attribute is required when either of the "value" or "location" attributes is
328
+ # provided
329
+ name_attribute_value = property_element.attributes[$ATTRIBUTE_NAME]
330
+
331
+ # Retrieve the value of the "location" attribute, if it was specified.
332
+ location_attribute_value = property_element.attributes[$ATTRIBUTE_LOCATION]
333
+
334
+ # If the "location" attribute was specified...
335
+ unless(location_attribute_value.nil?)
336
+ if(name_attribute_value.nil?)
337
+ raise "A #{$ATTRIBUTE_NAME} attribute must be specified for property element [<#{$ELEMENT_PROPERTY} ... />] when the #{$ATTRIBUTE_LOCATION} attribute is specified."
338
+ end
339
+
340
+ # The location may contain expandable property directives. Replace any that exist prior to checking its
341
+ # validity as a file or directory
342
+ location = Kodekopelli::ExpandableProperties.replace_expandables(location_attribute_value, @property_hash)
343
+
344
+ unless File.exists?(location)
345
+ raise "Unable to locate file or folder for location [#{location}] with name [#{name_attribute_value}]. Current working directory is [#{File.expand_path('.')}]. Properies: [#{@property_hash.to_s}]."
346
+ end
347
+
348
+ @property_hash[name_attribute_value] = location
349
+
350
+ # No additional work is required for the current element.
351
+ return
352
+ end
353
+
354
+ # Retrieve the value of the "value" attribute, if it was specified
355
+ value_attribute_value = property_element.attributes[$ATTRIBUTE_VALUE]
356
+
357
+ if(name_attribute_value.nil?)
358
+ raise "A #{$ATTRIBUTE_NAME} attribute must be specified for property element [<#{$ELEMENT_PROPERTY} ... />]"
359
+ end
360
+
361
+ if(value_attribute_value.nil?)
362
+ raise "A #{$ATTRIBUTE_VALUE} attribute must be specified for property element [<#{$ELEMENT_PROPERTY} ... />]"
363
+ end
364
+
365
+ # Add the property value to the property hash, keyed by the property name
366
+ @property_hash[name_attribute_value] = Kodekopelli::ExpandableProperties.replace_expandables(value_attribute_value, @property_hash)
367
+
368
+ end
369
+
370
+ def digest_properties_file(filename)
371
+ # The location of the properties file may contain expandable property directives. Replace any
372
+ # that exist prior to attempting its retrieval.
373
+ location = Kodekopelli::ExpandableProperties.replace_expandables(filename, @property_hash)
374
+ @logger.debug("Loading properties from file [#{location}]...")
375
+
376
+ # Create a hash containing those simple properties defined in the properties file
377
+ properties_file_hash = Kodekopelli::PropertiesFile.to_hash(location)
378
+
379
+ # For each property defined in the properties file, add it to the
380
+ # properties hash.
381
+ properties_file_hash.each_pair { |key, value|
382
+ @property_hash[key] = Kodekopelli::ExpandableProperties.replace_expandables(value, @property_hash)
383
+ }
384
+ @logger.debug("Finished loading properties from file [#{location}].")
385
+ end
386
+
387
+ # Adds any global attributes specified in the definitions file as top-level
388
+ # context attributes.
389
+ def initialize_global_attributes(doc)
390
+ @logger.debug("Initializing global attributes...")
391
+
392
+ # Hash to hold global attributes
393
+ values = Kodekopelli::FrozenKeyHash.new
394
+
395
+ # Iterate over the <attribute ... /> elements in the document, performing fine-grained
396
+ # processing for each.
397
+ doc.elements.each("#{$ELEMENT_ROOT}/#{$ELEMENT_ATTRIBUTE}") { |attribute_element|
398
+ # Retrieve the value of the "name" attribute
399
+ name_attribute_value = attribute_element.attributes[$ATTRIBUTE_NAME]
400
+
401
+ # Retrieve the value of the "attribute" attribute
402
+ value_attribute_value = attribute_element.attributes[$ATTRIBUTE_VALUE]
403
+
404
+ if(name_attribute_value.nil?)
405
+ raise "A #{$ATTRIBUTE_NAME} attribute must be specified for property element [<#{$ELEMENT_ATTRIBUTE} ... />]"
406
+ end
407
+
408
+ if(value_attribute_value.nil?)
409
+ raise "A #{$ATTRIBUTE_VALUE} attribute must be specified for property element [<#{$ELEMENT_ATTRIBUTE} ... />]"
410
+ end
411
+
412
+ # The attribute value may contain expandable property directives. Replace any
413
+ # that exist prior to storing it in the context
414
+ values[name_attribute_value] = Kodekopelli::ExpandableProperties.replace_expandables(value_attribute_value, @property_hash)
415
+ }
416
+
417
+ push_hash_context(values)
418
+ @logger.debug("Finished initializing global attributes.")
419
+ end
420
+
421
+ def process_composite(composite_node)
422
+ @logger.debug("Attempting to process composite element...")
423
+ composite_results = ''
424
+
425
+ unless(composite_node.elements.size > 0)
426
+ raise "No child elements were specified for composite element [#{composite_node.to_s}]"
427
+ end
428
+
429
+ push_context(composite_node)
430
+
431
+ if(composite_node.attributes[$ATTRIBUTE_TEMPLATE].nil?)
432
+ # raise "No template attribute was specified for composite element [#{composite_node.to_s}]"
433
+ composite_results = process_children(composite_node)
434
+ else
435
+ # Retrieve the value of the "template" attribute
436
+ template_attribute_value = composite_node.attributes[$ATTRIBUTE_TEMPLATE]
437
+
438
+ # The location may contain expandable property directives. Replace any that exist prior
439
+ # to retrieving its contents.
440
+ location = Kodekopelli::ExpandableProperties.replace_expandables(template_attribute_value, @property_hash)
441
+
442
+ @logger.debug("Attempting to process composite element with template [#{location}]...")
443
+ erb_script = Kodekopelli::Util.file_contents(location)
444
+
445
+ # Compile the ERB script and run it
446
+ composite_results = process_erb(erb_script)
447
+ end
448
+
449
+ @logger.debug("Finished processing composite element.")
450
+
451
+ pop_context
452
+ composite_results
453
+ end
454
+
455
+ # Process the contents of the erb_script provided,
456
+ # returning the results.
457
+ def process_erb(erb_script)
458
+ script = erb_script || ''
459
+ begin
460
+ erb = ERB.new(script)
461
+ erb_results = erb.result(binding)
462
+ rescue => err
463
+ raise err #, "There was a problem interpreting the ERB script."
464
+ end
465
+ erb_results
466
+ end
467
+
468
+ # Given a template node, process its content using the template specified or, optionally,
469
+ # processes its content directly.
470
+ def process_template(node)
471
+ # By default, the erb script is equal to the element data
472
+ template_element_content = node.text
473
+
474
+ # Retrieve any inline template element content that
475
+ # may exist, defaulting to the empty string
476
+ erb_script = template_element_content || ''
477
+
478
+ # If no template element content was found, retrieve the template
479
+ # attribute value specified in the current template element.
480
+ if(erb_script == '')
481
+ # Retrieve the value of the "template" attribute, if it was specified.
482
+ template_attribute_value = node.attributes[$ATTRIBUTE_TEMPLATE]
483
+
484
+ # If no template attribute was found for the current template element,
485
+ # see if it exists in the contexgt...
486
+ if(template_attribute_value.nil?)
487
+ template_attribute_value = att($ATTRIBUTE_TEMPLATE, nil)
488
+ if(template_attribute_value.nil?)
489
+ raise "No template location specified for element [#{node.to_s}]"
490
+ end
491
+ end
492
+
493
+ # The location may contain expandable property directives. Replace any that exist prior
494
+ # to retrieving its contents.
495
+ location = Kodekopelli::ExpandableProperties.replace_expandables(template_attribute_value, @property_hash)
496
+
497
+ @logger.debug("Attempting to process template [#{location}]...")
498
+ erb_script = Kodekopelli::Util.file_contents(location)
499
+ @logger.debug("Finished processing template [#{location}].")
500
+ end
501
+
502
+ context = push_context(node)
503
+
504
+ # Compile the ERB script and run it
505
+ erb_results = process_erb(erb_script)
506
+
507
+ pop_context
508
+ erb_results
509
+ end
510
+
511
+ # Given an include node, retrieves the contents of the file as
512
+ # specified by its file attribute
513
+ #
514
+ # :call-seq:
515
+ # process_include(node) -> file contents
516
+ #
517
+ def process_include(node, process=false)
518
+ filename = node.attributes[$ATTRIBUTE_FILE]
519
+
520
+ # The location may contain expandable property directives. Replace any that exist prior
521
+ # to retrieving its contents.
522
+ location = Kodekopelli::ExpandableProperties.replace_expandables(filename, @property_hash)
523
+
524
+ contents = Kodekopelli::Util.file_contents(location)
525
+
526
+ if(process)
527
+ contents = process_erb(contents)
528
+ end
529
+ contents
530
+ end
531
+
532
+ # Resets all contexts and other runtime artifacts to
533
+ # allow for processing a fresh definitions file
534
+ def reset_state
535
+ @logger.debug('Resetting state...')
536
+
537
+ @property_hash.clear
538
+ @context_stack.clear
539
+ @node_stack.clear
540
+
541
+ @logger.debug('Reset complete.')
542
+ end
543
+
544
+ # Returns a hash of the current context.
545
+ #
546
+ # :call-seq:
547
+ # build_current_context -> hash
548
+ #
549
+ def build_current_context
550
+ # The context_stack instance attribute is an array of hashes
551
+ # containing 1 or more contexts, from oldest to newest. Flatten
552
+ # it so that the more recent values take precedence over the
553
+ # older values.
554
+ @current_context = {}
555
+ @context_stack.each {|current_hash|
556
+ current_hash.each { |key, value|
557
+ @current_context[ key ] = value
558
+ }
559
+ }
560
+ @current_context
561
+ end
562
+
563
+ # Returns the topmost node on the node stack
564
+ # :call-seq:
565
+ # current_node -> node
566
+ #
567
+ def current_node
568
+ @node_stack.last
569
+ end
570
+
571
+ def process_children(node)
572
+ result = ''
573
+ # Iterate through the elements and process them
574
+ node.elements.each { |current_node|
575
+ if(current_node.name == $ELEMENT_TEMPLATE)
576
+ result += process_template(current_node)
577
+ end
578
+
579
+ if(current_node.name == $ELEMENT_COMPOSITE)
580
+ result += process_composite(current_node)
581
+ end
582
+
583
+ if(current_node.name == $ELEMENT_INCLUDE)
584
+ result += process_include(current_node)
585
+ end
586
+ }
587
+ result
588
+ end
589
+
590
+ # Given an XML node, adds a context to the stack
591
+ def push_context(node)
592
+ @node_stack.push(node)
593
+ # Create a hash of the attributes for this XML node
594
+ values = {}
595
+
596
+ node.attributes.each { | key, value |
597
+ # The attribute value may contain expandable property directives. Replace any
598
+ # that exist prior to storing it in the context
599
+ values[key] = Kodekopelli::ExpandableProperties.replace_expandables(value, @property_hash)
600
+ }
601
+
602
+ push_hash_context(values)
603
+ end
604
+
605
+ # Given a hash, adds a context to the stack
606
+ def push_hash_context(values)
607
+ # Add the has to the contet stack
608
+ @context_stack.push( values )
609
+ build_current_context
610
+ end
611
+
612
+ # Pops the topmost context for a particular
613
+ # node, indicating that processing has completed
614
+ def pop_context
615
+ @node_stack.pop
616
+ @context_stack.pop
617
+ end
618
+
619
+ def process_definitions(definitions_file)
620
+ @logger.info("Processing Kodekopelli file [#{definitions_file}]...")
621
+ unless File.exists?(definitions_file)
622
+ raise "Could not locate definitions file [#{definitions_file}]."
623
+ end
624
+ begin
625
+ doc = REXML::Document.new(File.open(definitions_file))
626
+ rescue
627
+ raise "Could not parse definition file [#{definitions_file}]."
628
+ end
629
+ generated_file_count = 0
630
+ # Process properties, if specified...
631
+ # Set Kodekopelli-specific properties
632
+ @property_hash[$KODEKOPELLI_VERSION_PROPERTY] = $KODEKOPELLI_VERSION
633
+ @property_hash[$KODEKOPELLI_START_TIME_PROPERTY] = Time.now
634
+
635
+ property_elements = doc.root.elements[$ELEMENT_PROPERTY]
636
+ if(property_elements.nil?)
637
+ @logger.debug("No property [<#{$ELEMENT_PROPERTY} ... />] elements found.")
638
+ else
639
+ initialize_properties(doc)
640
+ doc.root.elements.delete_all $ELEMENT_PROPERTY
641
+ end
642
+ @logger.debug("Kodekopelli Properties:\n#{@property_hash.to_s}")
643
+
644
+ # Process globals, if specified...
645
+ global_attributes = doc.root.elements[$ELEMENT_ATTRIBUTE]
646
+ if(global_attributes.nil?)
647
+ @logger.debug("No global attribute [<#{$ELEMENT_ATTRIBUTE} ... />] elements found.");
648
+ else
649
+ initialize_global_attributes(doc)
650
+ doc.root.elements.delete_all $ELEMENT_ATTRIBUTE
651
+ end
652
+
653
+ # Start the context stack
654
+ push_context(doc.root)
655
+
656
+ # Get the <filegroup>...</filegroup> nodes
657
+ filegroup_nodes = doc.root.elements
658
+
659
+ filegroup_nodes.each { |current_filegroup_node|
660
+
661
+ file_nodes = current_filegroup_node.elements
662
+
663
+ push_context(current_filegroup_node)
664
+
665
+ file_nodes.each { |current_file_node|
666
+ file_name = current_file_node.attributes[$ATTRIBUTE_NAME]
667
+
668
+ # The location of the output file name may contain expandable property directives. Replace any
669
+ # that exist prior to attempting its retrieval.
670
+ target = Kodekopelli::ExpandableProperties.replace_expandables(file_name, @property_hash)
671
+
672
+ @logger.info("Generating file [#{target}]...")
673
+ begin
674
+ outfile = File.open(target, "w")
675
+ rescue
676
+ raise "Unable to create output file #{target}"
677
+ end
678
+
679
+ push_context(current_file_node)
680
+
681
+ template_attribute_value = current_file_node.attributes[$ATTRIBUTE_TEMPLATE] || att($ATTRIBUTE_TEMPLATE, nil)
682
+
683
+ if(template_attribute_value.nil?)
684
+ if(current_file_node.elements.size == 0)
685
+ erb_script = current_file_node.text || ''
686
+ outfile.write(process_erb(erb_script))
687
+ else
688
+ # Iterate through the elements and process them
689
+ outfile.write(process_children(current_file_node))
690
+ end
691
+ else
692
+ # The location may contain expandable property directives. Replace any that exist prior
693
+ # to retrieving its contents.
694
+ location = Kodekopelli::ExpandableProperties.replace_expandables(template_attribute_value, @property_hash)
695
+
696
+ @logger.debug("Attempting to process file element with template [#{location}]...")
697
+ erb_script = Kodekopelli::Util.file_contents(location)
698
+
699
+ # Compile the ERB script and run it
700
+ outfile.write(process_erb(erb_script))
701
+ @logger.debug("Finished processing file element.")
702
+ end
703
+
704
+ # Remove individual output file context
705
+ pop_context
706
+ outfile.close
707
+ @logger.info("Finished generating file [#{target}].")
708
+
709
+ generated_file_count += 1
710
+ } # End individual output file iteration
711
+
712
+ # Remove filegroup context
713
+ pop_context
714
+
715
+ } # End filegroup iteration
716
+
717
+ # Remove the root element context
718
+ pop_context
719
+
720
+ @logger.info("#{generated_file_count} total files were generated.")
721
+ @logger.info("Finished processing Kodekopelli file [#{definitions_file}].")
722
+
723
+ reset_state
724
+ end
725
+ end
726
+ end