stormpath-sdk 1.6.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -0
- data/CHANGES.md +11 -0
- data/README.md +23 -25
- data/lib/stormpath-sdk.rb +11 -2
- data/lib/stormpath-sdk/api_key.rb +0 -1
- data/lib/stormpath-sdk/auth/basic_authenticator.rb +7 -7
- data/lib/stormpath-sdk/auth/basic_login_attempt.rb +7 -11
- data/lib/stormpath-sdk/auth/create_factor.rb +1 -1
- data/lib/stormpath-sdk/auth/register_service_provider.rb +41 -0
- data/lib/stormpath-sdk/auth/username_password_request.rb +3 -5
- data/lib/stormpath-sdk/cache/cache.rb +3 -3
- data/lib/stormpath-sdk/cache/cache_entry.rb +2 -2
- data/lib/stormpath-sdk/cache/cache_manager.rb +3 -4
- data/lib/stormpath-sdk/cache/cache_stats.rb +1 -3
- data/lib/stormpath-sdk/cache/disabled_cache_store.rb +5 -8
- data/lib/stormpath-sdk/cache/memory_store.rb +1 -1
- data/lib/stormpath-sdk/cache/redis_store.rb +4 -4
- data/lib/stormpath-sdk/client.rb +35 -33
- data/lib/stormpath-sdk/data_store.rb +278 -257
- data/lib/stormpath-sdk/error.rb +18 -7
- data/lib/stormpath-sdk/http/authc/sauthc1_signer.rb +76 -82
- data/lib/stormpath-sdk/http/http_client_request_executor.rb +10 -8
- data/lib/stormpath-sdk/http/response.rb +5 -7
- data/lib/stormpath-sdk/id_site/id_site_result.rb +5 -6
- data/lib/stormpath-sdk/oauth/access_token_authentication_result.rb +5 -9
- data/lib/stormpath-sdk/oauth/authenticator.rb +2 -2
- data/lib/stormpath-sdk/oauth/error.rb +4 -4
- data/lib/stormpath-sdk/oauth/id_site_grant_request.rb +1 -1
- data/lib/stormpath-sdk/oauth/password_grant_request.rb +1 -1
- data/lib/stormpath-sdk/oauth/refresh_grant_request.rb +2 -2
- data/lib/stormpath-sdk/oauth/stormpath_grant_request.rb +2 -2
- data/lib/stormpath-sdk/provider/account_access.rb +0 -2
- data/lib/stormpath-sdk/provider/account_result.rb +1 -2
- data/lib/stormpath-sdk/provider/facebook/facebook_provider.rb +6 -2
- data/lib/stormpath-sdk/provider/facebook/facebook_provider_data.rb +7 -3
- data/lib/stormpath-sdk/provider/github/github_provider.rb +6 -2
- data/lib/stormpath-sdk/provider/github/github_provider_data.rb +6 -2
- data/lib/stormpath-sdk/provider/google/google_provider.rb +7 -3
- data/lib/stormpath-sdk/provider/google/google_provider_data.rb +6 -2
- data/lib/stormpath-sdk/provider/linkedin/linkedin_provider.rb +6 -2
- data/lib/stormpath-sdk/provider/linkedin/linkedin_provider_data.rb +6 -2
- data/lib/stormpath-sdk/provider/provider.rb +8 -4
- data/lib/stormpath-sdk/provider/provider_data.rb +6 -2
- data/lib/stormpath-sdk/provider/saml/saml_provider.rb +10 -4
- data/lib/stormpath-sdk/provider/saml/saml_provider_data.rb +6 -3
- data/lib/stormpath-sdk/provider/stormpath/stormpath_provider.rb +6 -2
- data/lib/stormpath-sdk/provider/stormpath/stormpath_provider_data.rb +6 -2
- data/lib/stormpath-sdk/provider/twitter/twitter_provider.rb +6 -2
- data/lib/stormpath-sdk/provider/twitter/twitter_provider_data.rb +6 -2
- data/lib/stormpath-sdk/resource/account.rb +46 -40
- data/lib/stormpath-sdk/resource/account_link.rb +9 -5
- data/lib/stormpath-sdk/resource/account_linking_policy.rb +8 -4
- data/lib/stormpath-sdk/resource/account_membership.rb +1 -1
- data/lib/stormpath-sdk/resource/account_overrides.rb +20 -16
- data/lib/stormpath-sdk/resource/account_store.rb +15 -11
- data/lib/stormpath-sdk/resource/account_store_mapping.rb +14 -13
- data/lib/stormpath-sdk/resource/application.rb +147 -136
- data/lib/stormpath-sdk/resource/application_web_config.rb +11 -7
- data/lib/stormpath-sdk/resource/associations.rb +36 -43
- data/lib/stormpath-sdk/resource/attribute_statement_mapping_rules.rb +8 -0
- data/lib/stormpath-sdk/resource/base.rb +201 -200
- data/lib/stormpath-sdk/resource/challenge.rb +12 -8
- data/lib/stormpath-sdk/resource/collection.rb +77 -76
- data/lib/stormpath-sdk/resource/custom_data.rb +60 -61
- data/lib/stormpath-sdk/resource/custom_data_hash_methods.rb +28 -25
- data/lib/stormpath-sdk/resource/custom_data_storage.rb +18 -16
- data/lib/stormpath-sdk/resource/directory.rb +37 -60
- data/lib/stormpath-sdk/resource/email_verification_token.rb +7 -3
- data/lib/stormpath-sdk/resource/error.rb +8 -4
- data/lib/stormpath-sdk/resource/expansion.rb +22 -20
- data/lib/stormpath-sdk/resource/factor.rb +12 -8
- data/lib/stormpath-sdk/resource/field.rb +8 -4
- data/lib/stormpath-sdk/resource/group.rb +21 -16
- data/lib/stormpath-sdk/resource/group_membership.rb +7 -5
- data/lib/stormpath-sdk/resource/instance.rb +10 -6
- data/lib/stormpath-sdk/resource/linked_account.rb +7 -3
- data/lib/stormpath-sdk/resource/oauth_policy.rb +7 -3
- data/lib/stormpath-sdk/resource/organization.rb +14 -10
- data/lib/stormpath-sdk/resource/organization_account_store_mapping.rb +8 -4
- data/lib/stormpath-sdk/resource/password_reset_token.rb +9 -5
- data/lib/stormpath-sdk/resource/phone.rb +8 -4
- data/lib/stormpath-sdk/resource/registered_saml_service_provider.rb +8 -0
- data/lib/stormpath-sdk/resource/saml_identity_provider.rb +14 -0
- data/lib/stormpath-sdk/resource/saml_identity_provider_metadata.rb +9 -0
- data/lib/stormpath-sdk/resource/saml_policy.rb +10 -0
- data/lib/stormpath-sdk/resource/saml_service_provider.rb +7 -0
- data/lib/stormpath-sdk/{provider/saml/saml_mapping_rules.rb → resource/saml_service_provider_metadata.rb} +6 -5
- data/lib/stormpath-sdk/resource/saml_service_provider_registration.rb +11 -0
- data/lib/stormpath-sdk/resource/schema.rb +8 -4
- data/lib/stormpath-sdk/resource/tenant.rb +11 -8
- data/lib/stormpath-sdk/resource/user_info_mapping_rules.rb +7 -3
- data/lib/stormpath-sdk/resource/utils.rb +7 -10
- data/lib/stormpath-sdk/resource/verification_email.rb +7 -3
- data/lib/stormpath-sdk/resource/x_509_certificate.rb +7 -0
- data/lib/stormpath-sdk/util/assert.rb +1 -3
- data/lib/stormpath-sdk/version.rb +2 -2
- data/spec/auth/basic_authenticator_spec.rb +28 -24
- data/spec/auth/register_service_provider_spec.rb +68 -0
- data/spec/auth/sauthc1_signer_spec.rb +8 -4
- data/spec/cache/cache_entry_spec.rb +28 -29
- data/spec/cache/cache_spec.rb +9 -9
- data/spec/cache/cache_stats_spec.rb +1 -1
- data/spec/client_spec.rb +63 -63
- data/spec/data_store_spec.rb +23 -14
- data/spec/oauth/access_token_authentication_result_spec.rb +8 -2
- data/spec/provider/account_resolver_spec.rb +6 -4
- data/spec/provider/provider_spec.rb +6 -6
- data/spec/resource/account_creation_policy_spec.rb +1 -1
- data/spec/resource/account_link_spec.rb +7 -15
- data/spec/resource/account_spec.rb +17 -17
- data/spec/resource/account_store_mapping_spec.rb +16 -22
- data/spec/resource/account_store_spec.rb +3 -3
- data/spec/resource/application_spec.rb +324 -330
- data/spec/resource/base_spec.rb +7 -31
- data/spec/resource/collection_spec.rb +63 -114
- data/spec/resource/custom_data_spec.rb +1 -1
- data/spec/resource/directory_spec.rb +91 -87
- data/spec/resource/expansion_spec.rb +10 -10
- data/spec/resource/factor_spec.rb +1 -1
- data/spec/resource/group_spec.rb +1 -1
- data/spec/resource/linked_account_spec.rb +7 -7
- data/spec/resource/organization_spec.rb +12 -11
- data/spec/resource/phone_spec.rb +1 -1
- data/spec/resource/registered_saml_service_provider_spec.rb +35 -0
- data/spec/resource/saml_identity_provider_metadata_spec.rb +27 -0
- data/spec/resource/saml_identity_provider_spec.rb +94 -0
- data/spec/resource/saml_policy_spec.rb +27 -0
- data/spec/resource/saml_service_provider_registration_spec.rb +58 -0
- data/spec/resource/saml_service_provider_spec.rb +19 -0
- data/spec/resource/status_spec.rb +4 -3
- data/spec/resource/tenant_spec.rb +4 -6
- data/spec/spec_helper.rb +1 -1
- data/spec/support/custom_data_save_period.rb +4 -0
- data/spec/support/custom_data_storage_behavior.rb +7 -8
- data/spec/support/mocked_provider_accounts.rb +101 -101
- data/spec/support/mocked_saml_responses.rb +130 -0
- data/spec/support/resource_factory.rb +4 -4
- data/spec/support/resource_helpers.rb +10 -4
- data/spec/support/resource_matchers.rb +4 -4
- data/spec/support/test_request_executor.rb +2 -2
- metadata +21 -8
- data/lib/stormpath-sdk/provider/saml/saml_provider_metadata.rb +0 -19
- data/spec/fixtures/response/create_saml_directory.json +0 -26
- data/spec/fixtures/response/create_saml_directory_mapping_rules.json +0 -12
- data/spec/fixtures/response/get_saml_directory_provider.json +0 -16
- data/spec/fixtures/response/get_saml_directory_provider_metadata.json +0 -12
@@ -13,257 +13,258 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
16
|
+
module Stormpath
|
17
|
+
module Resource
|
18
|
+
class Base
|
19
|
+
include Stormpath::Resource::Utils
|
20
|
+
include Stormpath::Resource::Associations
|
21
|
+
|
22
|
+
HREF_PROP_NAME = 'href'.freeze
|
23
|
+
DEFAULT_SERVER_HOST = Stormpath::DataStore::DEFAULT_SERVER_HOST
|
24
|
+
attr_reader :client, :properties, :dirty_properties
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def prop_reader(*args)
|
28
|
+
args.each do |name|
|
29
|
+
define_method(name) do
|
30
|
+
get_property name
|
31
|
+
end
|
32
|
+
end
|
29
33
|
end
|
30
|
-
end
|
31
|
-
end
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
def prop_writer(*args)
|
36
|
+
args.each do |name|
|
37
|
+
define_method("#{name}=") do |value|
|
38
|
+
set_property name, value
|
39
|
+
end
|
40
|
+
end
|
37
41
|
end
|
38
|
-
end
|
39
|
-
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
def prop_accessor(*args)
|
44
|
+
args.each do |name|
|
45
|
+
prop_reader name
|
46
|
+
prop_writer name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def prop_non_printable(*args)
|
51
|
+
@non_printable_properties ||= []
|
52
|
+
args.each do |name|
|
53
|
+
property_name = name.to_s.camelize :lower
|
54
|
+
@non_printable_properties << property_name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def non_printable_properties
|
59
|
+
@non_printable_properties ||= []
|
60
|
+
Array.new(@non_printable_properties)
|
61
|
+
end
|
45
62
|
end
|
46
|
-
end
|
47
63
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
64
|
+
def initialize(properties_or_url, client = nil, query = nil)
|
65
|
+
properties = case properties_or_url
|
66
|
+
when String
|
67
|
+
{ HREF_PROP_NAME => properties_or_url }
|
68
|
+
when Hash
|
69
|
+
properties_or_url
|
70
|
+
else
|
71
|
+
{}
|
72
|
+
end
|
73
|
+
|
74
|
+
@client = client
|
75
|
+
@query = query
|
76
|
+
@read_lock = Mutex.new
|
77
|
+
@write_lock = Mutex.new
|
78
|
+
@properties = {}
|
79
|
+
@dirty_properties = {}
|
80
|
+
@deleted_properties = []
|
81
|
+
set_properties properties
|
53
82
|
end
|
54
|
-
end
|
55
83
|
|
56
|
-
|
57
|
-
|
58
|
-
Array.new @non_printable_properties
|
59
|
-
end
|
60
|
-
end
|
84
|
+
def new?
|
85
|
+
prop = read_property HREF_PROP_NAME
|
61
86
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
else
|
69
|
-
{}
|
70
|
-
end
|
71
|
-
|
72
|
-
@client = client
|
73
|
-
@query = query
|
74
|
-
@read_lock = Mutex.new
|
75
|
-
@write_lock = Mutex.new
|
76
|
-
@properties = Hash.new
|
77
|
-
@dirty_properties = Hash.new
|
78
|
-
@deleted_properties = Array.new
|
79
|
-
set_properties properties
|
80
|
-
end
|
87
|
+
if prop.nil?
|
88
|
+
true
|
89
|
+
else
|
90
|
+
prop.respond_to?('empty') && prop.empty?
|
91
|
+
end
|
92
|
+
end
|
81
93
|
|
82
|
-
|
83
|
-
|
94
|
+
def href
|
95
|
+
get_property HREF_PROP_NAME
|
96
|
+
end
|
84
97
|
|
85
|
-
|
86
|
-
|
87
|
-
else
|
88
|
-
prop.respond_to? 'empty' and prop.empty?
|
89
|
-
end
|
90
|
-
end
|
98
|
+
def get_property_names
|
99
|
+
@read_lock.lock
|
91
100
|
|
92
|
-
|
93
|
-
|
94
|
-
|
101
|
+
begin
|
102
|
+
@properties.keys
|
103
|
+
ensure
|
104
|
+
@read_lock.unlock
|
105
|
+
end
|
106
|
+
end
|
95
107
|
|
96
|
-
|
97
|
-
|
108
|
+
def get_dirty_property_names
|
109
|
+
@read_lock.lock
|
98
110
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
111
|
+
begin
|
112
|
+
@dirty_properties.keys
|
113
|
+
ensure
|
114
|
+
@read_lock.unlock
|
115
|
+
end
|
116
|
+
end
|
105
117
|
|
106
|
-
|
107
|
-
|
118
|
+
def set_properties(properties)
|
119
|
+
@write_lock.lock
|
108
120
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
121
|
+
begin
|
122
|
+
@properties.clear
|
123
|
+
@dirty_properties.clear
|
124
|
+
@dirty = false
|
125
|
+
|
126
|
+
if properties
|
127
|
+
@properties = deep_sanitize properties
|
128
|
+
@dirty_properties = @properties if new?
|
129
|
+
# Don't consider this resource materialized if it is only a reference. A reference is any object that
|
130
|
+
# has only one 'href' property.
|
131
|
+
href_only = ((@properties.size == 1) && @properties.key?(HREF_PROP_NAME))
|
132
|
+
@materialized = !href_only
|
133
|
+
else
|
134
|
+
@materialized = false
|
135
|
+
end
|
115
136
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
begin
|
120
|
-
@properties.clear
|
121
|
-
@dirty_properties.clear
|
122
|
-
@dirty = false
|
123
|
-
|
124
|
-
if properties
|
125
|
-
@properties = deep_sanitize properties
|
126
|
-
@dirty_properties = @properties if new?
|
127
|
-
# Don't consider this resource materialized if it is only a reference. A reference is any object that
|
128
|
-
# has only one 'href' property.
|
129
|
-
href_only = (@properties.size == 1 and @properties.has_key? HREF_PROP_NAME)
|
130
|
-
@materialized = !href_only
|
131
|
-
else
|
132
|
-
@materialized = false
|
137
|
+
ensure
|
138
|
+
@write_lock.unlock
|
139
|
+
end
|
133
140
|
end
|
134
141
|
|
135
|
-
|
136
|
-
|
137
|
-
end
|
138
|
-
end
|
142
|
+
def get_property(name, options = {})
|
143
|
+
property_name = name.to_s
|
139
144
|
|
140
|
-
|
141
|
-
|
145
|
+
unless options[:ignore_camelcasing] == true
|
146
|
+
property_name = property_name.camelize(:lower)
|
147
|
+
end
|
142
148
|
|
143
|
-
|
144
|
-
|
145
|
-
|
149
|
+
if HREF_PROP_NAME != property_name
|
150
|
+
# not the href/id, must be a property that requires materialization:
|
151
|
+
unless new? || materialized?
|
146
152
|
|
147
|
-
|
148
|
-
|
149
|
-
|
153
|
+
# only materialize if the property hasn't been set previously (no need to execute a server
|
154
|
+
# request since we have the most recent value already):
|
155
|
+
present = false
|
150
156
|
|
151
|
-
|
152
|
-
|
153
|
-
|
157
|
+
@read_lock.lock
|
158
|
+
begin
|
159
|
+
present = @dirty_properties.key? property_name
|
160
|
+
ensure
|
161
|
+
@read_lock.unlock
|
162
|
+
end
|
154
163
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
164
|
+
unless present
|
165
|
+
# exhausted present properties - we require a server call:
|
166
|
+
materialize
|
167
|
+
end
|
168
|
+
end
|
160
169
|
end
|
161
170
|
|
162
|
-
|
163
|
-
# exhausted present properties - we require a server call:
|
164
|
-
materialize
|
165
|
-
end
|
171
|
+
read_property property_name
|
166
172
|
end
|
167
|
-
end
|
168
173
|
|
169
|
-
|
170
|
-
|
174
|
+
def set_property(name, value, options = {})
|
175
|
+
property_name = name.to_s
|
171
176
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
unless options[:ignore_camelcasing] == true
|
176
|
-
property_name = property_name.camelize(:lower)
|
177
|
-
end
|
177
|
+
unless options[:ignore_camelcasing] == true
|
178
|
+
property_name = property_name.camelize(:lower)
|
179
|
+
end
|
178
180
|
|
179
|
-
|
181
|
+
@write_lock.lock
|
180
182
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
183
|
+
begin
|
184
|
+
@properties.store property_name, value
|
185
|
+
@dirty_properties.store property_name, value
|
186
|
+
@dirty = true
|
187
|
+
ensure
|
188
|
+
@write_lock.unlock
|
189
|
+
end
|
190
|
+
end
|
189
191
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
192
|
+
def ==(other)
|
193
|
+
if other.is_a?(self.class)
|
194
|
+
href == other.href
|
195
|
+
else
|
196
|
+
super
|
197
|
+
end
|
198
|
+
end
|
197
199
|
|
198
|
-
|
200
|
+
private
|
199
201
|
|
200
|
-
|
201
|
-
|
202
|
-
|
202
|
+
def data_store
|
203
|
+
client.data_store
|
204
|
+
end
|
203
205
|
|
204
|
-
|
205
|
-
|
206
|
-
|
206
|
+
def materialized?
|
207
|
+
@materialized
|
208
|
+
end
|
207
209
|
|
208
|
-
|
209
|
-
|
210
|
+
def materialize
|
211
|
+
clazz = self.class
|
210
212
|
|
211
|
-
|
213
|
+
@write_lock.lock
|
212
214
|
|
213
|
-
|
214
|
-
|
215
|
-
|
215
|
+
begin
|
216
|
+
resource = data_store.get_resource href, clazz, @query
|
217
|
+
@properties.replace resource.properties
|
216
218
|
|
217
|
-
|
218
|
-
|
219
|
+
# retain dirty properties:
|
220
|
+
@properties.merge! @dirty_properties
|
219
221
|
|
220
|
-
|
221
|
-
|
222
|
-
|
222
|
+
@materialized = true
|
223
|
+
ensure
|
224
|
+
@write_lock.unlock
|
225
|
+
end
|
223
226
|
end
|
224
|
-
end
|
225
227
|
|
226
|
-
|
227
|
-
|
228
|
-
|
228
|
+
def printable_property?(property_name)
|
229
|
+
!self.class.non_printable_properties.include? property_name
|
230
|
+
end
|
229
231
|
|
230
|
-
|
231
|
-
|
232
|
-
props[HREF_PROP_NAME]
|
232
|
+
def get_href_from_hash(props)
|
233
|
+
props[HREF_PROP_NAME] if props && props.is_a?(Hash)
|
233
234
|
end
|
234
|
-
end
|
235
235
|
|
236
|
-
|
237
|
-
|
236
|
+
def read_property(name)
|
237
|
+
@read_lock.lock
|
238
238
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
239
|
+
begin
|
240
|
+
@properties[name]
|
241
|
+
ensure
|
242
|
+
@read_lock.unlock
|
243
|
+
end
|
243
244
|
end
|
244
|
-
end
|
245
245
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
246
|
+
def sanitize(properties)
|
247
|
+
{}.tap do |sanitized_properties|
|
248
|
+
properties.map do |key, value|
|
249
|
+
property_name = key.to_s.camelize :lower
|
250
|
+
sanitized_properties[property_name] =
|
251
|
+
if (value.is_a?(Hash) || value.is_a?(Stormpath::Resource::Base)) && (property_name != 'customData')
|
252
|
+
deep_sanitize value
|
253
|
+
else
|
254
|
+
value
|
255
|
+
end
|
256
|
+
end
|
256
257
|
end
|
257
258
|
end
|
258
|
-
end
|
259
259
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
260
|
+
def deep_sanitize(hash_or_resource)
|
261
|
+
case hash_or_resource
|
262
|
+
when Stormpath::Resource::Base
|
263
|
+
deep_sanitize hash_or_resource.properties
|
264
|
+
when Hash
|
265
|
+
sanitize hash_or_resource
|
266
|
+
end
|
266
267
|
end
|
267
268
|
end
|
268
|
-
|
269
|
+
end
|
269
270
|
end
|