active_cmis 0.1.0

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