active_cmis 0.1.0
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.
- data/.gitignore +3 -0
- data/LICENSE +26 -0
- data/README.md +30 -0
- data/Rakefile +36 -0
- data/TODO +8 -0
- data/VERSION +1 -0
- data/lib/active_cmis.rb +27 -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 +74 -0
- data/lib/active_cmis/atomic_types.rb +232 -0
- data/lib/active_cmis/attribute_prefix.rb +35 -0
- data/lib/active_cmis/collection.rb +175 -0
- data/lib/active_cmis/document.rb +314 -0
- data/lib/active_cmis/exceptions.rb +82 -0
- data/lib/active_cmis/folder.rb +21 -0
- data/lib/active_cmis/internal/caching.rb +86 -0
- data/lib/active_cmis/internal/connection.rb +171 -0
- data/lib/active_cmis/internal/utils.rb +69 -0
- data/lib/active_cmis/ns.rb +18 -0
- data/lib/active_cmis/object.rb +543 -0
- data/lib/active_cmis/policy.rb +13 -0
- data/lib/active_cmis/property_definition.rb +175 -0
- data/lib/active_cmis/rel.rb +17 -0
- data/lib/active_cmis/relationship.rb +47 -0
- data/lib/active_cmis/rendition.rb +65 -0
- data/lib/active_cmis/repository.rb +258 -0
- data/lib/active_cmis/server.rb +88 -0
- data/lib/active_cmis/type.rb +193 -0
- metadata +110 -0
@@ -0,0 +1,543 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
class Object
|
3
|
+
include Internal::Caching
|
4
|
+
|
5
|
+
# The repository that contains this object
|
6
|
+
# @return [Repository]
|
7
|
+
attr_reader :repository
|
8
|
+
|
9
|
+
# The cmis:objectId of the object, or nil if the document does not yet exist in the repository
|
10
|
+
# @return [String,nil]
|
11
|
+
attr_reader :key
|
12
|
+
alias id key
|
13
|
+
|
14
|
+
# Creates a representation of an CMIS Object in the repository
|
15
|
+
#
|
16
|
+
# Not meant for direct use, use {Repository#object_by_id} instead. To create a new object use the new method on the type that you want the new object to have.
|
17
|
+
#
|
18
|
+
# @param [Repository] repository The repository this object belongs to
|
19
|
+
# @param [Nokogiri::XML::Node,nil] data The preparsed XML Atom Entry or nil if the object does not yet exist
|
20
|
+
# @param [Hash] parameters A list of parameters used to get the Atom Entry
|
21
|
+
def initialize(repository, data, parameters)
|
22
|
+
@repository = repository
|
23
|
+
@data = data
|
24
|
+
|
25
|
+
@updated_attributes = []
|
26
|
+
|
27
|
+
if @data.nil?
|
28
|
+
# Creating a new type from scratch
|
29
|
+
raise Error::Constraint.new("This type is not creatable") unless self.class.creatable
|
30
|
+
@key = parameters["id"]
|
31
|
+
@allowable_actions = {}
|
32
|
+
@parent_folders = [] # start unlinked
|
33
|
+
else
|
34
|
+
@key = parameters["id"] || attribute('cmis:objectId')
|
35
|
+
@self_link = data.xpath("at:link[@rel = 'self']/@href", NS::COMBINED).first
|
36
|
+
@self_link = @self_link.text
|
37
|
+
end
|
38
|
+
@used_parameters = parameters
|
39
|
+
# FIXME: decide? parameters to use?? always same ? or parameter with reload ?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Via method missing attribute accessors and setters are provided for the CMIS attributes of an object.
|
43
|
+
# If attributes have a colon in their name you can access them by changing the colon in a dot
|
44
|
+
#
|
45
|
+
# @example Set an attribute named DateTimePropMV
|
46
|
+
# my_object.DateTimePropMV = Time.now #=> "Wed Apr 07 14:34:19 0200 2010"
|
47
|
+
# @example Read the attribute named DateTimePropMV
|
48
|
+
# my_object.DateTimePropMV #=> "Wed Apr 07 14:34:19 0200 2010"
|
49
|
+
# @example Get the cmis:name of an object
|
50
|
+
# my_object.cmis.name #=> "My object 25"
|
51
|
+
def method_missing(method, *parameters)
|
52
|
+
string = method.to_s
|
53
|
+
if string[-1] == ?=
|
54
|
+
assignment = true
|
55
|
+
string = string[0..-2]
|
56
|
+
end
|
57
|
+
if attributes.keys.include? string
|
58
|
+
if assignment
|
59
|
+
update(string => parameters.first)
|
60
|
+
else
|
61
|
+
attribute(string)
|
62
|
+
end
|
63
|
+
elsif self.class.attribute_prefixes.include? string
|
64
|
+
if assignment
|
65
|
+
raise NotImplementedError.new("Mass assignment not yet supported to prefix")
|
66
|
+
else
|
67
|
+
@attribute_prefix ||= {}
|
68
|
+
@attribute_prefix[method] ||= AttributePrefix.new(self, string)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [String]
|
76
|
+
def inspect
|
77
|
+
"#<#{self.class.inspect} @key=#{key}>"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Shorthand for the cmis:name of an object
|
81
|
+
# @return [String]
|
82
|
+
def name
|
83
|
+
attribute('cmis:name')
|
84
|
+
end
|
85
|
+
cache :name
|
86
|
+
|
87
|
+
# A list of all attributes that have changed locally
|
88
|
+
# @return [Array<String>]
|
89
|
+
attr_reader :updated_attributes
|
90
|
+
|
91
|
+
# Attribute getter for the CMIS attributes of an object
|
92
|
+
# @param [String] name The property id of the attribute
|
93
|
+
def attribute(name)
|
94
|
+
attributes[name]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Attribute getter for the CMIS attributes of an object
|
98
|
+
# @return [Hash{String => ::Object}] All attributes, the keys are the property ids of the attributes
|
99
|
+
def attributes
|
100
|
+
self.class.attributes.inject({}) do |hash, (key, attr)|
|
101
|
+
if data.nil?
|
102
|
+
if key == "cmis:objectTypeId"
|
103
|
+
hash[key] = self.class.id
|
104
|
+
else
|
105
|
+
hash[key] = nil
|
106
|
+
end
|
107
|
+
else
|
108
|
+
properties = data.xpath("cra:object/c:properties", NS::COMBINED)
|
109
|
+
values = attr.extract_property(properties)
|
110
|
+
hash[key] = if values.nil? || values.empty?
|
111
|
+
if attr.repeating
|
112
|
+
[]
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
elsif attr.repeating
|
117
|
+
values.map do |value|
|
118
|
+
attr.property_type.cmis2rb(value)
|
119
|
+
end
|
120
|
+
else
|
121
|
+
attr.property_type.cmis2rb(values.first)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
hash
|
125
|
+
end
|
126
|
+
end
|
127
|
+
cache :attributes
|
128
|
+
|
129
|
+
# Attribute setter for all CMIS attributes. This only updates this copy of the object.
|
130
|
+
# Use save to make these changes permanent and visible in the repositorhy.
|
131
|
+
# (use {#reload} after save on other instances of this document to reflect these changes)
|
132
|
+
#
|
133
|
+
# @param [{String => ::Object}] attributes A hash with new values for selected attributes
|
134
|
+
# @raise [Error::Constraint] if a readonly attribute is set
|
135
|
+
# @raise if a value can't be converted to the necessary type or falls outside the constraints
|
136
|
+
# @return [{String => ::Object}] The updated attributes hash
|
137
|
+
def update(attributes)
|
138
|
+
attributes.each do |key, value|
|
139
|
+
if (property = self.class.attributes[key.to_s]).nil?
|
140
|
+
raise Error::Constraint.new("You are trying to add an unknown attribute (#{key})")
|
141
|
+
else
|
142
|
+
property.validate_ruby_value(value)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
self.updated_attributes.concat(attributes.keys).uniq!
|
146
|
+
self.attributes.merge!(attributes)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Saves all changes to the object in the repository.
|
150
|
+
#
|
151
|
+
# *WARNING*: because of the way CMIS is constructed the save operation is not atomic if updates happen to different aspects of the object
|
152
|
+
# (parent folders, attributes, content stream, acl), we can't work around this because CMIS lacks transactions
|
153
|
+
# @return [Object]
|
154
|
+
def save
|
155
|
+
# FIXME: find a way to handle errors?
|
156
|
+
# FIXME: what if multiple objects are created in the course of a save operation?
|
157
|
+
result = self
|
158
|
+
updated_aspects.each do |hash|
|
159
|
+
result = result.send(hash[:message], *hash[:parameters])
|
160
|
+
end
|
161
|
+
result
|
162
|
+
end
|
163
|
+
|
164
|
+
# @return [Hash{String => Boolean,String}] A hash containing all actions allowed on this object for the current user
|
165
|
+
def allowable_actions
|
166
|
+
actions = {}
|
167
|
+
_allowable_actions.children.map do |node|
|
168
|
+
actions[node.name.sub("can", "")] = case t = node.text
|
169
|
+
when "true", "1"; true
|
170
|
+
when "false", "0"; false
|
171
|
+
else t
|
172
|
+
end
|
173
|
+
end
|
174
|
+
actions
|
175
|
+
end
|
176
|
+
cache :allowable_actions
|
177
|
+
|
178
|
+
# Returns all relationships where this object is the target
|
179
|
+
# @return [Collection]
|
180
|
+
def target_relations
|
181
|
+
query = "at:link[@rel = '#{Rel[repository.cmis_version][:relationships]}']/@href"
|
182
|
+
link = data.xpath(query, NS::COMBINED)
|
183
|
+
if link.length == 1
|
184
|
+
link = Internal::Utils.append_parameters(link.text, "relationshipDirection" => "target", "includeSubRelationshipTypes" => true)
|
185
|
+
Collection.new(repository, link)
|
186
|
+
else
|
187
|
+
raise "Expected exactly 1 relationships link for #{key}, got #{link.length}, are you sure this is a document/folder?"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
cache :target_relations
|
191
|
+
|
192
|
+
# Returns all relationships where this object is the source
|
193
|
+
# @return [Collection]
|
194
|
+
def source_relations
|
195
|
+
query = "at:link[@rel = '#{Rel[repository.cmis_version][:relationships]}']/@href"
|
196
|
+
link = data.xpath(query, NS::COMBINED)
|
197
|
+
if link.length == 1
|
198
|
+
link = Internal::Utils.append_parameters(link.text, "relationshipDirection" => "source", "includeSubRelationshipTypes" => true)
|
199
|
+
Collection.new(repository, link)
|
200
|
+
else
|
201
|
+
raise "Expected exactly 1 relationships link for #{key}, got #{link.length}, are you sure this is a document/folder?"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
cache :source_relations
|
205
|
+
|
206
|
+
# @return [Acl,nil] The ACL of the document, if there is any at all
|
207
|
+
def acl
|
208
|
+
if repository.acls_readable? && allowable_actions["GetACL"]
|
209
|
+
# FIXME: actual query should perhaps look at CMIS version before deciding which relation is applicable?
|
210
|
+
query = "at:link[@rel = '#{Rel[repository.cmis_version][:acl]}']/@href"
|
211
|
+
link = data.xpath(query, NS::COMBINED)
|
212
|
+
if link.length == 1
|
213
|
+
Acl.new(repository, self, link.first.text, data.xpath("cra:object/c:acl", NS::COMBINED))
|
214
|
+
else
|
215
|
+
raise "Expected exactly 1 acl for #{key}, got #{link.length}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Depending on the repository there can be more than 1 parent folder
|
221
|
+
# Always returns [] for relationships, policies may also return []
|
222
|
+
#
|
223
|
+
# @return [Array<Folder>,Collection] The parent folders in an array or a collection
|
224
|
+
def parent_folders
|
225
|
+
parent_feed = Internal::Utils.extract_links(data, 'up', 'application/atom+xml','type' => 'feed')
|
226
|
+
unless parent_feed.empty?
|
227
|
+
Collection.new(repository, parent_feed.first)
|
228
|
+
else
|
229
|
+
parent_entry = Internal::Utils.extract_links(data, 'up', 'application/atom+xml','type' => 'entries')
|
230
|
+
unless parent_entry.empty?
|
231
|
+
e = conn.get_atom_entry(parent_entry.first)
|
232
|
+
[ActiveCMIS::Object.from_atom_entry(repository, e)]
|
233
|
+
else
|
234
|
+
[]
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
cache :parent_folders
|
239
|
+
|
240
|
+
# Files an object in a folder, if the repository supports multi-filing this will be an additional folder, else it will replace the previous folder
|
241
|
+
#
|
242
|
+
# @param [Folder] folder The (replacement) folder
|
243
|
+
# @return [void]
|
244
|
+
def file(folder)
|
245
|
+
raise Error::Constraint.new("Filing not supported for objects of type: #{self.class.id}") unless self.class.fileable
|
246
|
+
@original_parent_folders ||= parent_folders.dup
|
247
|
+
if repository.capabilities["MultiFiling"]
|
248
|
+
@parent_folders << folder unless @parent_folders.detect {|f| f.id == folder.id }
|
249
|
+
else
|
250
|
+
@parent_folders = [folder]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Removes an object from a given folder or all folders. If the repository does not support unfiling this method throws an error if the document would have no folders left after unfiling.
|
255
|
+
#
|
256
|
+
# @param [Folder,nil] folder
|
257
|
+
# @return [void]
|
258
|
+
def unfile(folder = nil)
|
259
|
+
# Conundrum: should this throw exception if folder is not actually among parent_folders?
|
260
|
+
raise Error::Constraint.new("Filing not supported for objects of type: #{self.class.id}") unless self.class.fileable
|
261
|
+
@original_parent_folders ||= parent_folders.dup
|
262
|
+
if repository.capabilities["UnFiling"]
|
263
|
+
if folder.nil?
|
264
|
+
@parent_folders = []
|
265
|
+
else
|
266
|
+
@parent_folders.delete_if {|f| f.id == folder.id}
|
267
|
+
end
|
268
|
+
else
|
269
|
+
@parent_folders.delete_if {|f| f.id == folder.id}
|
270
|
+
if @parent_folders.empty?
|
271
|
+
@parent_folders = @original_parent_folders
|
272
|
+
@original_parent_folders = nil
|
273
|
+
raise Error::NotSupported.new("Unfiling not supported for this repository")
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Empties the locally cached and updated values, updated data is asked from the server the next time a value is requested.
|
279
|
+
# @raise [RuntimeError] if the object is not yet created on the server
|
280
|
+
# @return [void]
|
281
|
+
def reload
|
282
|
+
if @self_link.nil?
|
283
|
+
raise "Can't reload unsaved object"
|
284
|
+
else
|
285
|
+
__reload
|
286
|
+
@updated_attributes = []
|
287
|
+
@original_parent_folders = nil
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
# Internal value, not meant for common-day use
|
293
|
+
# @private
|
294
|
+
# @return [Hash]
|
295
|
+
attr_reader :used_parameters
|
296
|
+
|
297
|
+
def self_link(options = {})
|
298
|
+
url = @self_link
|
299
|
+
if options.empty?
|
300
|
+
url
|
301
|
+
else
|
302
|
+
Internal::Utils.append_parameters(url, options)
|
303
|
+
end
|
304
|
+
#repository.object_by_id_url(options.merge("id" => id))
|
305
|
+
end
|
306
|
+
|
307
|
+
def data
|
308
|
+
parameters = {"includeAllowableActions" => true, "renditionFilter" => "*", "includeACL" => true}
|
309
|
+
data = conn.get_atom_entry(self_link(parameters))
|
310
|
+
@used_parameters = parameters
|
311
|
+
data
|
312
|
+
end
|
313
|
+
cache :data
|
314
|
+
|
315
|
+
def conn
|
316
|
+
@repository.conn
|
317
|
+
end
|
318
|
+
|
319
|
+
def _allowable_actions
|
320
|
+
if actions = data.xpath('cra:object/c:allowableActions', NS::COMBINED).first
|
321
|
+
actions
|
322
|
+
else
|
323
|
+
links = data.xpath("at:link[@rel = '#{Rel[repository.cmis_version][:allowableactions]}']/@href", NS::COMBINED)
|
324
|
+
if link = links.first
|
325
|
+
conn.get_xml(link.text)
|
326
|
+
else
|
327
|
+
nil
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# @param properties a hash key/definition pairs of properties to be rendered (defaults to all attributes)
|
333
|
+
# @param attributes a hash key/value pairs used to determine the values rendered (defaults to self.attributes)
|
334
|
+
# @param options
|
335
|
+
def render_atom_entry(properties = self.class.attributes, attributes = self.attributes, options = {})
|
336
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
337
|
+
xml.entry(NS::COMBINED) do
|
338
|
+
xml.parent.namespace = xml.parent.namespace_definitions.detect {|ns| ns.prefix == "at"}
|
339
|
+
xml["at"].author do
|
340
|
+
xml["at"].name conn.user # FIXME: find reliable way to set author?
|
341
|
+
end
|
342
|
+
xml["cra"].object do
|
343
|
+
xml["c"].properties do
|
344
|
+
properties.each do |key, definition|
|
345
|
+
definition.render_property(xml, attributes[key])
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
builder.to_xml
|
352
|
+
end
|
353
|
+
|
354
|
+
# @private
|
355
|
+
attr_writer :updated_attributes
|
356
|
+
|
357
|
+
def updated_aspects(checkin = nil)
|
358
|
+
result = []
|
359
|
+
|
360
|
+
if key.nil?
|
361
|
+
result << {:message => :save_new_object, :parameters => []}
|
362
|
+
if parent_folders.length > 1
|
363
|
+
# We started from 0 folders, we already added the first when creating the document
|
364
|
+
|
365
|
+
# Note: to keep a save operation at least somewhat atomic this might be better done in save_new_object
|
366
|
+
result << {:message => :save_folders, :parameters => [parent_folders]}
|
367
|
+
end
|
368
|
+
else
|
369
|
+
if !updated_attributes.empty?
|
370
|
+
result << {:message => :save_attributes, :parameters => [updated_attributes, attributes, checkin]}
|
371
|
+
end
|
372
|
+
if @original_parent_folders
|
373
|
+
result << {:message => :save_folders, :parameters => [parent_folders, checkin && !updated_attributes]}
|
374
|
+
end
|
375
|
+
end
|
376
|
+
if acl && acl.updated # We need to be able to do this for newly created documents and merge the two
|
377
|
+
result << {:message => :save_acl, :parameters => [acl]}
|
378
|
+
end
|
379
|
+
|
380
|
+
if result.empty? && checkin
|
381
|
+
# NOTE: this needs some thinking through: in particular this may not work well if there would be an updated content stream
|
382
|
+
result << {:message => :save_attributes, :parameters => [[], [], checkin]}
|
383
|
+
end
|
384
|
+
|
385
|
+
result
|
386
|
+
end
|
387
|
+
|
388
|
+
def save_new_object
|
389
|
+
if self.class.required_attributes.any? {|a, _| attribute(a).nil? }
|
390
|
+
raise Error::InvalidArgument.new("Not all required attributes are filled in")
|
391
|
+
end
|
392
|
+
|
393
|
+
properties = self.class.attributes.reject do |key, definition|
|
394
|
+
!updated_attributes.include?(key) && !definition.required
|
395
|
+
end
|
396
|
+
body = render_atom_entry(properties, attributes, :create => true)
|
397
|
+
|
398
|
+
url = create_url
|
399
|
+
response = conn.post(create_url, body, "Content-Type" => "application/atom+xml;type=entry")
|
400
|
+
# XXX: Currently ignoring Location header in response
|
401
|
+
|
402
|
+
response_data = Nokogiri::XML::parse(response).xpath("at:entry", NS::COMBINED) # Assume that a response indicates success?
|
403
|
+
|
404
|
+
@self_link = response_data.xpath("at:link[@rel = 'self']/@href", NS::COMBINED).first
|
405
|
+
@self_link = @self_link.text
|
406
|
+
reload
|
407
|
+
@key = attribute("cmis:objectId")
|
408
|
+
|
409
|
+
self
|
410
|
+
end
|
411
|
+
|
412
|
+
def save_attributes(attributes, values, checkin = nil)
|
413
|
+
if attributes.empty? && checkin.nil?
|
414
|
+
raise "Error: saving attributes but nothing to do"
|
415
|
+
end
|
416
|
+
properties = self.class.attributes.reject {|key,_| !updated_attributes.include?(key)}
|
417
|
+
body = render_atom_entry(properties, values, :checkin => checkin)
|
418
|
+
|
419
|
+
if checkin.nil?
|
420
|
+
parameters = {}
|
421
|
+
else
|
422
|
+
checkin, major, comment = *checkin
|
423
|
+
parameters = {"checkin" => checkin}
|
424
|
+
if checkin
|
425
|
+
parameters.merge! "major" => !!major, "checkinComment" => Internal::Utils.escape_url_parameter(comment)
|
426
|
+
|
427
|
+
if properties.empty?
|
428
|
+
# The standard specifies that we can have an empty body here, that does not seem to be true for OpenCMIS
|
429
|
+
# body = ""
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# NOTE: Spec says Entity Tag should be used for changeTokens, that does not seem to work
|
435
|
+
if ct = attribute("cmis:changeToken")
|
436
|
+
parameters.merge! "changeToken" => Internal::Utils.escape_url_parameter(ct)
|
437
|
+
end
|
438
|
+
|
439
|
+
uri = self_link(parameters)
|
440
|
+
response = conn.put(uri, body)
|
441
|
+
|
442
|
+
data = Nokogiri::XML.parse(response).xpath("at:entry", NS::COMBINED)
|
443
|
+
if data.xpath("cra:object/c:properties/c:propertyId[@propertyDefinitionId = 'cmis:objectId']/c:value", NS::COMBINED).text == id
|
444
|
+
reload
|
445
|
+
@data = data
|
446
|
+
self
|
447
|
+
else
|
448
|
+
reload # Updated attributes should be forgotten here
|
449
|
+
ActiveCMIS::Object.from_atom_entry(repository, data)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def save_folders(requested_parent_folders, checkin = nil)
|
454
|
+
current = parent_folders.to_a
|
455
|
+
future = requested_parent_folders.to_a
|
456
|
+
|
457
|
+
common_folders = future.map {|f| f.id}.select {|id| current.any? {|f| f.id == id } }
|
458
|
+
|
459
|
+
added = future.select {|f1| current.all? {|f2| f1.id != f2.id } }
|
460
|
+
removed = current.select {|f1| future.all? {|f2| f1.id != f2.id } }
|
461
|
+
|
462
|
+
# NOTE: an absent atom:content is important here according to the spec, for the moment I did not suffer from this
|
463
|
+
body = render_atom_entry("cmis:objectId" => self.class.attributes["cmis:objectId"])
|
464
|
+
|
465
|
+
# Note: change token does not seem to matter here
|
466
|
+
# FIXME: currently we assume the data returned by post is not important, I'm not sure that this is always true
|
467
|
+
if added.empty?
|
468
|
+
removed.each do |folder|
|
469
|
+
url = repository.unfiled.url
|
470
|
+
url = Internal::Utils.append_parameters(url, "removeFrom" => Internal::Utils.escape_url_parameter(removed.id))
|
471
|
+
conn.post(url, body, "Content-Type" => "application/atom+xml;type=entry")
|
472
|
+
end
|
473
|
+
elsif removed.empty?
|
474
|
+
added.each do |folder|
|
475
|
+
conn.post(folder.items.url, body, "Content-Type" => "application/atom+xml;type=entry")
|
476
|
+
end
|
477
|
+
else
|
478
|
+
removed.zip(added) do |r, a|
|
479
|
+
url = a.items.url
|
480
|
+
url = Internal::Utils.append_parameters(url, "sourceFolderId" => Internal::Utils.escape_url_parameter(r.id))
|
481
|
+
conn.post(url, body, "Content-Type" => "application/atom+xml;type=entry")
|
482
|
+
end
|
483
|
+
if extra = added[removed.length..-1]
|
484
|
+
extra.each do |folder|
|
485
|
+
conn.post(folder.items.url, body, "Content-Type" => "application/atom+xml;type=entry")
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
self
|
491
|
+
end
|
492
|
+
|
493
|
+
def save_acl(acl)
|
494
|
+
acl.save
|
495
|
+
reload
|
496
|
+
self
|
497
|
+
end
|
498
|
+
|
499
|
+
class << self
|
500
|
+
# The repository this type is defined in
|
501
|
+
# @return [Repository]
|
502
|
+
attr_reader :repository
|
503
|
+
|
504
|
+
# @private
|
505
|
+
def from_atom_entry(repository, data, parameters = {})
|
506
|
+
query = "cra:object/c:properties/c:propertyId[@propertyDefinitionId = '%s']/c:value"
|
507
|
+
type_id = data.xpath(query % "cmis:objectTypeId", NS::COMBINED).text
|
508
|
+
klass = repository.type_by_id(type_id)
|
509
|
+
if klass
|
510
|
+
if klass <= self
|
511
|
+
klass.new(repository, data, parameters)
|
512
|
+
else
|
513
|
+
raise "You tried to do from_atom_entry on a type which is not a supertype of the type of the document you identified"
|
514
|
+
end
|
515
|
+
else
|
516
|
+
raise "The object #{extract_property(data, "String", 'cmis:name')} has an unrecognized type #{type_id}"
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
# @private
|
521
|
+
def from_parameters(repository, parameters)
|
522
|
+
url = repository.object_by_id_url(parameters)
|
523
|
+
data = repository.conn.get_atom_entry(url)
|
524
|
+
from_atom_entry(repository, data, parameters)
|
525
|
+
end
|
526
|
+
|
527
|
+
# A list of all attributes defined on this object
|
528
|
+
# @param [Boolean] inherited Nonfunctional
|
529
|
+
# @return [Hash{String => PropertyDefinition}]
|
530
|
+
def attributes(inherited = false)
|
531
|
+
{}
|
532
|
+
end
|
533
|
+
|
534
|
+
# The key of the CMIS Type
|
535
|
+
# @return [String]
|
536
|
+
# @raise [NotImplementedError] for Object/Folder/Document/Policy/Relationship
|
537
|
+
def key
|
538
|
+
raise NotImplementedError
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|