timfel-active_cmis 0.3.1

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