timfel-active_cmis 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/README.md +34 -0
- data/Rakefile +36 -0
- data/TODO +7 -0
- data/active_cmis.gemspec +77 -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 +299 -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 +222 -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 +80 -0
- data/lib/active_cmis/repository.rb +327 -0
- data/lib/active_cmis/server.rb +113 -0
- data/lib/active_cmis/type.rb +200 -0
- data/lib/active_cmis/version.rb +8 -0
- data/lib/active_cmis.rb +31 -0
- metadata +111 -0
@@ -0,0 +1,563 @@
|
|
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' => 'entry')
|
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
|
+
# Tries to delete the object
|
292
|
+
# To delete all versions of a Document try #all_versions.delete
|
293
|
+
#
|
294
|
+
# For policies this may just remove the policy from the policy group
|
295
|
+
# of a document, this depends on how you retrieved the policy. Be careful
|
296
|
+
def destroy
|
297
|
+
conn.delete(self_link)
|
298
|
+
end
|
299
|
+
|
300
|
+
private
|
301
|
+
# Internal value, not meant for common-day use
|
302
|
+
# @private
|
303
|
+
# @return [Hash]
|
304
|
+
attr_reader :used_parameters
|
305
|
+
|
306
|
+
def self_link(options = {})
|
307
|
+
url = @self_link
|
308
|
+
if options.empty?
|
309
|
+
url
|
310
|
+
else
|
311
|
+
Internal::Utils.append_parameters(url, options)
|
312
|
+
end
|
313
|
+
#repository.object_by_id_url(options.merge("id" => id))
|
314
|
+
end
|
315
|
+
|
316
|
+
def data
|
317
|
+
parameters = {"includeAllowableActions" => true, "renditionFilter" => "*", "includeACL" => true}
|
318
|
+
data = conn.get_atom_entry(self_link(parameters))
|
319
|
+
@used_parameters = parameters
|
320
|
+
data
|
321
|
+
end
|
322
|
+
cache :data
|
323
|
+
|
324
|
+
def conn
|
325
|
+
@repository.conn
|
326
|
+
end
|
327
|
+
|
328
|
+
def _allowable_actions
|
329
|
+
if actions = data.xpath('cra:object/c:allowableActions', NS::COMBINED).first
|
330
|
+
actions
|
331
|
+
else
|
332
|
+
links = data.xpath("at:link[@rel = '#{Rel[repository.cmis_version][:allowableactions]}']/@href", NS::COMBINED)
|
333
|
+
if link = links.first
|
334
|
+
conn.get_xml(link.text)
|
335
|
+
else
|
336
|
+
nil
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# @param properties a hash key/definition pairs of properties to be rendered (defaults to all attributes)
|
342
|
+
# @param attributes a hash key/value pairs used to determine the values rendered (defaults to self.attributes)
|
343
|
+
# @param options
|
344
|
+
# @yield [entry] Optional block to customize the rendered atom entry
|
345
|
+
# @yieldparam [Nokogiri::XML::Builder] entry The entry XML builder element on which you can add additional tags (uses the NS::COMBINED namespaces)
|
346
|
+
def render_atom_entry(properties = self.class.attributes, attributes = self.attributes, options = {})
|
347
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
348
|
+
xml.entry(NS::COMBINED) do
|
349
|
+
xml.parent.namespace = xml.parent.namespace_definitions.detect {|ns| ns.prefix == "at"}
|
350
|
+
xml["at"].author do
|
351
|
+
xml["at"].name conn.user # FIXME: find reliable way to set author?
|
352
|
+
end
|
353
|
+
xml["at"].title attributes["cmis:name"]
|
354
|
+
if attributes["cmis:objectId"]
|
355
|
+
xml["at"].id_ attributes["cmis:objectId"]
|
356
|
+
else
|
357
|
+
xml["at"].id_ "random-garbage"
|
358
|
+
end
|
359
|
+
xml["cra"].object do
|
360
|
+
xml["c"].properties do
|
361
|
+
properties.each do |key, definition|
|
362
|
+
definition.render_property(xml, attributes[key])
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
yield(xml) if block_given?
|
367
|
+
end
|
368
|
+
end
|
369
|
+
conn.logger.debug builder.to_xml
|
370
|
+
builder.to_xml
|
371
|
+
end
|
372
|
+
|
373
|
+
# @private
|
374
|
+
attr_writer :updated_attributes
|
375
|
+
|
376
|
+
def updated_aspects(checkin = nil)
|
377
|
+
result = []
|
378
|
+
|
379
|
+
if key.nil?
|
380
|
+
result << {:message => :save_new_object, :parameters => []}
|
381
|
+
if parent_folders.length > 1
|
382
|
+
# We started from 0 folders, we already added the first when creating the document
|
383
|
+
|
384
|
+
# Note: to keep a save operation at least somewhat atomic this might be better done in save_new_object
|
385
|
+
result << {:message => :save_folders, :parameters => [parent_folders]}
|
386
|
+
end
|
387
|
+
else
|
388
|
+
if !updated_attributes.empty?
|
389
|
+
result << {:message => :save_attributes, :parameters => [updated_attributes, attributes, checkin]}
|
390
|
+
end
|
391
|
+
if @original_parent_folders
|
392
|
+
result << {:message => :save_folders, :parameters => [parent_folders, checkin && !updated_attributes]}
|
393
|
+
end
|
394
|
+
end
|
395
|
+
if acl && acl.updated # We need to be able to do this for newly created documents and merge the two
|
396
|
+
result << {:message => :save_acl, :parameters => [acl]}
|
397
|
+
end
|
398
|
+
|
399
|
+
if result.empty? && checkin
|
400
|
+
# NOTE: this needs some thinking through: in particular this may not work well if there would be an updated content stream
|
401
|
+
result << {:message => :save_attributes, :parameters => [[], [], checkin]}
|
402
|
+
end
|
403
|
+
|
404
|
+
result
|
405
|
+
end
|
406
|
+
|
407
|
+
def save_new_object
|
408
|
+
if self.class.required_attributes.any? {|a, _| attribute(a).nil? }
|
409
|
+
raise Error::InvalidArgument.new("Not all required attributes are filled in")
|
410
|
+
end
|
411
|
+
|
412
|
+
properties = self.class.attributes.reject do |key, definition|
|
413
|
+
# !updated_attributes.include?(key) && !definition.required
|
414
|
+
attributes[key].nil? or definition.updatability == "readonly"
|
415
|
+
end
|
416
|
+
body = render_atom_entry(properties, attributes, :create => true)
|
417
|
+
|
418
|
+
url = create_url
|
419
|
+
response = conn.post(create_url, body, "Content-Type" => "application/atom+xml;type=entry")
|
420
|
+
# XXX: Currently ignoring Location header in response
|
421
|
+
|
422
|
+
response_data = Nokogiri::XML::parse(response).xpath("at:entry", NS::COMBINED) # Assume that a response indicates success?
|
423
|
+
|
424
|
+
@self_link = response_data.xpath("at:link[@rel = 'self']/@href", NS::COMBINED).first
|
425
|
+
@self_link = @self_link.text
|
426
|
+
reload
|
427
|
+
@key = attribute("cmis:objectId")
|
428
|
+
|
429
|
+
self
|
430
|
+
end
|
431
|
+
|
432
|
+
def save_attributes(attributes, values, checkin = nil)
|
433
|
+
if attributes.empty? && checkin.nil?
|
434
|
+
raise "Error: saving attributes but nothing to do"
|
435
|
+
end
|
436
|
+
properties = self.class.attributes.reject {|key,_| !updated_attributes.include?(key)}
|
437
|
+
body = render_atom_entry(properties, values, :checkin => checkin)
|
438
|
+
|
439
|
+
if checkin.nil?
|
440
|
+
parameters = {}
|
441
|
+
else
|
442
|
+
checkin, major, comment = *checkin
|
443
|
+
parameters = {"checkin" => checkin}
|
444
|
+
if checkin
|
445
|
+
parameters.merge! "major" => !!major, "checkinComment" => Internal::Utils.escape_url_parameter(comment)
|
446
|
+
|
447
|
+
if properties.empty?
|
448
|
+
# The standard specifies that we can have an empty body here, that does not seem to be true for OpenCMIS
|
449
|
+
# body = ""
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# NOTE: Spec says Entity Tag should be used for changeTokens, that does not seem to work
|
455
|
+
if ct = attribute("cmis:changeToken")
|
456
|
+
parameters.merge! "changeToken" => Internal::Utils.escape_url_parameter(ct)
|
457
|
+
end
|
458
|
+
|
459
|
+
uri = self_link(parameters)
|
460
|
+
response = conn.put(uri, body)
|
461
|
+
|
462
|
+
data = Nokogiri::XML.parse(response, nil, nil, Nokogiri::XML::ParseOptions::STRICT).xpath("at:entry", NS::COMBINED)
|
463
|
+
if data.xpath("cra:object/c:properties/c:propertyId[@propertyDefinitionId = 'cmis:objectId']/c:value", NS::COMBINED).text == id
|
464
|
+
reload
|
465
|
+
@data = data
|
466
|
+
self
|
467
|
+
else
|
468
|
+
reload # Updated attributes should be forgotten here
|
469
|
+
ActiveCMIS::Object.from_atom_entry(repository, data)
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def save_folders(requested_parent_folders, checkin = nil)
|
474
|
+
current = parent_folders.to_a
|
475
|
+
future = requested_parent_folders.to_a
|
476
|
+
|
477
|
+
common_folders = future.map {|f| f.id}.select {|id| current.any? {|f| f.id == id } }
|
478
|
+
|
479
|
+
added = future.select {|f1| current.all? {|f2| f1.id != f2.id } }
|
480
|
+
removed = current.select {|f1| future.all? {|f2| f1.id != f2.id } }
|
481
|
+
|
482
|
+
# NOTE: an absent atom:content is important here according to the spec, for the moment I did not suffer from this
|
483
|
+
body = render_atom_entry("cmis:objectId" => self.class.attributes["cmis:objectId"])
|
484
|
+
|
485
|
+
# Note: change token does not seem to matter here
|
486
|
+
# FIXME: currently we assume the data returned by post is not important, I'm not sure that this is always true
|
487
|
+
if added.empty?
|
488
|
+
removed.each do |folder|
|
489
|
+
url = repository.unfiled.url
|
490
|
+
url = Internal::Utils.append_parameters(url, "removeFrom" => Internal::Utils.escape_url_parameter(removed.id))
|
491
|
+
conn.post(url, body, "Content-Type" => "application/atom+xml;type=entry")
|
492
|
+
end
|
493
|
+
elsif removed.empty?
|
494
|
+
added.each do |folder|
|
495
|
+
conn.post(folder.items.url, body, "Content-Type" => "application/atom+xml;type=entry")
|
496
|
+
end
|
497
|
+
else
|
498
|
+
removed.zip(added) do |r, a|
|
499
|
+
url = a.items.url
|
500
|
+
url = Internal::Utils.append_parameters(url, "sourceFolderId" => Internal::Utils.escape_url_parameter(r.id))
|
501
|
+
conn.post(url, body, "Content-Type" => "application/atom+xml;type=entry")
|
502
|
+
end
|
503
|
+
if extra = added[removed.length..-1]
|
504
|
+
extra.each do |folder|
|
505
|
+
conn.post(folder.items.url, body, "Content-Type" => "application/atom+xml;type=entry")
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
self
|
511
|
+
end
|
512
|
+
|
513
|
+
def save_acl(acl)
|
514
|
+
acl.save
|
515
|
+
reload
|
516
|
+
self
|
517
|
+
end
|
518
|
+
|
519
|
+
class << self
|
520
|
+
# The repository this type is defined in
|
521
|
+
# @return [Repository]
|
522
|
+
attr_reader :repository
|
523
|
+
|
524
|
+
# @private
|
525
|
+
def from_atom_entry(repository, data, parameters = {})
|
526
|
+
query = "cra:object/c:properties/c:propertyId[@propertyDefinitionId = '%s']/c:value"
|
527
|
+
type_id = data.xpath(query % "cmis:objectTypeId", NS::COMBINED).text
|
528
|
+
klass = repository.type_by_id(type_id)
|
529
|
+
if klass
|
530
|
+
if klass <= self
|
531
|
+
klass.new(repository, data, parameters)
|
532
|
+
else
|
533
|
+
raise "You tried to do from_atom_entry on a type which is not a supertype of the type of the document you identified"
|
534
|
+
end
|
535
|
+
else
|
536
|
+
raise "The object #{extract_property(data, "String", 'cmis:name')} has an unrecognized type #{type_id}"
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# @private
|
541
|
+
def from_parameters(repository, parameters)
|
542
|
+
url = repository.object_by_id_url(parameters)
|
543
|
+
data = repository.conn.get_atom_entry(url)
|
544
|
+
from_atom_entry(repository, data, parameters)
|
545
|
+
end
|
546
|
+
|
547
|
+
# A list of all attributes defined on this object
|
548
|
+
# @param [Boolean] inherited Nonfunctional
|
549
|
+
# @return [Hash{String => PropertyDefinition}]
|
550
|
+
def attributes(inherited = false)
|
551
|
+
{}
|
552
|
+
end
|
553
|
+
|
554
|
+
# The key of the CMIS Type
|
555
|
+
# @return [String]
|
556
|
+
# @raise [NotImplementedError] for Object/Folder/Document/Policy/Relationship
|
557
|
+
def key
|
558
|
+
raise NotImplementedError
|
559
|
+
end
|
560
|
+
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|