cmis_active 0.3.7

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.
@@ -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