timfel-active_cmis 0.3.1

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,299 @@
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
+ # Returns all documents in the version series of this document.
69
+ # Uses self to represent the version of this document
70
+ # @return [Collection<Document>, Array(self)]
71
+ def versions
72
+ link = data.xpath("at:link[@rel = 'version-history']/@href", NS::COMBINED)
73
+ if link = link.first
74
+ Collection.new(repository, link) # Problem: does not in fact use self
75
+ else
76
+ # The document is not versionable
77
+ [self]
78
+ end
79
+ end
80
+ cache :versions
81
+
82
+ # Returns self if this is the latest version
83
+ # Note: There will allways be a latest version in a version series
84
+ # @return [Document]
85
+ def latest_version
86
+ link = data.xpath("at:link[@rel = 'current-version']/@href", NS::COMBINED)
87
+ if link.first
88
+ entry = conn.get_atom_entry(link.first.text)
89
+ self_or_new(entry)
90
+ else
91
+ # FIXME: should somehow return the current version even for opencmis
92
+ self
93
+ end
94
+ end
95
+
96
+ # Returns self if this is the working copy
97
+ # Returns nil if there is no working copy
98
+ # @return [Document]
99
+ def working_copy
100
+ link = data.xpath("at:link[@rel = 'working-copy']/@href", NS::COMBINED)
101
+ if link.first
102
+ entry = conn.get_atom_entry(link.first.text)
103
+ self_or_new(entry)
104
+ else
105
+ nil
106
+ end
107
+ end
108
+
109
+ def latest?
110
+ attributes["cmis:isLatestVersion"]
111
+ end
112
+ def major?
113
+ attributes["cmis:isMajorVersion"]
114
+ end
115
+ def latest_major?
116
+ attributes["cmis:isLatestMajorVersion"]
117
+ end
118
+
119
+ def working_copy?
120
+ return false if key.nil?
121
+
122
+ # NOTE: This may not be a sufficient condition, but according to the spec it should be
123
+ !data.xpath("at:link[@rel = 'via']", NS::COMBINED).empty?
124
+ end
125
+
126
+ # Returns information about the checked out status of this document
127
+ #
128
+ # @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
129
+ def version_series_checked_out
130
+ attributes = self.attributes
131
+ if attributes["cmis:isVersionSeriesCheckedOut"]
132
+ result = {}
133
+ if attributes.has_key? "cmis:versionSeriesCheckedOutBy"
134
+ result[:by] = attributes["cmis:versionSeriesCheckedOutBy"]
135
+ end
136
+ if attributes.has_key? "cmis:versionSeriesCheckedOutId"
137
+ result[:id] = attributes["cmis:versionSeriesCheckedOutId"]
138
+ end
139
+ result
140
+ else
141
+ nil
142
+ end
143
+ end
144
+
145
+ # The checkout operation results in a Private Working Copy
146
+ #
147
+ # Most properties should be the same as for the document that was checked out,
148
+ # certain properties may differ such as cmis:objectId and cmis:creationDate.
149
+ #
150
+ # The content stream of the PWC may be identical to that of the document
151
+ # that was checked out, or it may be unset.
152
+ # @return [Document] The checked out version of this document
153
+ def checkout
154
+ body = render_atom_entry(self.class.attributes.reject {|k,v| k != "cmis:objectId"})
155
+
156
+ response = conn.post_response(repository.checkedout.url, body)
157
+ if 200 <= response.code.to_i && response.code.to_i < 300
158
+ entry = Nokogiri::XML.parse(response.body, nil, nil, Nokogiri::XML::ParseOptions::STRICT).xpath("/at:entry", NS::COMBINED)
159
+ result = self_or_new(entry)
160
+ if result.working_copy? # Work around a bug in OpenCMIS where result returned is the version checked out not the PWC
161
+ result
162
+ else
163
+ conn.logger.warn "Repository did not return working copy for checkout operation"
164
+ result.working_copy
165
+ end
166
+ else
167
+ raise response.body
168
+ end
169
+ end
170
+
171
+ # This action may not be permitted (query allowable_actions to see whether it is permitted)
172
+ # @return [void]
173
+ def cancel_checkout
174
+ if !self.class.versionable
175
+ raise Error::Constraint.new("Object is not versionable, can't cancel checkout")
176
+ elsif working_copy?
177
+ conn.delete(self_link)
178
+ else
179
+ raise Error::InvalidArgument.new("Not a working copy")
180
+ end
181
+ end
182
+
183
+ # You can specify whether the new version should be major (defaults to true)
184
+ # You can optionally give a list of attributes that need to be set.
185
+ #
186
+ # This operation exists only for Private Working Copies
187
+ # @return [Document] The final version that results from the checkin
188
+ def checkin(major = true, comment = "", updated_attributes = {})
189
+ if working_copy?
190
+ update(updated_attributes)
191
+ result = self
192
+ updated_aspects([true, major, comment]).each do |hash|
193
+ result = result.send(hash[:message], *hash[:parameters])
194
+ end
195
+ result
196
+ else
197
+ raise "Not a working copy"
198
+ end
199
+ end
200
+
201
+ # @return [void]
202
+ def reload
203
+ @updated_contents = nil
204
+ super
205
+ end
206
+
207
+ private
208
+ attr_reader :updated_contents
209
+
210
+ def render_atom_entry(properties = self.class.attributes, attributes = self.attributes, options = {})
211
+ super(properties, attributes, options) do |entry|
212
+ if updated_contents && (options[:create] || options[:checkin])
213
+ entry["cra"].content do
214
+ entry["cra"].mediatype(updated_contents[:mime_type] || "application/binary")
215
+ data = updated_contents[:data] || File.read(updated_contents[:file])
216
+ entry["cra"].base64 [data].pack("m")
217
+ end
218
+ end
219
+ if block_given?
220
+ yield(entry)
221
+ end
222
+ end
223
+ end
224
+
225
+
226
+ def updated_aspects(checkin = nil)
227
+ if working_copy? && !(checkin || repository.pwc_ubdatable)
228
+ raise Error::NotSupported.new("Updating a PWC without checking in is not supported by repository")
229
+ end
230
+ unless working_copy? || checkin.nil?
231
+ raise Error::NotSupported.new("Can't check in when not checked out")
232
+ end
233
+
234
+ result = super
235
+
236
+ unless checkin || key.nil? || updated_contents.nil?
237
+ # Don't set content_stream separately if it can be done by setting the content during create
238
+ #
239
+ # TODO: For checkin we could try to see if we can save it via puts *before* we checkin,
240
+ # If not checking in we should also try to see if we can actually save it
241
+ result << {:message => :save_content_stream, :parameters => [updated_contents]}
242
+ end
243
+
244
+ result
245
+ end
246
+
247
+ def self_or_new(entry)
248
+ if entry.nil?
249
+ nil
250
+ elsif entry.xpath("cra:object/c:properties/c:propertyId[@propertyDefinitionId = 'cmis:objectId']/c:value", NS::COMBINED).text == id
251
+ reload
252
+ @data = entry
253
+ self
254
+ else
255
+ ActiveCMIS::Object.from_atom_entry(repository, entry)
256
+ end
257
+ end
258
+
259
+ def create_url
260
+ if f = parent_folders.first
261
+ url = f.items.url
262
+ if self.class.versionable # Necessary in OpenCMIS at least
263
+ url
264
+ else
265
+ Internal::Utils.append_parameters(url, "versioningState" => "none")
266
+ end
267
+ else
268
+ raise Error::NotSupported.new("Creating an unfiled document is not supported by CMIS")
269
+ # Can't create documents that are unfiled, CMIS does not support it (note this means exceptions should not actually be NotSupported)
270
+ end
271
+ end
272
+
273
+ def save_content_stream(stream)
274
+ # Should never occur (is private method)
275
+ raise "no content to save" if stream.nil?
276
+
277
+ # put to link with rel 'edit-media' if it's there
278
+ # NOTE: cmislib uses the src link of atom:content instead, that might be correct
279
+ edit_links = Internal::Utils.extract_links(data, "edit-media")
280
+ if edit_links.length == 1
281
+ link = edit_links.first
282
+ elsif edit_links.empty?
283
+ raise Error.new("No edit-media link, can't save content")
284
+ else
285
+ raise Error.new("Too many edit-media links, don't know how to choose")
286
+ end
287
+ data = stream[:data] || File.open(stream[:file])
288
+ content_type = stream[:mime_type] || "application/octet-stream"
289
+
290
+ if stream.has_key?(:overwrite)
291
+ url = Internal::Utils.append_parameters(link, "overwrite" => stream[:overwrite])
292
+ else
293
+ url = link
294
+ end
295
+ conn.put(url, data, "Content-Type" => content_type)
296
+ self
297
+ end
298
+ end
299
+ 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
@@ -0,0 +1,86 @@
1
+ module ActiveCMIS
2
+ module Internal
3
+ module Caching
4
+ def self.included(cl)
5
+ cl.extend ClassMethods
6
+ end
7
+
8
+ # A module for internal use only.
9
+ module ClassMethods
10
+
11
+ # Creates a proxy method for the given method names that caches the result.
12
+ #
13
+ # Parameters are passed and ignored, cached values will be returned regardless of the parameters.
14
+ # @param [Symbol, <Symbol>] Names of methods that will be cached
15
+ # @return [void]
16
+ def cache(*names)
17
+ (@cached_methods ||= []).concat(names).uniq!
18
+ names.each do |name|
19
+ alias_method("#{name}__uncached", name)
20
+ class_eval <<-RUBY, __FILE__, __LINE__+1
21
+ if private_method_defined? :"#{name}"
22
+ private_method = true
23
+ end
24
+ def #{name}(*a, &b)
25
+ if defined? @#{name}
26
+ @#{name}
27
+ else
28
+ @#{name} = #{name}__uncached(*a, &b)
29
+ end
30
+ end
31
+ if private_method
32
+ private :"#{name}__uncached"
33
+ private :"#{name}"
34
+ end
35
+ RUBY
36
+ end
37
+ reloadable
38
+ end
39
+
40
+ # Creates methods to retrieve attributes with the given names.
41
+ #
42
+ # If the given attribute does not yet exist the method #load_from_data will be called
43
+ #
44
+ # @param [Symbol, <Symbol>] Names of desired attributes
45
+ # @return [void]
46
+ def cached_reader(*names)
47
+ (@cached_methods ||= []).concat(names).uniq!
48
+ names.each do |name|
49
+ define_method "#{name}" do
50
+ if instance_variable_defined? "@#{name}"
51
+ instance_variable_get("@#{name}")
52
+ else
53
+ load_from_data # FIXME: make flexible?
54
+ instance_variable_get("@#{name}")
55
+ end
56
+ end
57
+ end
58
+ reloadable
59
+ end
60
+
61
+ private
62
+ def reloadable
63
+ class_eval <<-RUBY, __FILE__, __LINE__
64
+ def __reload
65
+ #{@cached_methods.inspect}.map do |method|
66
+ :"@\#{method}"
67
+ end.select do |iv|
68
+ instance_variable_defined? iv
69
+ end.each do |iv|
70
+ remove_instance_variable iv
71
+ end + (defined?(super) ? super : [])
72
+ end
73
+ private :__reload
74
+ RUBY
75
+ unless instance_methods.include? "reload"
76
+ class_eval <<-RUBY, __FILE__, __LINE__
77
+ def reload
78
+ __reload
79
+ end
80
+ RUBY
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end