adhearsion 2.0.0.beta1 → 2.0.0.rc1

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 (118) hide show
  1. data/.travis.yml +2 -4
  2. data/CHANGELOG.md +34 -4
  3. data/README.markdown +2 -1
  4. data/Rakefile +22 -1
  5. data/adhearsion.gemspec +1 -0
  6. data/bin/ahn +0 -2
  7. data/features/cli_daemon.feature +2 -0
  8. data/features/cli_restart.feature +19 -0
  9. data/features/cli_start.feature +4 -6
  10. data/features/cli_stop.feature +3 -0
  11. data/features/step_definitions/app_generator_steps.rb +2 -0
  12. data/features/step_definitions/cli_steps.rb +2 -0
  13. data/features/support/aruba_helper.rb +2 -0
  14. data/features/support/env.rb +8 -46
  15. data/features/support/utils.rb +2 -0
  16. data/lib/adhearsion.rb +4 -6
  17. data/lib/adhearsion/call.rb +71 -17
  18. data/lib/adhearsion/call_controller.rb +25 -14
  19. data/lib/adhearsion/call_controller/dial.rb +34 -15
  20. data/lib/adhearsion/call_controller/input.rb +186 -144
  21. data/lib/adhearsion/call_controller/output.rb +10 -6
  22. data/lib/adhearsion/call_controller/record.rb +11 -13
  23. data/lib/adhearsion/call_controller/utility.rb +2 -0
  24. data/lib/adhearsion/calls.rb +4 -2
  25. data/lib/adhearsion/cli.rb +4 -0
  26. data/lib/adhearsion/cli_commands.rb +8 -2
  27. data/lib/adhearsion/configuration.rb +7 -3
  28. data/lib/adhearsion/console.rb +17 -17
  29. data/lib/adhearsion/events.rb +10 -4
  30. data/lib/adhearsion/foundation.rb +9 -0
  31. data/lib/adhearsion/foundation/custom_daemonizer.rb +3 -1
  32. data/lib/adhearsion/foundation/exception_handler.rb +2 -0
  33. data/lib/adhearsion/foundation/libc.rb +2 -0
  34. data/lib/adhearsion/foundation/object.rb +3 -0
  35. data/lib/adhearsion/foundation/thread_safety.rb +5 -11
  36. data/lib/adhearsion/generators.rb +2 -0
  37. data/lib/adhearsion/generators/app/app_generator.rb +2 -0
  38. data/lib/adhearsion/generators/app/templates/README.md +9 -0
  39. data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +38 -16
  40. data/lib/adhearsion/generators/app/templates/config/environment.rb +2 -0
  41. data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +5 -3
  42. data/lib/adhearsion/generators/controller/controller_generator.rb +2 -0
  43. data/lib/adhearsion/generators/controller/templates/lib/controller.rb +2 -0
  44. data/lib/adhearsion/generators/controller/templates/spec/controller_spec.rb +2 -0
  45. data/lib/adhearsion/generators/generator.rb +3 -1
  46. data/lib/adhearsion/generators/plugin/plugin_generator.rb +2 -0
  47. data/lib/adhearsion/initializer.rb +31 -17
  48. data/lib/adhearsion/linux_proc_name.rb +2 -0
  49. data/lib/adhearsion/logging.rb +5 -3
  50. data/lib/adhearsion/menu_dsl.rb +2 -0
  51. data/lib/adhearsion/menu_dsl/calculated_match.rb +2 -0
  52. data/lib/adhearsion/menu_dsl/calculated_match_collection.rb +2 -0
  53. data/lib/adhearsion/menu_dsl/fixnum_match_calculator.rb +2 -0
  54. data/lib/adhearsion/menu_dsl/match_calculator.rb +2 -0
  55. data/lib/adhearsion/menu_dsl/menu.rb +58 -4
  56. data/lib/adhearsion/menu_dsl/menu_builder.rb +14 -1
  57. data/lib/adhearsion/menu_dsl/range_match_calculator.rb +4 -1
  58. data/lib/adhearsion/menu_dsl/string_match_calculator.rb +2 -0
  59. data/lib/adhearsion/outbound_call.rb +2 -0
  60. data/lib/adhearsion/plugin.rb +9 -7
  61. data/lib/adhearsion/plugin/collection.rb +3 -1
  62. data/lib/adhearsion/plugin/initializer.rb +3 -1
  63. data/lib/adhearsion/process.rb +8 -2
  64. data/lib/adhearsion/punchblock_plugin.rb +3 -1
  65. data/lib/adhearsion/punchblock_plugin/initializer.rb +34 -11
  66. data/lib/adhearsion/router.rb +4 -2
  67. data/lib/adhearsion/router/route.rb +2 -0
  68. data/lib/adhearsion/script_ahn_loader.rb +2 -0
  69. data/lib/adhearsion/tasks.rb +2 -0
  70. data/lib/adhearsion/tasks/configuration.rb +2 -0
  71. data/lib/adhearsion/tasks/debugging.rb +8 -0
  72. data/lib/adhearsion/tasks/environment.rb +2 -0
  73. data/lib/adhearsion/tasks/plugins.rb +2 -0
  74. data/lib/adhearsion/tasks/testing.rb +2 -0
  75. data/lib/adhearsion/version.rb +3 -1
  76. data/pre-commit +2 -0
  77. data/spec/adhearsion/call_controller/dial_spec.rb +114 -25
  78. data/spec/adhearsion/call_controller/input_spec.rb +192 -169
  79. data/spec/adhearsion/call_controller/output_spec.rb +26 -12
  80. data/spec/adhearsion/call_controller/record_spec.rb +29 -77
  81. data/spec/adhearsion/call_controller/utility_spec.rb +69 -0
  82. data/spec/adhearsion/call_controller_spec.rb +90 -15
  83. data/spec/adhearsion/call_spec.rb +92 -24
  84. data/spec/adhearsion/calls_spec.rb +9 -7
  85. data/spec/adhearsion/configuration_spec.rb +58 -56
  86. data/spec/adhearsion/console_spec.rb +4 -2
  87. data/spec/adhearsion/events_spec.rb +9 -7
  88. data/spec/adhearsion/generators_spec.rb +3 -1
  89. data/spec/adhearsion/initializer_spec.rb +16 -14
  90. data/spec/adhearsion/logging_spec.rb +11 -9
  91. data/spec/adhearsion/menu_dsl/calculated_match_collection_spec.rb +6 -4
  92. data/spec/adhearsion/menu_dsl/calculated_match_spec.rb +6 -4
  93. data/spec/adhearsion/menu_dsl/fixnum_match_calculator_spec.rb +3 -1
  94. data/spec/adhearsion/menu_dsl/match_calculator_spec.rb +2 -0
  95. data/spec/adhearsion/menu_dsl/menu_builder_spec.rb +42 -11
  96. data/spec/adhearsion/menu_dsl/menu_spec.rb +197 -36
  97. data/spec/adhearsion/menu_dsl/range_match_calculator_spec.rb +4 -2
  98. data/spec/adhearsion/menu_dsl/string_match_calculator_spec.rb +5 -3
  99. data/spec/adhearsion/outbound_call_spec.rb +7 -5
  100. data/spec/adhearsion/plugin_spec.rb +19 -15
  101. data/spec/adhearsion/process_spec.rb +12 -7
  102. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +35 -15
  103. data/spec/adhearsion/punchblock_plugin_spec.rb +4 -1
  104. data/spec/adhearsion/router/route_spec.rb +8 -6
  105. data/spec/adhearsion/router_spec.rb +12 -10
  106. data/spec/adhearsion_spec.rb +13 -2
  107. data/spec/capture_warnings.rb +33 -0
  108. data/spec/spec_helper.rb +4 -0
  109. data/spec/support/call_controller_test_helpers.rb +2 -4
  110. data/spec/support/initializer_stubs.rb +8 -5
  111. data/spec/support/logging_helpers.rb +2 -0
  112. data/spec/support/punchblock_mocks.rb +2 -0
  113. metadata +84 -71
  114. data/EVENTS +0 -11
  115. data/lib/adhearsion/call_controller/menu.rb +0 -124
  116. data/lib/adhearsion/foundation/all.rb +0 -8
  117. data/spec/adhearsion/call_controller/menu_spec.rb +0 -120
  118. data/spec/adhearsion/menu_dsl_spec.rb +0 -12
