killbill 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Jarfile +3 -2
  2. data/README.md +21 -0
  3. data/VERSION +1 -1
  4. data/killbill.gemspec +4 -2
  5. data/lib/killbill.rb +3 -4
  6. data/lib/killbill/http_servlet.rb +99 -0
  7. data/lib/killbill/jpayment.rb +152 -0
  8. data/lib/killbill/jresponse/jconverter.rb +100 -0
  9. data/lib/killbill/jresponse/jpayment_method_response.rb +89 -0
  10. data/lib/killbill/jresponse/jpayment_method_response_internal.rb +54 -0
  11. data/lib/killbill/jresponse/jpayment_response.rb +59 -0
  12. data/lib/killbill/jresponse/jrefund_response.rb +60 -0
  13. data/lib/killbill/logger.rb +44 -0
  14. data/lib/killbill/payment.rb +16 -8
  15. data/lib/killbill/plugin.rb +59 -26
  16. data/lib/killbill/rake_task.rb +27 -8
  17. data/lib/killbill/response/payment_method_response.rb +31 -0
  18. data/lib/killbill/response/payment_method_response_internal.rb +20 -0
  19. data/lib/killbill/response/payment_response.rb +22 -0
  20. data/lib/killbill/response/payment_status.rb +10 -0
  21. data/lib/killbill/response/refund_response.rb +23 -0
  22. data/spec/killbill/base_plugin_spec.rb +7 -0
  23. data/spec/killbill/config_test.ru +7 -0
  24. data/spec/killbill/jpayment_spec.rb +121 -0
  25. data/spec/killbill/jresponse/jconverter_spec.rb +208 -0
  26. data/spec/killbill/jresponse/jpayment_method_response_internal_spec.rb +34 -0
  27. data/spec/killbill/jresponse/jpayment_method_response_spec.rb +53 -0
  28. data/spec/killbill/jresponse/jpayment_response_spec.rb +41 -0
  29. data/spec/killbill/jresponse/jrefund_response_spec.rb +41 -0
  30. data/spec/killbill/killbill_integration_spec.rb +4 -5
  31. data/spec/killbill/payment_plugin_spec.rb +7 -3
  32. data/spec/killbill/payment_test.rb +88 -0
  33. data/spec/killbill/rack_handler_spec.rb +16 -0
  34. data/spec/spec_helper.rb +5 -0
  35. metadata +79 -16
