active_cmis 0.1.0

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,13 @@
1
+ module ActiveCMIS
2
+ class Policy < ActiveCMIS::Object
3
+ private
4
+ def create_url
5
+ if f = parent_folders.first
6
+ f.items.url
7
+ else
8
+ raise "not yet"
9
+ # Policy collection of containing document?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,175 @@
1
+ module ActiveCMIS
2
+ class PropertyDefinition
3
+ # @return [String]
4
+ attr_reader :object_type, :id, :local_name, :local_namespace, :query_name,
5
+ :display_name, :description, :cardinality, :property_type, :updatability,
6
+ :default_value
7
+ # @return [Boolean]
8
+ attr_reader :inherited, :required, :queryable, :orderable, :choices, :open_choice
9
+
10
+ # @private
11
+ def initialize(object_type, property_definition)
12
+ @object_type = object_type
13
+ @property_definition = property_definition
14
+ params = {}
15
+ property_type = nil
16
+ property_definition.map do |node|
17
+ next unless node.namespace
18
+ next unless node.namespace.href == NS::CMIS_CORE
19
+
20
+ # FIXME: add support for "choices"
21
+ case node.node_name
22
+ when "id"
23
+ @id = node.text
24
+ when "localName"
25
+ @local_name = node.text
26
+ when "localNamespace"
27
+ @local_namespace = node.text
28
+ when "displayName"
29
+ @display_name = node.text
30
+ when "queryName"
31
+ @query_name = node.text
32
+ when "propertyType"
33
+ # Will be post processed, but we need to know all the parameters before we can pick an atomic type
34
+ property_type = node.text
35
+ when "cardinality"
36
+ @cardinality = node.text
37
+ when "updatability"
38
+ @updatability = node.text
39
+ when "inherited"
40
+ @inherited = AtomicType::Boolean.xml_to_bool(node.text)
41
+ when "required"
42
+ @required = AtomicType::Boolean.xml_to_bool(node.text)
43
+ when "queryable"
44
+ @queryable = AtomicType::Boolean.xml_to_bool(node.text)
45
+ when "orderable"
46
+ @orderable = AtomicType::Boolean.xml_to_bool(node.text)
47
+ when "openChoice"
48
+ @open_choice = AtomicType::Boolean.xml_to_bool(node.text)
49
+ when "maxValue", "minValue", "resolution", "precision", "maxLength"
50
+ params[node.node_name] = node.text
51
+ end
52
+ end
53
+
54
+ if required && updatability == "readonly"
55
+ logger.warn "The server behaved strange: attribute #{self.inspect} required but readonly, will set required to false"
56
+ required = false
57
+ end
58
+
59
+ @property_type = case property_type.downcase
60
+ when "string"
61
+ max_length = params["maxLength"] ? params["maxLength"].to_i : nil
62
+ AtomicType::String.new(max_length)
63
+ when "decimal"
64
+ min_value = params["minValue"] ? params["minValue"].to_f : nil
65
+ max_value = params["maxValue"] ? params["maxValue"].to_f : nil
66
+ AtomicType::Decimal.new(params["precision"].to_i, min_value, max_value)
67
+ when "integer"
68
+ min_value = params["minValue"] ? params["minValue"].to_i : nil
69
+ max_value = params["maxValue"] ? params["maxValue"].to_i : nil
70
+ AtomicType::Integer.new(min_value, max_value)
71
+ when "datetime"
72
+ AtomicType::DateTime.new(params["resolution"] || (logger.warn "No resolution for DateTime #{@id}"; "time") )
73
+ when "html"
74
+ AtomicType::HTML.new
75
+ when "id"
76
+ AtomicType::ID.new
77
+ when "boolean"
78
+ AtomicType::Boolean.new
79
+ when "uri"
80
+ AtomicType::URI.new
81
+ else
82
+ raise "Unknown property type #{property_type}"
83
+ end
84
+ end
85
+
86
+ # @return [Boolean] Returns true if the attribute can have multiple values
87
+ def repeating
88
+ cardinality == "multi"
89
+ end
90
+
91
+ # @return [String]
92
+ def inspect
93
+ "#{object_type.display_name}:#{id} => #{property_type}#{"[]" if repeating}"
94
+ end
95
+ alias to_s inspect
96
+
97
+ # @return [String]
98
+ def property_name
99
+ "property#{property_type}"
100
+ end
101
+
102
+ # @private
103
+ def render_property(xml, value)
104
+ xml["c"].send(property_name, "propertyDefinitionId" => id) {
105
+ if repeating
106
+ value.each do |v|
107
+ property_type.rb2cmis(xml, v)
108
+ end
109
+ else
110
+ property_type.rb2cmis(xml, value)
111
+ end
112
+ }
113
+ end
114
+
115
+ # @private
116
+ # FIXME: should probably also raise error for out of bounds case
117
+ def validate_ruby_value(value)
118
+ if updatability == "readonly" # FIXME: what about oncreate?
119
+ raise "You are trying to update a readonly attribute (#{self})"
120
+ elsif required && value.nil?
121
+ raise "You are trying to unset a required attribute (#{self})"
122
+ elsif repeating != (Array === value)
123
+ raise "You are ignoring the cardinality for an attribute (#{self})"
124
+ else
125
+ if repeating && z = value.detect {|v| !property_type.can_handle?(v)}
126
+ raise "Can't assign attribute with type #{z.class} to attribute with type #{property_type}"
127
+ elsif !repeating && !property_type.can_handle?(value)
128
+ raise "Can't assign attribute with type #{value.class} to attribute with type #{property_type}"
129
+ end
130
+ end
131
+ end
132
+
133
+ # @private
134
+ def extract_property(properties)
135
+ elements = properties.children.select do |n|
136
+ n.node_name == property_name &&
137
+ n["propertyDefinitionId"] == id &&
138
+ n.namespace.href == NS::CMIS_CORE
139
+ end
140
+ if elements.empty?
141
+ if required
142
+ logger.warn "The server behaved strange: attribute #{self.inspect} required but not present among properties"
143
+ # raise ActiveCMIS::Error.new("The server behaved strange: attribute #{self.inspect} required but not present among properties")
144
+ end
145
+ if repeating
146
+ []
147
+ else
148
+ nil
149
+ end
150
+ elsif elements.length == 1
151
+ values = elements.first.children.select {|node| node.name == 'value' && node.namespace && node.namespace.href == ActiveCMIS::NS::CMIS_CORE}
152
+ if required && values.empty?
153
+ logger.warn "The server behaved strange: attribute #{self.inspect} required but not present among properties"
154
+ #raise ActiveCMIS::Error.new("The server behaved strange: attribute #{self.inspect} required but no values specified")
155
+ end
156
+ if !repeating && values.length > 1
157
+ logger.warn "The server behaved strange: attribute #{self.inspect} required but not present among properties"
158
+ #raise ActiveCMIS::Error.new("The server behaved strange: attribute #{self.inspect} not repeating but multiple values given")
159
+ end
160
+ values
161
+ else
162
+ raise "Property is not unique"
163
+ end
164
+ end
165
+
166
+ # @return [Logger] The logger of the repository
167
+ def logger
168
+ repository.logger
169
+ end
170
+ # @return [Repository] The repository that the CMIS type is defined in
171
+ def repository
172
+ object_type.repository
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveCMIS
2
+ module Rel
3
+ def self.[](version)
4
+ if version == '1.0'
5
+ prefix = "http://docs.oasis-open.org/ns/cmis/link/200908/"
6
+ {
7
+ :allowableactions => "#{prefix}allowableactions",
8
+ :acl => "#{prefix}acl",
9
+ :relationships => "#{prefix}relationships",
10
+ :changes => "#{prefix}changes",
11
+ }
12
+ else
13
+ raise ActiveCMIS::Error.new("ActiveCMIS only works with CMIS 1.0, requested version was #{version}")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveCMIS
2
+ class Relationship < ::ActiveCMIS::Object
3
+ # @return [Object]
4
+ def source
5
+ Internal::Utils.string_or_id_to_object(attribute("cmis:sourceId"))
6
+ end
7
+ cache :source
8
+
9
+ # @return [Object]
10
+ def target
11
+ Internal::Utils.string_or_id_to_object(attribute("cmis:targetId"))
12
+ end
13
+ cache :target
14
+
15
+ # Remove the relationship
16
+ # @return [void]
17
+ def delete
18
+ conn.delete(self_link)
19
+ end
20
+
21
+ # @see Object#update
22
+ # @param (see ActiveCMIS::Object#update)
23
+ # @return [void]
24
+ def update(updates = {})
25
+ super
26
+ # Potentially necessary if repositories support it
27
+ # Probably not though
28
+ if source = updates["cmis:sourceId"]
29
+ remove_instance_variable "@source"
30
+ end
31
+ if updates["cmis:targetId"]
32
+ remove_instance_variable "@target"
33
+ end
34
+ end
35
+
36
+ # Return [], a relationship is not fileable
37
+ # @return [Array()]
38
+ def parent_folders
39
+ []
40
+ end
41
+
42
+ private
43
+ def create_url
44
+ source.source_relations.url
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,65 @@
1
+ module ActiveCMIS
2
+ class Rendition
3
+ # @return [Repository]
4
+ attr_reader :repository
5
+ # @return [Numeric,nil] Size of the rendition, may not be given or misleading
6
+ attr_reader :size
7
+ # @return [String,nil]
8
+ attr_reader :rendition_kind
9
+ # @return [String,nil] The format is equal to the mime type, but may be unset or misleading
10
+ attr_reader :format
11
+
12
+ # @private
13
+ def initialize(repository, link)
14
+ @repository = repository
15
+
16
+ @rel = link['rel'] == "alternate"
17
+ @rendition_kind = link['renditionKind'] if rendition?
18
+ @format = link['type']
19
+ if link['href']
20
+ @url = URI(link['href'])
21
+ else # For inline content streams
22
+ @data = link['data']
23
+ end
24
+ @size = link['length'] ? link['length'].to_i : nil
25
+
26
+
27
+ @link = link # FIXME: For debugging purposes only, remove
28
+ end
29
+
30
+ # Used to differentiate between rendition and primary content
31
+ def rendition?
32
+ @rel == "alternate"
33
+ end
34
+ # Used to differentiate between rendition and primary content
35
+ def primary?
36
+ @rel.nil?
37
+ end
38
+
39
+ # Returns a hash with the name of the file to which was written, the lenthe, and the content type
40
+ #
41
+ # *WARNING*: this loads the complete file in memory and dumps it at once, this should be fixed
42
+ # @param [String] filename Location to store the content.
43
+ # @return [Hash]
44
+ def get_file(file_name)
45
+ if @url
46
+ response = repository.conn.get_response(@url)
47
+ status = response.code.to_i
48
+ if 200 <= status && status < 300
49
+ data = response.body
50
+ else
51
+ raise HTTPError.new("Problem downloading rendition: status: #{status}, message: #{response.body}")
52
+ end
53
+ content_type = response.content_type
54
+ content_lenth = response.content_length || response.body.length # In case content encoding is chunked? ??
55
+ else
56
+ data = @data
57
+ content_type = @format
58
+ content_length = @data.length
59
+ end
60
+ File.open(file_name, "w") {|f| f.syswrite data }
61
+
62
+ {:file => file_name, :content_type => content_type, :content_length => content_length}
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,258 @@
1
+ module ActiveCMIS
2
+ class Repository
3
+ # @return [Logger] A logger to which debug output and so on is sent
4
+ attr_reader :logger
5
+
6
+ # @private
7
+ def initialize(connection, logger, initial_data) #:nodoc:
8
+ @conn = connection
9
+ @data = initial_data
10
+ @logger = logger
11
+ end
12
+
13
+ # Use authentication to access the CMIS repository
14
+ #
15
+ # e.g.: repo.authenticate(:basic, "username", "password")
16
+ # @return [void]
17
+ def authenticate(method, *params)
18
+ conn.authenticate(method, *params)
19
+ nil
20
+ end
21
+
22
+ # The identifier of the repository
23
+ # @return [String]
24
+ def key
25
+ @key ||= data.xpath('cra:repositoryInfo/c:repositoryId', NS::COMBINED).text
26
+ end
27
+
28
+ # @return [String]
29
+ def inspect
30
+ "<#ActiveCMIS::Repository #{key}>"
31
+ end
32
+
33
+ # The version of the CMIS standard supported by this repository
34
+ # @return [String]
35
+ def cmis_version
36
+ # NOTE: we might want to "version" our xml namespaces depending on the CMIS version
37
+ # If we do that we need to make this method capable of not using the predefined namespaces
38
+ #
39
+ # On the other hand breaking the XML namespace is probably going to break other applications too so the might not change them even when the spec is updated
40
+ @cmis_version ||= data.xpath("cra:repositoryInfo/c:cmisVersionSupported", NS::COMBINED).text
41
+ end
42
+
43
+ # Finds the object with a given ID in the repository
44
+ #
45
+ # @param [String] id
46
+ # @param parameters A list of parameters used to get (defaults are what you should use)
47
+ # @return [Object]
48
+ def object_by_id(id, parameters = {"renditionFilter" => "*", "includeAllowableActions" => "true", "includeACL" => true})
49
+ ActiveCMIS::Object.from_parameters(self, parameters.merge("id" => id))
50
+ end
51
+
52
+ # @private
53
+ def object_by_id_url(parameters)
54
+ template = pick_template("objectbyid")
55
+ raise "Repository does not define required URI-template 'objectbyid'" unless template
56
+ url = fill_in_template(template, parameters)
57
+ end
58
+
59
+ # Finds the type with a given ID in the repository
60
+ # @return [Class]
61
+ def type_by_id(id)
62
+ @type_by_id ||= {}
63
+ if result = @type_by_id[id]
64
+ result
65
+ else
66
+ template = pick_template("typebyid")
67
+ raise "Repository does not define required URI-template 'typebyid'" unless template
68
+ url = fill_in_template(template, "id" => id)
69
+
70
+ @type_by_id[id] = Type.create(conn, self, conn.get_atom_entry(url))
71
+ end
72
+ end
73
+
74
+ %w[root checkedout unfiled].each do |coll_name|
75
+ define_method coll_name do
76
+ iv = :"@#{coll_name}"
77
+ if instance_variable_defined?(iv)
78
+ instance_variable_get(iv)
79
+ else
80
+ href = data.xpath("app:collection[cra:collectionType[child::text() = '#{coll_name}']]/@href", NS::COMBINED)
81
+ if href.first
82
+ result = Collection.new(self, href.first)
83
+ else
84
+ result = nil
85
+ end
86
+ instance_variable_set(iv, result)
87
+ end
88
+ end
89
+ end
90
+
91
+ # A collection containing the CMIS base types supported by this repository
92
+ # @return [Collection<Class>]
93
+ def base_types
94
+ @base_types ||= begin
95
+ query = "app:collection[cra:collectionType[child::text() = 'types']]/@href"
96
+ href = data.xpath(query, NS::COMBINED)
97
+ if href.first
98
+ url = href.first.text
99
+ Collection.new(self, url) do |entry|
100
+ id = entry.xpath("cra:type/c:id", NS::COMBINED).text
101
+ type_by_id id
102
+ end
103
+ else
104
+ raise "Repository has no types collection, this is strange and wrong"
105
+ end
106
+ end
107
+ end
108
+
109
+ # An array containing all the types used by this repository
110
+ # @return [<Class>]
111
+ def types
112
+ @types ||= base_types.map do |t|
113
+ t.all_subtypes
114
+ end.flatten
115
+ end
116
+
117
+ # Returns a collection with the changes since the given changeLogToken.
118
+ #
119
+ # Completely uncached so use with care
120
+ #
121
+ # @param options Keys can be Symbol or String, all options are optional
122
+ # @option options [String] filter
123
+ # @option options [String] changeLogToken A token indicating which changes you already know about
124
+ # @option options [Integer] maxItems For paging
125
+ # @option options [Boolean] includeAcl
126
+ # @option options [Boolean] includePolicyIds
127
+ # @option options [Boolean] includeProperties
128
+ # @return [Collection]
129
+ def changes(options = {})
130
+ query = "at:link[@rel = '#{Rel[cmis_version][:changes]}']/@href"
131
+ link = data.xpath(query, NS::COMBINED)
132
+ if link = link.first
133
+ link = Internal::Utils.append_parameters(link.to_s, options)
134
+ Collection.new(self, link)
135
+ end
136
+ end
137
+
138
+ # Returns a collection with the results of a query (if supported by the repository)
139
+ #
140
+ # @param [#to_s] query_string A query in the CMIS SQL format (unescaped in any way)
141
+ # @param [{Symbol => ::Object}] options Optional configuration for the query
142
+ # @option options [Boolean] :searchAllVersions (false)
143
+ # @option options [Boolean] :includeAllowableActions (false)
144
+ # @option options ["none","source","target","both"] :includeRelationships
145
+ # @option options [String] :renditionFilter ('cmis:none') Possible values: 'cmis:none', '*' (all), comma-separated list of rendition kinds or mimetypes
146
+ # @option options [Integer] :maxItems used for paging
147
+ # @option options [Integer] :skipCount (0) used for paging
148
+ # @return [Collection]
149
+ def query(query_string, options = {})
150
+ raise "This repository does not support queries" if capabilities["Query"] == "none"
151
+ # For the moment we make no difference between metadataonly,fulltextonly,bothseparate and bothcombined
152
+ # Nor do we look at capabilities["Join"] (none, inneronly, innerandouter)
153
+
154
+ # For searchAllVersions need to check capabilities["AllVersionsSearchable"]
155
+ # includeRelationships, includeAllowableActions and renditionFilter only work if SELECT only contains attributes from 1 object
156
+ valid_params = ["searchAllVersions", "includeAllowableActions", "includeRelationships", "renditionFilter", "maxItems", "skipCount"]
157
+ invalid_params = options.keys - valid_params
158
+ unless invalid_params.empty?
159
+ raise "Invalid parameters for query: #{invalid_params.join ', '}"
160
+ end
161
+
162
+ # FIXME: options are not respected yet by pick_template
163
+ url = pick_template("query", :mimetype => "application/atom+xml", :type => "feed")
164
+ url = fill_in_template(url, options.merge("q" => query_string))
165
+ Collection.new(self, url)
166
+ end
167
+
168
+ # The root folder of the repository (as defined in the CMIS standard)
169
+ # @return [Folder]
170
+ def root_folder
171
+ @root_folder ||= object_by_id(data.xpath("cra:repositoryInfo/c:rootFolderId", NS::COMBINED).text)
172
+ end
173
+
174
+ # Returns an Internal::Connection object, normally you should not use this directly
175
+ # @return [Internal::Connection]
176
+ def conn
177
+ @conn ||= Internal::Connection.new
178
+ end
179
+
180
+ # Describes the capabilities of the repository
181
+ # @return [Hash{String => String,Boolean}] The hash keys have capability cut of their name
182
+ def capabilities
183
+ @capabilities ||= begin
184
+ capa = {}
185
+ data.xpath("cra:repositoryInfo/c:capabilities/*", NS::COMBINED).map do |node|
186
+ # FIXME: conversion should be based on knowledge about data model + transforming bool code should not be duplicated
187
+ capa[node.name.sub("capability", "")] = case t = node.text
188
+ when "true", "1"; true
189
+ when "false", "0"; false
190
+ else t
191
+ end
192
+ end
193
+ capa
194
+ end
195
+ end
196
+
197
+ # Responds with true if Private Working Copies are updateable, false otherwise
198
+ # (if false the PWC object can only be updated during the checkin)
199
+ def pwc_updatable?
200
+ capabilities["PWCUpdatable"]
201
+ end
202
+
203
+ # Responds with true if different versions of the same document can
204
+ # be filed in different folders
205
+ def version_specific_filing?
206
+ capabilities["VersionSpecificFiling"]
207
+ end
208
+
209
+ # returns true if ACLs can at least be viewed
210
+ def acls_readable?
211
+ ["manage", "discover"].include? capabilities["ACL"]
212
+ end
213
+
214
+ # You should probably not use this directly, use :anonymous instead where a user name is required
215
+ # @return [String]
216
+ def anonymous_user
217
+ if acls_readable?
218
+ data.xpath('cra:repositoryInfo/c:principalAnonymous', NS::COMBINED).text
219
+ end
220
+ end
221
+
222
+ # You should probably not use this directly, use :world instead where a user name is required
223
+ # @return [String]
224
+ def world_user
225
+ if acls_readable?
226
+ data.xpath('cra:repositoryInfo/c:principalAnyone', NS::COMBINED).text
227
+ end
228
+ end
229
+
230
+ private
231
+ # @private
232
+ attr_reader :data
233
+
234
+ def pick_template(name, options = {})
235
+ # FIXME: we can have more than 1 template with differing media types
236
+ # I'm not sure how to pick the right one in the most generic/portable way though
237
+ # So for the moment we pick the 1st and hope for the best
238
+ # Options are ignored for the moment
239
+ data.xpath("n:uritemplate[n:type[child::text() = '#{name}']][1]/n:template", "n" => NS::CMIS_REST).text
240
+ end
241
+
242
+
243
+ # The type parameter should contain the type of the uri-template
244
+ #
245
+ # The keys of the values hash should be strings,
246
+ # if a key is not in the hash it is presumed to be equal to the empty string
247
+ # The values will be percent-encoded in the fill_in_template method
248
+ # If a given key is not present in the template it will be ignored silently
249
+ #
250
+ # e.g. fill_in_template("objectbyid", "id" => "@root@", "includeACL" => true)
251
+ # -> 'http://example.org/repo/%40root%40?includeRelationships&includeACL=true'
252
+ def fill_in_template(template, values)
253
+ result = template.gsub /\{([^}]+)\}/ do |match|
254
+ Internal::Utils.percent_encode(values[$1].to_s)
255
+ end
256
+ end
257
+ end
258
+ end