@@ -1,27 +1,28 @@
1
- # Centralized way to overwrite any Adhearsion platform or plugin configuration
2
- # - Execute rake adhearsion:config:desc to get the configuration options
3
- # - Execute rake adhearsion:config:show to get the configuration values
4
- #
5
- # To update a plugin configuration you can write either:
6
- #
7
- # * Option 1
8
- # Adhearsion.config.<plugin-name> do |config|
9
- # config.<key> = <value>
10
- # end
11
- #
12
- # * Option 2
13
- # Adhearsion.config do |config|
14
- # config.<plugin-name>.<key> = <value>
15
- # end
1
+ # encoding: utf-8
16
2
 
17
3
  Adhearsion.config do |config|
18
4
 
5
+ # Centralized way to specify any Adhearsion platform or plugin configuration
6
+ # - Execute rake config:show to view the active configuration values
7
+ #
8
+ # To update a plugin configuration you can write either:
9
+ #
10
+ # * Option 1
11
+ # Adhearsion.config.<plugin-name> do |config|
12
+ # config.<key> = <value>
13
+ # end
14
+ #
15
+ # * Option 2
16
+ # Adhearsion.config do |config|
17
+ # config.<plugin-name>.<key> = <value>
18
+ # end
19
+
19
20
  config.development do |dev|
