cmis_active 0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/AUTHORS +9 -0
- data/LICENSE +26 -0
- data/README.md +55 -0
- data/Rakefile +34 -0
- data/TODO +7 -0
- data/VERSION.yml +5 -0
- data/active_cmis.gemspec +79 -0
- data/lib/active_cmis.rb +30 -0
- data/lib/active_cmis/acl.rb +181 -0
- data/lib/active_cmis/acl_entry.rb +26 -0
- data/lib/active_cmis/active_cmis.rb +87 -0
- data/lib/active_cmis/atomic_types.rb +245 -0
- data/lib/active_cmis/attribute_prefix.rb +35 -0
- data/lib/active_cmis/collection.rb +206 -0
- data/lib/active_cmis/document.rb +356 -0
- data/lib/active_cmis/exceptions.rb +82 -0
- data/lib/active_cmis/folder.rb +36 -0
- data/lib/active_cmis/internal/caching.rb +86 -0
- data/lib/active_cmis/internal/connection.rb +241 -0
- data/lib/active_cmis/internal/utils.rb +82 -0
- data/lib/active_cmis/ns.rb +18 -0
- data/lib/active_cmis/object.rb +563 -0
- data/lib/active_cmis/policy.rb +13 -0
- data/lib/active_cmis/property_definition.rb +179 -0
- data/lib/active_cmis/query_result.rb +40 -0
- data/lib/active_cmis/rel.rb +17 -0
- data/lib/active_cmis/relationship.rb +49 -0
- data/lib/active_cmis/rendition.rb +86 -0
- data/lib/active_cmis/repository.rb +327 -0
- data/lib/active_cmis/server.rb +121 -0
- data/lib/active_cmis/type.rb +200 -0
- data/lib/active_cmis/version.rb +10 -0
- metadata +132 -0
@@ -0,0 +1,356 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
class Document < ActiveCMIS::Object
|
3
|
+
# Returns an ActiveCMIS::Rendition to the content stream or nil if there is none
|
4
|
+
# @return [Rendition]
|
5
|
+
def content_stream
|
6
|
+
if content = data.xpath("at:content", NS::COMBINED).first
|
7
|
+
if content['src']
|
8
|
+
ActiveCMIS::Rendition.new(repository, self, "href" => content['src'], "type" => content["type"])
|
9
|
+
else
|
10
|
+
if content['type'] =~ /\+xml$/
|
11
|
+
content_data = content.to_xml # FIXME: this may not preserve whitespace
|
12
|
+
else
|
13
|
+
content_data = data.unpack("m*").first
|
14
|
+
end
|
15
|
+
ActiveCMIS::Rendition.new(repository, self, "data" => content_data, "type" => content["type"])
|
16
|
+
end
|
17
|
+
elsif content = data.xpath("cra:content", NS::COMBINED).first
|
18
|
+
content.children.each do |node|
|
19
|
+
next unless node.namespace and node.namespace.href == NS::CMIS_REST
|
20
|
+
content_data = node.text if node.name == "base64"
|
21
|
+
content_type = node.text if node.name == "mediaType"
|
22
|
+
end
|
23
|
+
data = content_data.unpack("m*").first
|
24
|
+
ActiveCMIS::Rendition.new(repository, self, "data" => content_data, "type" => content_type)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
cache :content_stream
|
28
|
+
|
29
|
+
# Will reload if renditionFilter was not set or cmis:none, but not in other circumstances
|
30
|
+
# @return [Array<Rendition>]
|
31
|
+
def renditions
|
32
|
+
filter = used_parameters["renditionFilter"]
|
33
|
+
if filter.nil? || filter == "cmis:none"
|
34
|
+
reload
|
35
|
+
end
|
36
|
+
|
37
|
+
links = data.xpath("at:link[@rel = 'alternate']", NS::COMBINED)
|
38
|
+
links.map do |link|
|
39
|
+
ActiveCMIS::Rendition.new(repository, self, link)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
cache :renditions
|
43
|
+
|
44
|
+
# Sets new content to be uploaded, does not alter values you will get from content_stream (for the moment)
|
45
|
+
# @param [Hash] options A hash containing exactly one of :file or :data
|
46
|
+
# @option options [String] :file The name of a file to upload
|
47
|
+
# @option options [#read] :data Data you want to upload (if #length is defined it should give the total length that can be read)
|
48
|
+
# @option options [Boolean] :overwrite (true) Whether the contents should be overwritten (ignored in case of checkin)
|
49
|
+
# @option options [String] :mime_type
|
50
|
+
#
|
51
|
+
# @return [void]
|
52
|
+
def set_content_stream(options)
|
53
|
+
if key.nil?
|
54
|
+
if self.class.content_stream_allowed == "notallowed"
|
55
|
+
raise Error::StreamNotSupported.new("Documents of this type can't have content")
|
56
|
+
end
|
57
|
+
else
|
58
|
+
updatability = repository.capabilities["ContentStreamUpdatability"]
|
59
|
+
if updatability == "none"
|
60
|
+
raise Error::NotSupported.new("Content can't be updated in this repository")
|
61
|
+
elsif updatability == "pwconly" && !working_copy?
|
62
|
+
raise Error::Constraint.new("Content can only be updated for working copies in this repository")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
@updated_contents = options
|
66
|
+
end
|
67
|
+
|
68
|
+
# Sets versioning state.
|
69
|
+
# Only possible on new documents, or PWC documents
|
70
|
+
#
|
71
|
+
# @param ["major", "minor", "none", "checkedout"] state A string of the desired versioning state
|
72
|
+
#
|
73
|
+
# @return [void]
|
74
|
+
def set_versioning_state(state)
|
75
|
+
raise Error::Constraint.new("Can only set a different version state on PWC documents, or unsaved new documents") unless key.nil? || working_copy?
|
76
|
+
raise ArgumentError, "You must pass a String" unless state.is_a?(String)
|
77
|
+
if key.nil?
|
78
|
+
possible_values = %w[major minor none checkedout]
|
79
|
+
else
|
80
|
+
possible_values = %w[major minor]
|
81
|
+
end
|
82
|
+
raise ArgumentError, "Given state is invalid. Possible values are #{possible_values.join(", ")}" unless possible_values.include?(state)
|
83
|
+
|
84
|
+
@versioning_state = state
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns all documents in the version series of this document.
|
88
|
+
# Uses self to represent the version of this document
|
89
|
+
# @return [Collection<Document>, Array(self)]
|
90
|
+
def versions
|
91
|
+
link = data.xpath("at:link[@rel = 'version-history']/@href", NS::COMBINED)
|
92
|
+
if link = link.first
|
93
|
+
Collection.new(repository, link) # Problem: does not in fact use self
|
94
|
+
else
|
95
|
+
# The document is not versionable
|
96
|
+
[self]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
cache :versions
|
100
|
+
|
101
|
+
# Returns self if this is the latest version
|
102
|
+
# Note: There will allways be a latest version in a version series
|
103
|
+
# @return [Document]
|
104
|
+
def latest_version
|
105
|
+
link = data.xpath("at:link[@rel = 'current-version']/@href", NS::COMBINED)
|
106
|
+
if link.first
|
107
|
+
entry = conn.get_atom_entry(link.first.text)
|
108
|
+
self_or_new(entry)
|
109
|
+
else
|
110
|
+
# FIXME: should somehow return the current version even for opencmis
|
111
|
+
self
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns self if this is the working copy
|
116
|
+
# Returns nil if there is no working copy
|
117
|
+
# @return [Document]
|
118
|
+
def working_copy
|
119
|
+
link = data.xpath("at:link[@rel = 'working-copy']/@href", NS::COMBINED)
|
120
|
+
if link.first
|
121
|
+
entry = conn.get_atom_entry(link.first.text)
|
122
|
+
self_or_new(entry)
|
123
|
+
else
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def latest?
|
129
|
+
attributes["cmis:isLatestVersion"]
|
130
|
+
end
|
131
|
+
def major?
|
132
|
+
attributes["cmis:isMajorVersion"]
|
133
|
+
end
|
134
|
+
def latest_major?
|
135
|
+
attributes["cmis:isLatestMajorVersion"]
|
136
|
+
end
|
137
|
+
|
138
|
+
def working_copy?
|
139
|
+
return false if key.nil?
|
140
|
+
|
141
|
+
# NOTE: This may not be a sufficient condition, but according to the spec it should be
|
142
|
+
!data.xpath("at:link[@rel = 'via']", NS::COMBINED).empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns information about the checked out status of this document
|
146
|
+
#
|
147
|
+
# @return [Hash,nil] Keys are :by for the owner of the PWC and :id for the CMIS ID, both can be unset according to the spec
|
148
|
+
def version_series_checked_out
|
149
|
+
attributes = self.attributes
|
150
|
+
if attributes["cmis:isVersionSeriesCheckedOut"]
|
151
|
+
result = {}
|
152
|
+
if attributes.has_key? "cmis:versionSeriesCheckedOutBy"
|
153
|
+
result[:by] = attributes["cmis:versionSeriesCheckedOutBy"]
|
154
|
+
end
|
155
|
+
if attributes.has_key? "cmis:versionSeriesCheckedOutId"
|
156
|
+
result[:id] = attributes["cmis:versionSeriesCheckedOutId"]
|
157
|
+
end
|
158
|
+
result
|
159
|
+
else
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# The checkout operation results in a Private Working Copy
|
165
|
+
#
|
166
|
+
# Most properties should be the same as for the document that was checked out,
|
167
|
+
# certain properties may differ such as cmis:objectId and cmis:creationDate.
|
168
|
+
#
|
169
|
+
# The content stream of the PWC may be identical to that of the document
|
170
|
+
# that was checked out, or it may be unset.
|
171
|
+
# @return [Document] The checked out version of this document
|
172
|
+
def checkout
|
173
|
+
body = render_atom_entry(self.class.attributes.reject {|k,v| k != "cmis:objectId"})
|
174
|
+
|
175
|
+
response = conn.post_response(repository.checkedout.url, body)
|
176
|
+
if 200 <= response.code.to_i && response.code.to_i < 300
|
177
|
+
entry = Nokogiri::XML.parse(response.body, nil, nil, Nokogiri::XML::ParseOptions::STRICT).xpath("/at:entry", NS::COMBINED)
|
178
|
+
result = self_or_new(entry)
|
179
|
+
if result.working_copy? # Work around a bug in OpenCMIS where result returned is the version checked out not the PWC
|
180
|
+
result
|
181
|
+
else
|
182
|
+
conn.logger.warn "Repository did not return working copy for checkout operation"
|
183
|
+
result.working_copy
|
184
|
+
end
|
185
|
+
else
|
186
|
+
raise response.body
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# This action may not be permitted (query allowable_actions to see whether it is permitted)
|
191
|
+
# @return [void]
|
192
|
+
def cancel_checkout
|
193
|
+
if !self.class.versionable
|
194
|
+
raise Error::Constraint.new("Object is not versionable, can't cancel checkout")
|
195
|
+
elsif working_copy?
|
196
|
+
conn.delete(self_link)
|
197
|
+
else
|
198
|
+
raise Error::InvalidArgument.new("Not a working copy")
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# @overload checkin(major, comment = "", updated_attributes = {})
|
203
|
+
# Check in the private working copy. Raises a constraint error when the
|
204
|
+
# document is not a working copy
|
205
|
+
#
|
206
|
+
# @param [Boolean] major Whether the document will be checked in as a major version
|
207
|
+
# @param [String] comment An optional comment to use when creating the new version
|
208
|
+
# @param [Hash] updated_attributes A hash with updated attributes
|
209
|
+
# @return [Document] The final version that results from the checkin
|
210
|
+
# @overload checkin(comment = "", updated_attributes = {})
|
211
|
+
# Check in the private working copy. Raises a constraint error when the
|
212
|
+
# document is not a working copy
|
213
|
+
# The version will be the version set by set_versioning_state (default is
|
214
|
+
# a major version)
|
215
|
+
#
|
216
|
+
# @param [String] comment An optional comment to use when creating the new version
|
217
|
+
# @param [Hash] updated_attributes A hash with updated attributes
|
218
|
+
# @return [Document] The final version that results from the checkin
|
219
|
+
# @overload checkin(updated_attributes = {})
|
220
|
+
# Check in the private working copy with an empty message.
|
221
|
+
# Raises a constraint error when the document is not a working copy
|
222
|
+
# The version will be the version set by set_versioning_state (default is
|
223
|
+
# a major version)
|
224
|
+
#
|
225
|
+
# @param [Hash] updated_attributes A hash with updated attributes
|
226
|
+
# @return [Document] The final version that results from the checkin
|
227
|
+
def checkin(*options)
|
228
|
+
if options.length > 3
|
229
|
+
raise ArgumentError, "Too many arguments for checkin"
|
230
|
+
else
|
231
|
+
major, comment, updated_attributes = *options
|
232
|
+
if TrueClass === major or FalseClass === major
|
233
|
+
# Nothing changes: only defaults need to be filled in (if necessary)
|
234
|
+
elsif String === major
|
235
|
+
updated_attributes = comment
|
236
|
+
comment = major
|
237
|
+
# major will be true if: @versioning_state == "major", or if it's not set
|
238
|
+
major = @versioning_state != "minor"
|
239
|
+
elsif Hash === major
|
240
|
+
updated_attributes = major
|
241
|
+
major = @versioning_state != "minor"
|
242
|
+
end
|
243
|
+
comment ||= ""
|
244
|
+
updated_attributes ||= {}
|
245
|
+
end
|
246
|
+
|
247
|
+
if working_copy?
|
248
|
+
update(updated_attributes)
|
249
|
+
result = self
|
250
|
+
updated_aspects([true, major, comment]).each do |hash|
|
251
|
+
result = result.send(hash[:message], *hash[:parameters])
|
252
|
+
end
|
253
|
+
@versioning_state = nil
|
254
|
+
result
|
255
|
+
else
|
256
|
+
raise Error::Constraint, "Not a working copy"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# @return [void]
|
261
|
+
def reload
|
262
|
+
@updated_contents = nil
|
263
|
+
super
|
264
|
+
end
|
265
|
+
|
266
|
+
private
|
267
|
+
attr_reader :updated_contents
|
268
|
+
|
269
|
+
def render_atom_entry(properties = self.class.attributes, attributes = self.attributes, options = {})
|
270
|
+
super(properties, attributes, options) do |entry|
|
271
|
+
if updated_contents && (options[:create] || options[:checkin])
|
272
|
+
entry["cra"].content do
|
273
|
+
entry["cra"].mediatype(updated_contents[:mime_type] || "application/binary")
|
274
|
+
data = updated_contents[:data] || File.read(updated_contents[:file])
|
275
|
+
entry["cra"].base64 [data].pack("m")
|
276
|
+
end
|
277
|
+
end
|
278
|
+
if block_given?
|
279
|
+
yield(entry)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
def updated_aspects(checkin = nil)
|
286
|
+
if working_copy? && !(checkin || repository.pwc_updatable?)
|
287
|
+
raise Error::NotSupported.new("Updating a PWC without checking in is not supported by repository")
|
288
|
+
end
|
289
|
+
unless working_copy? || checkin.nil?
|
290
|
+
raise Error::NotSupported.new("Can't check in when not checked out")
|
291
|
+
end
|
292
|
+
|
293
|
+
result = super
|
294
|
+
|
295
|
+
if !key.nil? && !updated_contents.nil?
|
296
|
+
if checkin
|
297
|
+
# Update the content stream before checking in
|
298
|
+
result.unshift(:message => :save_content_stream, :parameters => [updated_contents])
|
299
|
+
else
|
300
|
+
# TODO: check that the content stream is updateable
|
301
|
+
result << {:message => :save_content_stream, :parameters => [updated_contents]}
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
result
|
306
|
+
end
|
307
|
+
|
308
|
+
def self_or_new(entry)
|
309
|
+
if entry.nil?
|
310
|
+
nil
|
311
|
+
elsif entry.xpath("cra:object/c:properties/c:propertyId[@propertyDefinitionId = 'cmis:objectId']/c:value", NS::COMBINED).text == id
|
312
|
+
reload
|
313
|
+
@data = entry
|
314
|
+
self
|
315
|
+
else
|
316
|
+
ActiveCMIS::Object.from_atom_entry(repository, entry)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def create_url
|
321
|
+
if f = parent_folders.first
|
322
|
+
url = f.items.url
|
323
|
+
Internal::Utils.append_parameters(url, "versioningState" => (self.class.versionable ? (@versioning_state || "major") : "none"))
|
324
|
+
else
|
325
|
+
raise Error::NotSupported.new("Creating an unfiled document is not supported by CMIS")
|
326
|
+
# Can't create documents that are unfiled, CMIS does not support it (note this means exceptions should not actually be NotSupported)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def save_content_stream(stream)
|
331
|
+
# Should never occur (is private method)
|
332
|
+
raise "no content to save" if stream.nil?
|
333
|
+
|
334
|
+
# put to link with rel 'edit-media' if it's there
|
335
|
+
# NOTE: cmislib uses the src link of atom:content instead, that might be correct
|
336
|
+
edit_links = Internal::Utils.extract_links(data, "edit-media")
|
337
|
+
if edit_links.length == 1
|
338
|
+
link = edit_links.first
|
339
|
+
elsif edit_links.empty?
|
340
|
+
raise Error.new("No edit-media link, can't save content")
|
341
|
+
else
|
342
|
+
raise Error.new("Too many edit-media links, don't know how to choose")
|
343
|
+
end
|
344
|
+
data = stream[:data] || File.open(stream[:file])
|
345
|
+
content_type = stream[:mime_type] || "application/octet-stream"
|
346
|
+
|
347
|
+
if stream.has_key?(:overwrite)
|
348
|
+
url = Internal::Utils.append_parameters(link, "overwrite" => stream[:overwrite])
|
349
|
+
else
|
350
|
+
url = link
|
351
|
+
end
|
352
|
+
conn.put(url, data, "Content-Type" => content_type)
|
353
|
+
self
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
# The base class for all CMIS exceptions,
|
3
|
+
# HTTP communication errors and the like are not catched by this
|
4
|
+
class Error < StandardError
|
5
|
+
# === Cause
|
6
|
+
# One or more of the input parameters to the service method is missing or invalid
|
7
|
+
class InvalidArgument < Error; end
|
8
|
+
|
9
|
+
# === Cause
|
10
|
+
# The service call has specified an object that does not exist in the Repository
|
11
|
+
class ObjectNotFound < Error; end
|
12
|
+
|
13
|
+
# === Cause
|
14
|
+
# The service method invoked requires an optional capability not supported by the repository
|
15
|
+
class NotSupported < Error; end
|
16
|
+
|
17
|
+
# === Cause
|
18
|
+
# The caller of the service method does not have sufficient permissions to perform the operation
|
19
|
+
class PermissionDenied < Error; end
|
20
|
+
|
21
|
+
# === Cause
|
22
|
+
# Any cause not expressible by another CMIS exception
|
23
|
+
class Runtime < Error; end
|
24
|
+
|
25
|
+
# === Intent
|
26
|
+
# The operation violates a Repository- or Object-level constraint defined in the CMIS domain model
|
27
|
+
#
|
28
|
+
# === Methods
|
29
|
+
# see the CMIS specification
|
30
|
+
class Constraint < Error; end
|
31
|
+
# === Intent
|
32
|
+
# The operation attempts to set the content stream for a Document
|
33
|
+
# that already has a content stream without explicitly specifying the
|
34
|
+
# "overwriteFlag" parameter
|
35
|
+
#
|
36
|
+
# === Methods
|
37
|
+
# see the CMIS specification
|
38
|
+
class ContentAlreadyExists < Error; end
|
39
|
+
# === Intent
|
40
|
+
# The property filter or rendition filter input to the operation is not valid
|
41
|
+
#
|
42
|
+
# === Methods
|
43
|
+
# see the CMIS specification
|
44
|
+
class FilterNotValid < Error; end
|
45
|
+
# === Intent
|
46
|
+
# The repository is not able to store the object that the user is creating/updating due to a name constraint violation
|
47
|
+
#
|
48
|
+
# === Methods
|
49
|
+
# see the CMIS specification
|
50
|
+
class NameConstraintViolation < Error; end
|
51
|
+
# === Intent
|
52
|
+
# The repository is not able to store the object that the user is creating/updating due to an internal storage problam
|
53
|
+
#
|
54
|
+
# === Methods
|
55
|
+
# see the CMIS specification
|
56
|
+
class Storage < Error; end
|
57
|
+
# === Intent
|
58
|
+
#
|
59
|
+
#
|
60
|
+
# === Methods
|
61
|
+
# see the CMIS specification
|
62
|
+
class StreamNotSupported < Error; end
|
63
|
+
# === Intent
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# === Methods
|
67
|
+
# see the CMIS specification
|
68
|
+
class UpdateConflict < Error; end
|
69
|
+
# === Intent
|
70
|
+
#
|
71
|
+
#
|
72
|
+
# === Methods
|
73
|
+
# see the CMIS specification
|
74
|
+
class Versioning < Error; end
|
75
|
+
end
|
76
|
+
|
77
|
+
class HTTPError < StandardError
|
78
|
+
class ServerError < HTTPError; end
|
79
|
+
class ClientError < HTTPError; end
|
80
|
+
class AuthenticationError < HTTPError; end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
class Folder < ActiveCMIS::Object
|
3
|
+
# Returns a collection of all items contained in this folder (1 level deep)
|
4
|
+
# @return [Collection<Document,Folder,Policy>]
|
5
|
+
def items
|
6
|
+
item_feed = Internal::Utils.extract_links(data, 'down', 'application/atom+xml','type' => 'feed')
|
7
|
+
raise "No child feed link for folder" if item_feed.empty?
|
8
|
+
Collection.new(repository, item_feed.first)
|
9
|
+
end
|
10
|
+
cache :items
|
11
|
+
|
12
|
+
def allowed_object_types
|
13
|
+
if attributes["cmis:allowedChildObjectTypeIds"].empty?
|
14
|
+
repository.types.select { |type| type.fileable }
|
15
|
+
else
|
16
|
+
# TODO: it is repository specific if subtypes of the allowed types MAY be filed (line 976)
|
17
|
+
#
|
18
|
+
# There is as far as I can see no other mention of this possibility in the spec, no way to
|
19
|
+
# check if this is so for any specific repository. In addition there is in a few places a
|
20
|
+
# requirement that an error is thrown if the cmis:objectTypeId is not in the list of allowed
|
21
|
+
# values. So for now this is not supported at all.
|
22
|
+
attributes["cmis:allowedChildObjectTypeIds"].map { |type_id| repository.type_by_id(type_id) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
cache :allowed_object_types
|
26
|
+
|
27
|
+
private
|
28
|
+
def create_url
|
29
|
+
if f = parent_folders.first
|
30
|
+
f.items.url
|
31
|
+
else
|
32
|
+
raise "Not possible to create folder without parent folder"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|