cmis_active 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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