20
21
  dev.platform.logging.level = :debug
21
22
  end
22
23
 
23
24
  ##
24
- # Use with Voxeo PRISM or other Rayo installation
25
+ # Use with Rayo (eg Voxeo PRISM)
25
26
  #
26
27
  # config.punchblock.username = "" # Your XMPP JID for use with Rayo
27
28
  # config.punchblock.password = "" # Your XMPP password
@@ -36,6 +37,27 @@ Adhearsion.config do |config|
36
37
  # config.punchblock.port = 5038 # Your AMI port
37
38
  end
38
39
 
40
+ Adhearsion::Events.draw do
41
+
42
+ # Register global handlers for events
43
+ #
44
+ # eg. Handling Punchblock events
45
+ # punchblock do |event|
46
+ # ...
47
+ # end
48
+ #
49
+ # eg Handling PeerStatus AMI events
50
+ # ami :name => 'PeerStatus' do |event|
51
+ # ...
52
+ # end
53
+ #
54
+ end
55
+
39
56
  Adhearsion.router do
57
+
58
+ #
59
+ # Specify your call routes, directing calls with particular attributes to a controller
60
+ #
61
+
40
62
  route 'default', SimonGame
41
63
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler'
3
5
  Bundler.setup
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  class SimonGame < Adhearsion::CallController
2
4
  def run
3
5
  answer
@@ -19,7 +21,7 @@ class SimonGame < Adhearsion::CallController
19
21
 
20
22
  def say_number
21
23
  update_number
22
- speak @number
24
+ say @number
23
25
  end
24
26
 
25
27
  def collect_attempt
@@ -28,9 +30,9 @@ class SimonGame < Adhearsion::CallController
28
30
 
29
31
  def verify_attempt
30
32
  if attempt_correct?
31
- speak 'good'
33
+ say 'good'
32
34
  else
33
- speak "#{@number.length - 1} times wrong, try again smarty"
35
+ say "#{@number.length - 1} times wrong, try again smarty"
34
36
  reset
35
37
  end