data/Jarfile CHANGED
@@ -1,2 +1,3 @@
1
- jar 'com.ning.billing:killbill-api', '0.1.48'
2
- jar 'com.ning.billing:killbill-util:tests', '0.1.48'
1
+ jar 'com.ning.billing:killbill-api', '0.1.56-SNAPSHOT'
2
+ jar 'com.ning.billing:killbill-util:tests', '0.1.56-SNAPSHOT'
3
+ jar 'javax.servlet:javax.servlet-api', '3.0.1'
data/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ [![Build Status](https://travis-ci.org/killbill/killbill-plugin-framework-ruby.png)](https://travis-ci.org/killbill/killbill-plugin-framework-ruby)
2
+ [![Code Climate](https://codeclimate.com/github/killbill/killbill-plugin-framework-ruby.png)](https://codeclimate.com/github/killbill/killbill-plugin-framework-ruby)
3
+
1
4
  killbill-plugin-framework-ruby
2
5
  ==============================
3
6
 
@@ -8,6 +11,8 @@ There are various types of plugins one can write for Killbill:
8
11
  1. notifications plugins, which listen to external bus events and can react to it
9
12
  2. payment plugins, which are used to issue payments against a payment gateway
10
13
 
14
+ Both types of plugin can interact with Killbill directly via killbill-library APIs and expose HTTP endpoints.
15
+
11
16
  How to write a Notification plugin
12
17
  ----------------------------------
13
18
 
@@ -88,6 +93,22 @@ Make sure to create the corresponding killbill.properties file:
88
93
  mainClass=MyPaymentPlugin
89
94
  pluginType=PAYMENT
90
95
 
96
+ How to expose HTTP endpoints
97
+ ----------------------------
98
+
99
+ Killbill exports a Rack handler that interfaces directly with the container in which killbill-server runs (e.g. Jetty).
100
+
101
+ This basically means that Killbill will understand native Rack config.ru files placed in the root of your plugin, e.g. (using Sinatra):
102
+
103
+ require 'sinatra'
104
+
105
+ get "/plugins/myPlugin/ping" do
106
+ status 200
107
+ "pong"
108
+ end
109
+ run Sinatra::Application
110
+
111
+
91
112
  Rake tasks
92
113
  ----------
93
114
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.0.1
data/killbill.gemspec CHANGED
@@ -23,10 +23,12 @@ Gem::Specification.new do |s|
23
23
  s.rdoc_options << '--exclude' << '.'
24
24
 
25
25
  s.add_development_dependency 'jbundler', '~> 0.4.1'
26
+ s.add_development_dependency 'rack', '>= 1.5.2'
26
27
  s.add_development_dependency 'rake', '>= 0.8.7'
27
28
  s.add_development_dependency 'rspec', '~> 2.12.0'
29
+ s.add_development_dependency 'sinatra', '~> 1.3.4'
28
30
 
29
- s.requirements << "jar 'com.ning.billing:killbill-api', '0.1.48'"
31
+ s.requirements << "jar 'com.ning.billing:killbill-api', '0.1.56-SNAPSHOT'"
30
32
  # For testing only
31
- s.requirements << "jar 'com.ning.billing:killbill-util:tests', '0.1.48'"
33
+ s.requirements << "jar 'com.ning.billing:killbill-util:tests', '0.1.56-SNAPSHOT'"
32
34
  end
data/lib/killbill.rb CHANGED
@@ -17,7 +17,6 @@ KILLBILL_APIS = %w(
17
17
  com.ning.billing.invoice.api.InvoiceMigrationApi
18
18
  com.ning.billing.invoice.api.InvoicePaymentApi
19
19
  com.ning.billing.invoice.api.InvoiceUserApi
20
- com.ning.billing.meter.api.MeterUserApi
21
20
  com.ning.billing.overdue.OverdueUserApi
22
21
  com.ning.billing.payment.api.PaymentApi
23
22
  com.ning.billing.tenant.api.TenantUserApi
@@ -35,12 +34,12 @@ rescue NameError
35
34
  begin
36
35
  require 'jbundler'
37
36
  KILLBILL_APIS.each { |api| java_import api }
38
- warn 'Using JBundler to load killbill-api. This should only happen in development mode!'
39
- warn "Classpath (see .jbundler/classpath.rb):\n\t#{JBUNDLER_CLASSPATH.join("\n\t")}"
37
+ warn 'Using JBundler to load killbill-api (see .jbundler/classpath.rb). This should only happen in development mode!'
40
38
  rescue LoadError => e
41
- warn 'Unable to load killbill-api and couldn\'t find JBundler. For development purposes, make sure to run: `bundle install && jbundle install\' from the killbill gem source tree'
39
+ warn 'Unable to load killbill-api. For development purposes, use JBundler (create the following Jarfile: http://git.io/eobYXA and run: `bundle install && jbundle install\')'
42
40
  end
43
41
  end
44
42
 
43
+ require 'killbill/http_servlet'
45
44
  require 'killbill/notification'
46
45
  require 'killbill/payment'
@@ -0,0 +1,99 @@
1
+ gem 'rack'
2
+ require 'rack'
3
+ require 'rack/rewindable_input'
4
+
5
+ require 'singleton'
6
+
7
+ module Killbill
8
+ module Plugin
9
+ java_package 'com.ning.billing.osgi.api.http'
10
+ class RackHandler < Java::javax.servlet.http.HttpServlet
11
+ include Singleton
12
+
13
+ def configure(logger, config_ru)
14
+ @logger = logger
15
+ @app = Rack::Builder.parse_file(config_ru).first
16
+ end
17
+
18
+ def configured?
19
+ !@app.nil?
20
+ end
21
+
22
+ java_signature 'void service(HttpServletRequest, HttpServletResponse)'
23
+ def service(servlet_request, servlet_response)
24
+ input = Rack::RewindableInput.new(servlet_request.input_stream.to_io)
25
+ scheme = servlet_request.scheme
26
+ method = servlet_request.method
27
+ request_uri = servlet_request.request_uri
28
+ query_string = servlet_request.query_string
29
+ server_name = servlet_request.server_name
30
+ server_port = servlet_request.server_port
31
+ content_type = servlet_request.content_type
32
+ content_length = servlet_request.content_length
33
+
34
+ headers = {}
35
+ servlet_request.header_names.reject { |name| name =~ /^Content-(Type|Length)$/i }.each do |name|
36
+ headers[name] = servlet_request.get_headers(name).to_a
37
+ end
38
+
39
+ response_status, response_headers, response_body = rack_service(request_uri, method, query_string, input, scheme, server_name, server_port, content_type, content_length, headers)
40
+
41
+ # Set status
42
+ servlet_response.status = response_status
43
+
44
+ # Set headers
45
+ response_headers.each do |header_name, header_value|
46
+ case header_name
47
+ when /^Content-Type$/i
48
+ servlet_response.content_type = header_value.to_s
49
+ when /^Content-Length$/i
50
+ servlet_response.content_length = header_value.to_i
51
+ else
52
+ servlet_response.add_header(header_name.to_s, header_value.to_s)
53
+ end
54
+ end
55
+
56
+ # Write output
57
+ response_stream = servlet_response.output_stream
58
+ response_body.each { |part| response_stream.write(part.to_java_bytes) }
59
+ response_stream.flush rescue nil
60
+ ensure
61
+ response_body.close if response_body.respond_to? :close
62
+ end
63
+
64
+ def rack_service(request_uri = '/', method = 'GET', query_string = '', input = '', scheme = 'http', server_name = 'localhost', server_port = 4567, content_type = 'text/plain', content_length = 0, headers = {})
65
+ rack_env = {
66
+ 'rack.version' => Rack::VERSION,
67
+ 'rack.multithread' => true,
68
+ 'rack.multiprocess' => false,
69
+ 'rack.input' => input,
70
+ # TODO
71
+ 'rack.errors' => java::lang::System::err.to_io,
72
+ 'rack.logger' => java::lang::System::out.to_io,
73
+ 'rack.url_scheme' => scheme,
74
+ 'REQUEST_METHOD' => method,
75
+ 'SCRIPT_NAME' => '',
76
+ 'PATH_INFO' => request_uri,
77
+ 'QUERY_STRING' => (query_string || ""),
78
+ 'SERVER_NAME' => server_name,
79
+ 'SERVER_PORT' => server_port.to_s
80
+ }
81
+
82
+ rack_env['CONTENT_TYPE'] = content_type unless content_type.nil?
83
+ rack_env['CONTENT_LENGTH'] = content_length unless content_length.nil?
84
+ headers.each do |name, values|
85
+ rack_env["HTTP_#{name.to_s.upcase.gsub(/-/,'_')}"] = values.join(',')
86
+ end
87
+
88
+ @app.call(rack_env)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # Fix bug in JRuby's handling of gems in jars (JRUBY-3986)
95
+ class File
96
+ def self.mtime(path)
97
+ stat(path).mtime
98
+ end
99
+ end
@@ -0,0 +1,152 @@
1
+ require 'singleton'
2
+
3
+ require 'killbill/plugin'
4
+ require 'killbill/jresponse/jpayment_response'
5
+ require 'killbill/jresponse/jrefund_response'
6
+ require 'killbill/jresponse/jpayment_method_response'
7
+ require 'killbill/jresponse/jpayment_method_response_internal'
8
+
9
+
10
+ module Killbill
11
+ module Plugin
12
+
13
+ java_package 'com.ning.billing.payment.plugin.api'
14
+ class JPayment
15
+
16
+ include Java::com.ning.billing.payment.plugin.api.PaymentPluginApi
17
+
18
+ attr_reader :real_payment
19
+
20
+ def initialize(real_class_name, services = {})
21
+ real_payment_class = class_from_string(real_class_name)
22
+ @real_payment = real_payment_class.new(services)
23
+ end
24
+
25
+ # TODO STEPH decide what to do the getName()
26
+ java_signature 'java.lang.String getName()'
27
+ def get_name
28
+ end
29
+
30
+ java_signature 'Java::com.ning.billing.payment.plugin.api.PaymentInfoPlugin processPayment(java.util.UUID, java.util.UUID, java.lang.BigDecimal, Java::com.ning.billing.util.callcontext.CallContext)'
31
+ def charge(*args)
32
+ do_call_handle_exception(__method__, *args) do |res|
33
+ return JPaymentResponse.new(res)
34
+ end
35
+ end
36
+
37
+ java_signature 'Java::com.ning.billing.payment.plugin.api.PaymentInfoPlugin getPaymentInfo(java.util.UUID, Java::com.ning.billing.util.callcontext.TenantContext)'
38
+ def get_payment_info(*args)
39
+ do_call_handle_exception(__method__, *args) do |res|
40
+ return JPaymentResponse.new(res)
41
+ end
42
+ end
43
+
44
+ java_signature 'Java::com.ning.billing.payment.plugin.api.RefundInfoPlugin processRefund(java.util.UUID, java.lang.BigDecimal, Java::com.ning.billing.util.callcontext.CallContext)'
45
+ def refund(*args)
46
+ do_call_handle_exception(__method__, *args) do |res|
47
+ return JRefundResponse.new(res)
48
+ end
49
+ end
50
+
51
+ java_signature 'void addPaymentMethod(java.util.UUID, java.util.UUID, Java::com.ning.billing.payment.api.PaymentMethodPlugin, Java::boolean, Java::com.ning.billing.util.callcontext.CallContext)'
52
+ def add_payment_method(*args)
53
+ do_call_handle_exception(__method__, *args) do |res|
54
+ return nil
55
+ end
56
+ end
57
+
58
+ java_signature 'void deletePaymentMethod(java.util.UUID, Java::com.ning.billing.util.callcontext.CallContext)'
59
+ def delete_payment_method(*args)
60
+ do_call_handle_exception(__method__, *args) do |res|
61
+ return nil
62
+ end
63
+ end
64
+
65
+ java_signature 'Java::com.ning.billing.payment.api.PaymentMethodPlugin getPaymentMethodDetail(java.util.UUID, java.util.UUID, Java::com.ning.billing.util.callcontext.TenantContext)'
66
+ def get_payment_method_detail(*args)
67
+ do_call_handle_exception(__method__, *args) do |res|
68
+ return JPaymentMethodResponse.new(res)
69
+ end
70
+ end
71
+
72
+ java_signature 'void setDefaultPaymentMethod(java.util.UUID kbPaymentMethodId, Java::com.ning.billing.util.callcontext.CallContext)'
73
+ def set_default_payment_method(*args)
74
+ do_call_handle_exception(__method__, *args) do |res|
75
+ return nil
76
+ end
77
+ end
78
+
79
+ java_signature 'java.util.List getPaymentMethods(java.util.UUID, Java::boolean refreshFromGateway, Java::com.ning.billing.util.callcontext.CallContext)'
80
+ def get_payment_methods(*args)
81
+ do_call_handle_exception(__method__, *args) do |res|
82
+ array_res = java.util.ArrayList.new
83
+ res.each do |el|
84
+ array_res.add(JPaymentMethodResponseInternal.new(el))
85
+ end
86
+ return array_res
87
+ end
88
+ end
89
+
90
+ java_signature 'void resetPaymentMethods(java.util.List)'
91
+ def reset_payment_methods(*args)
92
+ do_call_handle_exception(__method__, *args) do |res|
93
+ return nil
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def do_call_handle_exception(method_name, *args)
100
+ begin
101
+ rargs = convert_args(method_name, args)
102
+ res = @real_payment.send(method_name, *rargs)
103
+ yield(res)
104
+ rescue Exception => e
105
+ wrap_and_throw_exception(method_name, e)
106
+ end
107
+ end
108
+
109
+ def wrap_and_throw_exception(api, e)
110
+ raise Java::com.ning.billing.payment.plugin.api.PaymentPluginApiException.new("#{api} failure", e.message)
111
+ end
112
+
113
+ def class_from_string(str)
114
+ str.split('::').inject(Kernel) do |mod, class_name|
115
+ mod.const_get(class_name)
116
+ end
117
+ end
118
+
119
+ def convert_args(api, args)
120
+ args.collect! do |a|
121
+ if a.nil?
122
+ nil
123
+ elsif a.java_kind_of? java.util.UUID
124
+ JConverter.from_uuid(a)
125
+ elsif a.java_kind_of? java.math.BigDecimal
126
+ JConverter.from_big_decimal(a)
127
+ elsif a.java_kind_of? Java::com.ning.billing.payment.api.PaymentMethodPlugin
128
+ JConverter.from_payment_method_plugin(a)
129
+ elsif ((a.java_kind_of? Java::boolean) || (a.java_kind_of? java.lang.Boolean))
130
+ JConverter.from_boolean(a)
131
+ elsif a.java_kind_of? java.util.List
132
+ result = Array.new
133
+ if a.size > 0
134
+ first_element = a.get(0)
135
+ if first_element.java_kind_of? Java::com.ning.billing.payment.plugin.api.PaymentMethodInfoPlugin
136
+ a.each do |el|
137
+ result << JConverter.from_payment_method_info_plugin(el)
138
+ end
139
+ else
140
+ raise Java::com.ning.billing.payment.plugin.api.PaymentPluginApiException.new("#{api} failure", "Unexpected parameter type #{first_element.class} for list")
141
+ end
142
+ end
143
+ result
144
+ else
145
+ raise Java::com.ning.billing.payment.plugin.api.PaymentPluginApiException.new("#{api} failure", "Unexpected parameter type #{a.class}")
146
+ end
147
+ end
148
+ end
149
+
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,100 @@
1
+
2
+ require 'date'
3
+ require 'killbill/response/payment_status'
4
+
5
+ module Killbill
6
+ module Plugin
7
+
8
+ class JConverter
9
+
10
+ class << self
11
+
12
+ #
13
+ # Convert from ruby -> java
14
+ #
15
+ def to_uuid(uuid)
16
+ uuid.nil? ? nil : java.util.UUID.fromString(uuid.to_s)
17
+ end
18
+
19
+ def to_joda_date_time(time)
20
+ time.nil? ? nil : org.joda.time.DateTime.new(time.to_s)
21
+ end
22
+
23
+ def to_string(str)
24
+ str.nil? ? nil : java.lang.String.new(str)
25
+ end
26
+
27
+ def to_payment_plugin_status(status)
28
+ if status == PaymentStatus::SUCCESS
29
+ Java::com.ning.billing.payment.plugin.api.PaymentInfoPlugin::PaymentPluginStatus::PROCESSED
30
+ elsif status == PaymentStatus::ERROR
31
+ Java::com.ning.billing.payment.plugin.api.PaymentInfoPlugin::PaymentPluginStatus::ERROR
32
+ else
33
+ Java::com.ning.billing.payment.plugin.api.PaymentInfoPlugin::PaymentPluginStatus::UNDEFINED
34
+ end
35
+ end
36
+
37
+ def to_big_decimal(amount_in_cents)
38
+ amount_in_cents.nil? ? java.math.BigDecimal::ZERO : java.math.BigDecimal.new('%.2f' % (amount_in_cents.to_i/100.0))
39
+ end
40
+
41
+ def to_boolean(b)
42
+ java.lang.Boolean.new(b)
43
+ end
44
+
45
+
46
+ #
47
+ # Convert from java -> ruby
48
+ #
49
+ def from_uuid(uuid)
50
+ uuid.nil? ? nil : uuid.to_s
51
+ end
52
+
53
+ def from_joda_date_time(joda_time)
54
+ if joda_time.nil?
55
+ return nil
56
+ end
57
+
58
+ fmt = org.joda.time.format.ISODateTimeFormat.date_time
59
+ str = fmt.print(joda_time);
60
+ DateTime.iso8601(str)
61
+ end
62
+
63
+ def from_string(str)
64
+ str.nil? ? nil : str.to_s
65
+ end
66
+
67
+ def from_payment_plugin_status(status)
68
+ if status == Java::com.ning.billing.payment.plugin.api.PaymentInfoPlugin::PaymentPluginStatus::PROCESSED
69
+ PaymentStatus::SUCCESS
70
+ elsif status == Java::com.ning.billing.payment.plugin.api.PaymentInfoPlugin::PaymentPluginStatus::ERROR
71
+ PaymentStatus::ERROR
72
+ else
73
+ PaymentStatus::UNDEFINED
74
+ end
75
+ end
76
+
77
+ def from_big_decimal(big_decimal)
78
+ big_decimal.nil? ? 0 : big_decimal.multiply(java.math.BigDecimal.valueOf(100)).to_s.to_i
79
+ end
80
+
81
+ def from_boolean(b)
82
+ if b.nil?
83
+ return false
84
+ end
85
+
86
+ b_value = (b.java_kind_of? java.lang.Boolean) ? b.boolean_value : b
87
+ return b_value ? true : false
88
+ end
89
+
90
+ def from_payment_method_plugin(payment_method_plugin)
91
+ JPaymentMethodResponse.to_payment_method_response(payment_method_plugin)
92
+ end
93
+
94
+ def from_payment_method_info_plugin(payment_method_info_plugin)
95
+ JPaymentMethodResponseInternal.to_payment_method_response_internal(payment_method_info_plugin)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,89 @@
1
+ require 'killbill/response/payment_method_response'
2
+ require 'killbill/jresponse/jconverter'
3
+
4
+ module Killbill
5
+ module Plugin
6
+
7
+ class JPaymentMethodProperty < Java::com.ning.billing.payment.api.PaymentMethodPlugin::PaymentMethodKVInfo
8
+
9
+ class << self
10
+ def create_from(payment_method_prop)
11
+
12
+ key = JConverter.to_string(payment_method_prop.key)
13
+ value = JConverter.to_string(payment_method_prop.value)
14
+ is_updatable = JConverter.to_boolean(payment_method_prop.is_updatable)
15
+ Java::com.ning.billing.payment.api.PaymentMethodPlugin::PaymentMethodKVInfo.new(key, value, is_updatable)
16
+ end
17
+
18
+ def to_payment_method_property(jpayment_method_prop)
19
+ key = JConverter.from_string(jpayment_method_prop.get_key)
20
+ value = JConverter.from_string(jpayment_method_prop.get_value)
21
+ is_updatable = JConverter.from_boolean(jpayment_method_prop.get_is_updatable)
22
+ PaymentMethodProperty.new(key, value, is_updatable)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ java_package 'com.ning.billing.payment.api'
30
+ class JPaymentMethodResponse
31
+
32
+ include Java::com.ning.billing.payment.api.PaymentMethodPlugin
33
+
34
+ attr_reader :external_payment_method_id,
35
+ :is_default,
36
+ :properties
37
+
38
+ class << self
39
+ def to_payment_method_response(jpayment_method_response)
40
+ props = Array.new
41
+ jpayment_method_response.get_properties.each do |p|
42
+ props << JPaymentMethodProperty.to_payment_method_property(p)
43
+ end
44
+ pmid = JConverter.from_string(jpayment_method_response.get_external_payment_method_id)
45
+ default = JConverter.from_boolean(jpayment_method_response.is_default_payment_method)
46
+ PaymentMethodResponse.new(pmid, default, props)
47
+ end
48
+
49
+ end
50
+
51
+ def initialize(payment_method_response)
52
+ @external_payment_method_id = JConverter.to_string(payment_method_response.external_payment_method_id)
53
+ @is_default = JConverter.to_boolean(payment_method_response.is_default)
54
+ @properties = java.util.ArrayList.new
55
+ payment_method_response.properties.each do |p|
56
+ jp = JPaymentMethodProperty.create_from(p)
57
+ @properties.add(jp)
58
+ end
59
+ end
60
+
61
+ java_signature 'java.lang.String getExternalPaymentMethodId()'
62
+ def get_external_payment_method_id
63
+ @external_payment_method_id
64
+ end
65
+
66
+ java_signature 'java.lang.Boolean isDefaultPaymentMethod()'
67
+ def is_default_payment_method
68
+ @is_default
69
+ end
70
+
71
+ java_signature 'java.util.List getProperties()'
72
+ def get_properties
73
+ @properties
74
+ end
75
+
76
+ java_signature 'java.lang.String getValueString(java.lang.String)'
77
+ def get_value_string(key)
78
+ @properties.each do |p|
79
+ if p.key == key
80
+ return p
81
+ end
82
+ end
83
+ nil
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end