36
38
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module Generators
3
5
  class ControllerGenerator < Generator
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  class <%= @controller_name %> < Adhearsion::CallController
2
4
  def run
3
5
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  describe <%= @controller_name %> do
2
4
 
3
5
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  begin
2
4
  require 'thor/group'
3
5
  rescue LoadError
@@ -66,7 +68,7 @@ module Adhearsion
66
68
  def self.generator_name
67
69
  @generator_name ||= begin
68
70
  if generator = name.to_s.split('::').last
69
- generator.sub! /Generator$/, ''
71
+ generator.sub!(/Generator$/, '')
70
72
  generator.underscore
71
73
  end
72
74
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module Generators
3
5
  class PluginGenerator < Generator
@@ -1,5 +1,8 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'adhearsion/punchblock_plugin'
2
4
  require 'adhearsion/linux_proc_name'
5
+ require 'rbconfig'
3
6
 
4
7
  module Adhearsion
5
8
  class Initializer
@@ -41,6 +44,7 @@ module Adhearsion
41
44
  initialize_log_paths
42
45
  daemonize! if should_daemonize?
43
46
  start_logging
47
+ debugging_log
44
48
  launch_console if need_console?
45
49
  catch_termination_signal
46
50
  create_pid_file
@@ -52,10 +56,9 @@ module Adhearsion
52
56
  run_plugins
53
57
  trigger_after_initialized_hooks
54
58
 
55
- if Adhearsion.status == :booting
56
- Adhearsion::Process.booted
57
- logger.info "Adhearsion v#{Adhearsion::VERSION} initialized with environment <#{Adhearsion.config.platform.environment}>!"
58
- end
59
+ Adhearsion::Process.booted if Adhearsion.status == :booting
60
+
61
+ logger.info "Adhearsion v#{Adhearsion::VERSION} initialized in \"#{Adhearsion.config.platform.environment}\"!" if Adhearsion.status == :running
59
62
 
60
63
  # This method will block until all important threads have finished.
61
64
  # When it does, the process will exit.
@@ -63,25 +66,37 @@ module Adhearsion
63
66
  self
64
67
  end
65
68
 
69
+ def debugging_items
70
+ [
71
+ "OS: #{RbConfig::CONFIG['host_os']} - RUBY: #{RUBY_ENGINE} #{RUBY_VERSION}",
72
+ "Environment: #{ENV.inspect}",
73
+ Adhearsion.config.description(:all),
74
+ "Gem versions: #{Gem.loaded_specs.inject([]) { |c,g| c << "#{g[0]} #{g[1].version}" }}"
75
+ ]
76
+ end
77
+
78
+ def debugging_log
79
+ debugging_items.each do |item|
80
+ logger.trace item
81
+ end
82
+ end
83
+
66
84
  def update_rails_env_var
67
85
  env = ENV['AHN_ENV']
68
86
  if env && Adhearsion.config.valid_environment?(env.to_sym)
69
- if ENV['RAILS_ENV']
70
- logger.info "Using provided RAILS_ENV value of <#{ENV['RAILS_ENV']}>"
71
- else
72
- logger.warn "Setting RAILS_ENV variable to <#{env}>"
87
+ unless ENV['RAILS_ENV']
88
+ logger.info "Copying AHN_ENV (#{env}) to RAILS_ENV"
73
89
  ENV['RAILS_ENV'] = env
74
90
  end
75
91
  else
76
- env = ENV['RAILS_ENV']
77
- if env
78
- logger.info "Using the configured value for RAILS_ENV : <#{env}>"
79
- else
92
+ unless ENV['RAILS_ENV']
80
93
  env = Adhearsion.config.platform.environment.to_s
81
- logger.info "Defining RAILS_ENV variable to <#{env}>"
94
+ ENV['AHN_ENV'] = env
95
+ logger.info "Setting RAILS_ENV to \"#{env}\""
82
96
  ENV['RAILS_ENV'] = env
83
97
  end
84
98
  end
99
+ logger.warn "AHN_ENV(#{ENV['AHN_ENV']}) does not match RAILS_ENV(#{ENV['RAILS_ENV']})!" unless ENV['RAILS_ENV'] == ENV['AHN_ENV']
85
100
  env
86
101
  end
87
102
 
@@ -202,7 +217,7 @@ module Adhearsion
202
217
 
203
218
  def daemonize!
204
219
  logger.info "Daemonizing now!"
205
- logger.info "Creating PID file #{pid_file}"
220
+ logger.debug "Creating PID file #{pid_file}"
206
221
  extend Adhearsion::CustomDaemonizer
207
222
  daemonize resolve_log_file_path
208
223
  end
@@ -211,7 +226,6 @@ module Adhearsion
211
226
  Adhearsion::Process.important_threads << Thread.new do
212
227
  catching_standard_errors do
213
228
  Adhearsion::Console.run
214
- Adhearsion::Process.shutdown
215
229
  end
216
230
  end
217
231
  end
@@ -241,8 +255,8 @@ module Adhearsion
241
255
  end
242
256
 
243
257
  def initialize_exception_logger
244
- Events.register_handler :exception do |e|
245
- logger.error e
258
+ Events.register_handler :exception do |e, l|
259
+ (l || logger).error e
246
260
  end
247
261
  end
248
262
 
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
 
3
5
  # https://gist.github.com/1350729
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'logging'
2
4
 
3
5
  module Adhearsion
@@ -11,14 +13,14 @@ module Adhearsion
11
13
 
12
14
  ::Logging.color_scheme 'bright',
13
15
  :levels => {
16
+ :debug => :magenta,
14
17
  :info => :green,
15
18
  :warn => :yellow,
16
19
  :error => :red,
17
20
  :fatal => [:white, :on_red]
18
21
  },
19
- :date => :blue,
20
- :logger => :cyan,
21
- :message => :magenta
22
+ :date => [:bold, :blue],
23
+ :logger => :cyan
22
24
 
23
25
  def adhearsion_pattern
24
26
  '[%d] %-5l %c: %m\n'
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module MenuDSL
3
5
  extend ActiveSupport::Autoload
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module MenuDSL
3
5
  class CalculatedMatch
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module MenuDSL
3
5
  class CalculatedMatchCollection
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module MenuDSL
3
5
  class FixnumMatchCalculator < MatchCalculator
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module MenuDSL
3
5
  class MatchCalculator
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adhearsion
2
4
  module MenuDSL
3
5
 
@@ -6,21 +8,40 @@ module Adhearsion
6
8
  DEFAULT_MAX_NUMBER_OF_TRIES = 1
7
9
  DEFAULT_TIMEOUT = 5
8
10
 
9
- attr_reader :builder, :timeout, :tries_count, :max_number_of_tries
11
+ InvalidStructureError = Class.new StandardError
12
+
13
+ attr_reader :builder, :timeout, :tries_count, :max_number_of_tries, :terminator, :limit, :interruptible, :status
10
14
 
11
15
  def initialize(options = {}, &block)
12
16
  @tries_count = 0 # Counts the number of tries the menu's been executed
13
17
  @timeout = options[:timeout] || DEFAULT_TIMEOUT
14
18
  @max_number_of_tries = options[:tries] || DEFAULT_MAX_NUMBER_OF_TRIES
19
+ @terminator = options[:terminator].to_s
20
+ @limit = options[:limit]
21
+ @interruptible = options.has_key?(:interruptible) ? options[:interruptible] : true
15
22
  @builder = MenuDSL::MenuBuilder.new
23
+ @terminated = false
16
24
 
17
- @builder.build &block
25
+ @builder.build(&block) if block
18
26
 
19
27
  initialize_digit_buffer
20
28
  end
21
29
 
30
+ def validate(mode = nil)
31
+ case mode
32
+ when :basic
33
+ @terminator.present? || !!@limit || raise(InvalidStructureError, "You must specify at least one of limit or terminator")
34
+ else
35
+ @builder.has_matchers? || raise(InvalidStructureError, "You must specify one or more matchers")
36
+ end
37
+ end
38
+
22
39
  def <<(other)
23
- digit_buffer << other
40
+ if other == terminator
41
+ @terminated = true
42
+ else
43
+ digit_buffer << other
44
+ end
24
45
  end
25
46
 
26
47
  def digit_buffer
@@ -30,6 +51,7 @@ module Adhearsion
30
51
  def digit_buffer_string
31
52
  digit_buffer.to_s
32
53
  end
54
+ alias :result :digit_buffer_string
33
55
 
34
56
  def digit_buffer_empty?
35
57
  digit_buffer.empty?
@@ -38,6 +60,11 @@ module Adhearsion
38
60
  def continue
39
61
  return get_another_digit_or_timeout! if digit_buffer_empty?
40
62
 
63
+ return menu_terminated! if @terminated
64
+ return menu_limit_reached! if limit && digit_buffer.size >= limit
65
+
66
+ return menu_validator_terminated! if execute_validator_hook
67
+
41
68
  calculated_matches = builder.calculate_matches_for digit_buffer_string
42
69
 
43
70
  if calculated_matches.exact_match_count >= 1
@@ -47,7 +74,7 @@ module Adhearsion
47
74
  else
48
75
  get_another_digit_or_finish! first_exact_match.match_payload, first_exact_match.query
49
76
  end
50
- elsif calculated_matches.potential_match_count >= 1
77
+ elsif calculated_matches.potential_match_count >= 1 || !@builder.has_matchers?
51
78
  get_another_digit_or_timeout!
52
79
  else
53
80
  invalid!
@@ -75,6 +102,10 @@ module Adhearsion
75
102
  builder.execute_hook_for :failure, digit_buffer_string
76
103
  end
77
104
 
105
+ def execute_validator_hook
106
+ builder.execute_hook_for :validator, digit_buffer_string
107
+ end
108
+
78
109
  protected
79
110
 
80
111
  # If you're using a more complex class in subclasses, you may want to override this method in addition to the
@@ -84,18 +115,37 @@ module Adhearsion
84
115
  end
85
116
 
86
117
  def invalid!
118
+ @status = :invalid
87
119
  MenuResultInvalid.new
88
120
  end
89
121
 
90
122
  def menu_result_found!(match_object, new_extension)
123
+ @status = :matched
91
124
  MenuResultFound.new(match_object, new_extension)
92
125
  end
93
126
 
127
+ def menu_terminated!
128
+ @status = :terminated
129
+ MenuTerminated.new
130
+ end
131
+
132
+ def menu_validator_terminated!
133
+ @status = :validator_terminated
134
+ MenuValidatorTerminated.new
135
+ end
136
+
137
+ def menu_limit_reached!
138
+ @status = :limited
139
+ MenuLimitReached.new
140
+ end
141
+
94
142
  def get_another_digit_or_finish!(match_payload, new_extension)
143
+ @status = :multi_matched
95
144
  MenuGetAnotherDigitOrFinish.new(match_payload, new_extension)
96
145
  end
97
146
 
98
147
  def get_another_digit_or_timeout!
148
+ @status = :potential
99
149
  MenuGetAnotherDigitOrTimeout.new
100
150
  end
101
151
 
@@ -128,6 +178,10 @@ module Adhearsion
128
178
 
129
179
  MenuResultInvalid = Class.new MenuResult
130
180
 
181
+ MenuTerminated = Class.new MenuResultDone
182
+ MenuValidatorTerminated = Class.new MenuResultDone
183
+ MenuLimitReached = Class.new MenuResultDone
184
+
131
185
  # For our default purpose, we need the digit_buffer to behave much like a normal String except that it should
132
186
  # handle its own resetting (clearing)
133
187
  class ClearableStringBuffer < String