active_cmis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ doc
2
+ .yardoc
3
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2010 XAOP bvba
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+
11
+ Software is furnished to do so, subject to the following
12
+ conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+
24
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # ActiveCMIS Release 0.1.0 #
2
+ **Homepage**: <http://xaop.com/labs/activecmis>
3
+ **Git**: <http://github.com/xaop/activecmis>
4
+ **Author**: XAOP bvba
5
+ **Copyright**: 2010
6
+ **License**: MIT License
7
+ ## Synopsis ##
8
+ ActiveCMIS is Ruby library aimed at easing the interaction with various CMIS providers. It creates Ruby objects for CMIS objects, and creates Ruby classes that correspond to CMIS types.
9
+ ## Features ##
10
+ - Read support for all CMIS object types
11
+ - Write support and the ability to create new objects.
12
+ - Support for paging
13
+
14
+ ## Installation ##
15
+ If you haven't installed Nokogiri yet it will be installed automatically, you will need a C compiler and the development files for libxml2.
16
+
17
+ If you don't have Yardoc or bluecloth installed you won't be able to build the documentation and will need to add `--no-rdoc --no-ri` to the next command (or install yard and bluecloth first).
18
+
19
+ > gem install active_cmis
20
+ ## Usage ##
21
+ require 'active_cmis'
22
+ repository = ActiveCMIS.load_config('configuration', 'optional_filename_for_config')
23
+ f = repository.root_folder
24
+ p f.items.map do |i| i.cmis.name end
25
+
26
+ And so on ...
27
+
28
+ Full documentation of the API can be found at <http://xaop.com/labs/activecmis/doc/index.html>
29
+
30
+ A tutorial can be found at [the xaop site](http://xaop.com/labs/activecmis "tutorial")
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+
4
+ begin
5
+ require 'yard'
6
+
7
+ YARD::Rake::YardocTask.new do |t|
8
+ t.files = ['lib/**/*.rb', '-', 'TODO'] # optional
9
+ t.options = ["--default-return", "::Object", "--query", "!@private", "--hide-void-return"]
10
+ end
11
+ rescue LoadError
12
+ puts "Yard, or a dependency, not available. Install it with gem install jeweler"
13
+ end
14
+
15
+ begin
16
+ require 'jeweler'
17
+ Jeweler::Tasks.new do |gemspec|
18
+ gemspec.name = "active_cmis"
19
+ gemspec.summary = "A library to interact with CMIS repositories through the AtomPub/REST binding"
20
+ gemspec.description = "A CMIS library implementing both reading and updating capabilities through the AtomPub/REST binding to CMIS."
21
+ gemspec.email = "joeri@xaop.com"
22
+ gemspec.homepage = "http://xaop.com/labs/activecmis/"
23
+ gemspec.authors = ["Joeri Samson"]
24
+
25
+ gemspec.add_runtime_dependency 'nokogiri', '>= 1.4.1'
26
+
27
+ gemspec.has_rdoc = 'yard'
28
+ gemspec.extra_rdoc_files = ['TODO']
29
+ gemspec.rdoc_options << "--default-return" << "::Object" << "--query" << "!@private" << "--hide-void-return"
30
+
31
+ gemspec.required_ruby_version = '>= 1.8.6'
32
+ end
33
+ Jeweler::GemcutterTasks.new
34
+ rescue LoadError
35
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
36
+ end
data/TODO ADDED
@@ -0,0 +1,8 @@
1
+ - For {ActiveCMIS::Rendition#get_file Rendition#get_file}:
2
+ * filename should be optional, a temporary file should be created (e.g. in /tmp) if no filename is given
3
+ * Downloading content should use streaming where possible
4
+ - For {ActiveCMIS::Type::ClassMethods#attributes Type::ClassMethods#attributes}: inherited parameter is misleading (+ default is wrong, some other code depends on it being true by default if it would work)
5
+ - improve use of correct Exceptions:
6
+ * don't throw RuntimeError anymore (except perhaps where the library code is the only code that could cause the exception)
7
+ * try to use correct exceptions when receiving errors from server (may need some wrapper with repository specific code)
8
+ - improve support for queries (i.e. allow interface such as the with_ATTR_NAME way of ActiveDCTM, allow select to return only a few attributes, ...)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,27 @@
1
+ require 'nokogiri'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'yaml'
5
+ require 'logger'
6
+ require 'active_cmis/internal/caching'
7
+ require 'active_cmis/internal/connection'
8
+ require 'active_cmis/exceptions'
9
+ require 'active_cmis/server'
10
+ require 'active_cmis/repository'
11
+ require 'active_cmis/object'
12
+ require 'active_cmis/document'
13
+ require 'active_cmis/folder'
14
+ require 'active_cmis/policy'
15
+ require 'active_cmis/relationship'
16
+ require 'active_cmis/type'
17
+ require 'active_cmis/atomic_types'
18
+ require 'active_cmis/property_definition'
19
+ require 'active_cmis/collection.rb'
20
+ require 'active_cmis/rendition.rb'
21
+ require 'active_cmis/acl.rb'
22
+ require 'active_cmis/acl_entry.rb'
23
+ require 'active_cmis/ns'
24
+ require 'active_cmis/active_cmis'
25
+ require 'active_cmis/internal/utils'
26
+ require 'active_cmis/rel'
27
+ require 'active_cmis/attribute_prefix'
@@ -0,0 +1,181 @@
1
+ module ActiveCMIS
2
+ # ACLs belong to a document and have no identity of their own
3
+ #
4
+ # = Updating:
5
+ # The effect on documents other than the one this ACL belongs to depends
6
+ # on the repository.
7
+ #
8
+
9
+ class Acl
10
+ include Internal::Caching
11
+
12
+ # @return [Object] The document or object from which we got this ACL
13
+ attr_reader :document
14
+ # @return [Repository]
15
+ attr_reader :repository
16
+
17
+ # @private
18
+ def initialize(repository, document, link, _data = nil)
19
+ @repository = repository
20
+ @document = document
21
+ @self_link = case link
22
+ when URI; link
23
+ else URI(link)
24
+ end
25
+ @data = _data if _data
26
+ end
27
+
28
+ # Returns an array with all Acl entries.
29
+ # @return [Array<AclEntry>]
30
+ def permissions
31
+ data.xpath("c:permission", NS::COMBINED).map do |permit|
32
+ principal = nil
33
+ permissions = []
34
+ direct = false
35
+ permit.children.each do |child|
36
+ next unless child.namespace && child.namespace.href == NS::CMIS_CORE
37
+
38
+ case child.name
39
+ when "principal"
40
+ child.children.map do |n|
41
+ next unless n.namespace && n.namespace.href == NS::CMIS_CORE
42
+
43
+ if n.name == "principalId" && principal.nil?
44
+ principal = convert_principal(n.text)
45
+ end
46
+ end
47
+ when "permission"
48
+ permissions << child.text
49
+ when "direct"
50
+ direct = AtomicType::Boolean.xml_to_bool(child.text)
51
+ end
52
+ end
53
+ AclEntry.new(principal, permissions, direct)
54
+ end
55
+ end
56
+ cache :permissions
57
+
58
+ # An indicator that the ACL fully describes the permissions for this object.
59
+ # This means that there are no other security constraints.
60
+ def exact?
61
+ @exact ||= begin
62
+ value = data.xpath("c:exact", NS::COMBINED)
63
+ if value.empty?
64
+ false
65
+ elsif value.length == 1
66
+ AtomicType::Boolean.xml_to_bool(value.first.text)
67
+ else
68
+ raise "Unexpected multiplicity of exactness ACL"
69
+ end
70
+ end
71
+ end
72
+
73
+ # @param [String, :anonymous, :world] user Can be "cmis:user" to indicate the currently logged in user.
74
+ # For :anonymous and :world you can use both the the active_cmis symbol or the name used by the CMIS repository
75
+ # @param permissions (see ActiveCMIS::AclEntry#initialize)
76
+ # @return [void]
77
+ def grant_permission(user, *permissions)
78
+ principal = convert_principal(user)
79
+
80
+ relevant = self.permissions.select {|p| p.principal == principal && p.direct?}
81
+ if relevant = relevant.first
82
+ self.permissions.delete relevant
83
+ permissions.concat(relevant.permissions)
84
+ end
85
+
86
+ @updated = true
87
+ self.permissions << AclEntry.new(principal, permissions, true)
88
+ end
89
+
90
+ # @param (see ActiveCMIS::Acl#grant_permission)
91
+ # @return [void]
92
+ def revoke_permission(user, *permissions)
93
+ principal = convert_principal(user)
94
+
95
+ keep = self.permissions.reject {|p| p.principal == principal && p.permissions.any? {|t| permissions.include? t} }
96
+
97
+ relevant = self.permissions.select {|p| p.principal == principal && p.permissions.any? {|t| permissions.include? t} }
98
+ changed = relevant.map {|p| AclEntry.new(principal, p.permissions - permissions, p.direct?) }
99
+
100
+ @updated = true
101
+ @permissions = keep + changed
102
+ end
103
+
104
+ # @param user (see ActiveCMIS::Acl#grant_permission)
105
+ # @return [void]
106
+ def revoke_all_permissions(user)
107
+ principal = convert_principal(user)
108
+ @updated = true
109
+ permissions.reject! {|p| p.principal == principal}
110
+ end
111
+
112
+ # Needed to actually execute changes on the server, this method is also executed when you save an object with a modified ACL
113
+ # @return [void]
114
+ def apply
115
+ body = Nokogiri::XML::Builder.new do |xml|
116
+ xml.acl("xmlns" => NS::CMIS_CORE) do
117
+ permissions.each do |permission|
118
+ xml.permission do
119
+ xml.principal { xml.principalId { convert_principal(permission.principal) }}
120
+ xml.direct { permission.direct? }
121
+ permission.each do |permit|
122
+ xml.permission { permit }
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ conn.put(self_link("onlyBasicPermissions" => false), body)
129
+ reload
130
+ end
131
+
132
+ # True if there are local changes to the ACL
133
+ def updated?
134
+ @updated
135
+ end
136
+
137
+ # @return [void]
138
+ def reload
139
+ @updated = false
140
+ @exact = nil
141
+ __reload
142
+ end
143
+
144
+ private
145
+ def self_link(options = {})
146
+ Internal::Utils.add_parameters(@self_link, options)
147
+ end
148
+
149
+ def conn
150
+ repository.conn
151
+ end
152
+
153
+ def data
154
+ conn.get_xml(self_link).xpath("c:acl", NS::COMBINED)
155
+ end
156
+ cache :data
157
+
158
+ def anonymous_user
159
+ repository.anonymous_user
160
+ end
161
+ def world_user
162
+ repository.world_user
163
+ end
164
+
165
+ def convert_principal(principal)
166
+ case principal
167
+ when :anonymous
168
+ anonymous_user
169
+ when :world
170
+ world
171
+ when anonymous_user
172
+ :anonymous
173
+ when world_user
174
+ :world
175
+ else
176
+ principal
177
+ end
178
+ end
179
+
180
+ end
181
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveCMIS
2
+ class AclEntry
3
+ # @param [<String>] permissions A list of permissions, valid values depend on the repository
4
+ # @private
5
+ def initialize(principal, permissions, direct)
6
+ @principal = principal.freeze
7
+ @permissions = permissions.freeze
8
+ @permissions.each {|p| p.freeze}
9
+ @direct = direct
10
+ end
11
+
12
+
13
+ # Normal users are represented with a string, a non-logged in user is known
14
+ # as :anonymous, the principal :world represents the group of all logged in
15
+ # users.
16
+ # @return [String, :world, :anonymous]
17
+ attr_reader :principal
18
+ # @return [<String>] A frozen array of strings with the permissions
19
+ attr_reader :permissions
20
+
21
+ # True if this is the direct representation of the ACL from the repositories point of view. This means there are no hidden differences that can't be expressed within the limitations of CMIS
22
+ def direct?
23
+ @direct
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,74 @@
1
+ module ActiveCMIS
2
+ # Default logger: Outputs to STDOUT and has level set to DEBUG
3
+ # @return [Logger]
4
+ def self.default_logger
5
+ @default_logger ||= Logger.new(STDOUT)
6
+ end
7
+
8
+ # Will search for a given configuration in a file, and return the equivalent Repository
9
+ #
10
+ # server_url and repository_id are required options
11
+ #
12
+ # server_login, server_password and server_auth can be used to authenticate against the server,
13
+ # server_auth is optional and defaults to :basic
14
+ #
15
+ # You can also authenticate to the repository, by replacing server_ with repository_, by default
16
+ # the repository will use the same authentication parameters as the server
17
+ #
18
+ # The amoung of logging can be configured by setting log_level (default WARN), this can be done either
19
+ # by naming a Logger::Severity constant or the equivalent integer
20
+ #
21
+ # The destination of the logger output can be set with log_file (defaults to STDOUT), (should not contain ~)
22
+ #
23
+ # Default locations for the config file are: ./cmis.yml and .cmis.yml in that order
24
+ # @return [Repository]
25
+ def self.load_config(config_name, file = nil)
26
+ if file.nil?
27
+ ["cmis.yml", File.join(ENV["HOME"], ".cmis.yml")].each do |sl|
28
+ if File.exist?(sl)
29
+ file ||= sl
30
+ end
31
+ end
32
+ if file.nil?
33
+ raise "No configuration provided, and none found in standard locations"
34
+ end
35
+ elsif !File.exist?(file)
36
+ raise "Configuration file #{file} does not exist"
37
+ end
38
+
39
+ config = YAML.load_file(file)
40
+ if config.is_a? Hash
41
+ config = config[config_name]
42
+ if config.is_a? Hash
43
+ if config.has_key? "log_file"
44
+ logger = Logger.new(config["trace_file"])
45
+ else
46
+ logger = default_logger
47
+ end
48
+ if config.has_key? "log_level"
49
+ logger.level = Logger.const_get(config["trace_level"].upcase) rescue config["trace_level"].to_i
50
+ else
51
+ logger.level = Logger::WARN
52
+ end
53
+
54
+ server = Server.new(config["server_url"], logger)
55
+ if user_name = config["server_login"] and password = config["server_password"]
56
+ auth_type = config["server_auth"] || :basic
57
+ server.authenticate(auth_type, user_name, password)
58
+ end
59
+ repository = server.repository(config["repository_id"])
60
+ if user_name = config["repository_login"] and password = config["repository_password"]
61
+ auth_type = config["repository_auth"] || :basic
62
+ repository.authenticate(auth_type, user_name, password)
63
+ end
64
+ return repository
65
+ elsif config.nil?
66
+ raise "Configuration not found in file"
67
+ else
68
+ raise "Configuration does not have right format (not a hash)"
69
+ end
70
+ else
71
+ raise "Configuration file does not have right format (not a hash)"
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,232 @@
1
+ module ActiveCMIS
2
+ module AtomicType
3
+ class CommonBase
4
+ def cmis2rb(value)
5
+ if value.children.empty? && value.attribute("nil")
6
+ nil
7
+ else
8
+ _cmis2rb(value)
9
+ end
10
+ end
11
+ def rb2cmis(xml, value)
12
+ if value.nil?
13
+ xml.value("xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xsi:nil" => "true")
14
+ else
15
+ _rb2cmis(xml, value)
16
+ end
17
+ end
18
+ def can_handle?(value)
19
+ raise NotImplementedError
20
+ end
21
+ end
22
+
23
+ class String < CommonBase
24
+ attr_reader :max_length
25
+ def initialize(max_length)
26
+ @max_length = max_length
27
+ end
28
+
29
+ def to_s
30
+ "String"
31
+ end
32
+
33
+ def _cmis2rb(value)
34
+ value.text
35
+ end
36
+ def _rb2cmis(xml, value)
37
+ v = value.to_s
38
+ if max_length && v.length > max_length
39
+ raise Error::InvalidArgument.new("String representation is longer than maximum (max: #{max_length}, string: \n'\n#{v}\n')\n")
40
+ end
41
+ xml["c"].value v
42
+ end
43
+ def can_handle?(value)
44
+ value.respond_to?(:to_s)
45
+ end
46
+
47
+ private :_cmis2rb, :_rb2cmis
48
+ end
49
+
50
+ # Precision is ignored?
51
+ class Decimal < CommonBase
52
+ attr_reader :precision, :min_value, :max_value
53
+ def initialize(precision, min_value, max_value)
54
+ @precision, @min_value, @max_value = precision, min_value, max_value
55
+ end
56
+
57
+ def to_s
58
+ "Decimal"
59
+ end
60
+
61
+ def _cmis2rb(value)
62
+ value.text.to_f
63
+ end
64
+ def _rb2cmis(xml, value)
65
+ v = value.to_f
66
+ if (min_value && v < min_value) || (max_value && v > max_value)
67
+ raise Error::InvalidArgument.new("OutOfBounds: #{v} should be between #{min_value} and #{max_value}")
68
+ end
69
+ xml["c"].value("%f" % v)
70
+ end
71
+ def can_handle?(value)
72
+ value.respond_to?(:to_s)
73
+ end
74
+
75
+ private :_cmis2rb, :_rb2cmis
76
+ end
77
+
78
+ class Integer < CommonBase
79
+ attr_reader :min_value, :max_value
80
+ def initialize(min_value, max_value)
81
+ @min_value, @max_value = min_value, max_value
82
+ end
83
+
84
+ def to_s
85
+ "Integer"
86
+ end
87
+
88
+ def _cmis2rb(value)
89
+ value.text.to_i
90
+ end
91
+ def _rb2cmis(xml, value)
92
+ v = value.to_int
93
+ if (min_value && v < min_value) || (max_value && v > max_value)
94
+ raise Error::InvalidArgument.new("OutOfBounds: #{v} should be between #{min_value} and #{max_value}")
95
+ end
96
+ xml["c"].value("%i" % v)
97
+ end
98
+ def can_handle?(value)
99
+ value.respond_to?(:to_int)
100
+ end
101
+
102
+ private :_cmis2rb, :_rb2cmis
103
+ end
104
+
105
+ class DateTime < CommonBase
106
+ attr_reader :resolution
107
+
108
+ @instances ||= {}
109
+ def self.new(precision)
110
+ raise ArgumentError.new("Got precision = #{precision.inspect}") unless [YEAR, DATE, TIME].include? precision.to_s.downcase
111
+ @instances[precision] ||= super
112
+ end
113
+
114
+ def to_s
115
+ "DateTime"
116
+ end
117
+
118
+ def initialize(resolution)
119
+ @resolution = resolution
120
+ end
121
+ YEAR = "year"
122
+ DATE = "date"
123
+ TIME = "time"
124
+
125
+ def _cmis2rb(value)
126
+ case @resolution
127
+ when YEAR, DATE; ::DateTime.parse(value.text).to_date
128
+ when TIME; ::DateTime.parse(value.text)
129
+ end
130
+ end
131
+ def _rb2cmis(xml, value)
132
+ # FIXME: respect resolution, I still have to find out how to do that
133
+ xml["c"].value(value.strftime("%Y-%m-%dT%H:%M:%S%Z"))
134
+ end
135
+ def can_handle?(value)
136
+ value.respond_to?(:strftime)
137
+ end
138
+
139
+ private :_cmis2rb, :_rb2cmis
140
+ end
141
+
142
+ class Singleton < CommonBase
143
+ def self.new
144
+ @singleton ||= super
145
+ end
146
+ end
147
+
148
+ class Boolean < Singleton
149
+ def self.xml_to_bool(value)
150
+ case value
151
+ when "true", "1"; true
152
+ when "false", "0"; false
153
+ else raise ActiveCMIS::Error.new("An invalid boolean was found in CMIS")
154
+ end
155
+ end
156
+
157
+ def to_s
158
+ "Boolean"
159
+ end
160
+
161
+ def _cmis2rb(value)
162
+ self.class.xml_to_bool(value.text)
163
+ end
164
+ def _rb2cmis(xml, value)
165
+ xml["c"].value( (!!value).to_s )
166
+ end
167
+ def can_handle?(value)
168
+ [true, false].include?(value)
169
+ end
170
+
171
+ private :_cmis2rb, :_rb2cmis
172
+ end
173
+
174
+ class URI < Singleton
175
+ def to_s
176
+ "Uri"
177
+ end
178
+
179
+ def _cmis2rb(value)
180
+ URI.parse(value.text)
181
+ end
182
+ def _rb2cmis(xml, value)
183
+ xml["c"].value( value.to_s )
184
+ end
185
+ def can_handle?(value)
186
+ value.respond_to?(:to_s)
187
+ end
188
+
189
+ private :_cmis2rb, :_rb2cmis
190
+ end
191
+
192
+ class ID < Singleton
193
+ def to_s
194
+ "Id"
195
+ end
196
+
197
+ def _cmis2rb(value)
198
+ value.text
199
+ end
200
+ def _rb2cmis(xml, value)
201
+ case value
202
+ when ::ActiveCMIS::Object; value.id
203
+ else xml["c"].value( value.to_s )
204
+ end
205
+ end
206
+ def can_handle?(value)
207
+ value.class < ::ActiveCMIS::Object || value.respond_to?(:to_s)
208
+ end
209
+
210
+ private :_cmis2rb, :_rb2cmis
211
+ end
212
+
213
+ class HTML < Singleton
214
+ def to_s
215
+ "Html"
216
+ end
217
+
218
+ def _cmis2rb(value)
219
+ value.children
220
+ end
221
+ def _rb2cmis(xml, value)
222
+ # FIXME: Test that this works
223
+ xml["c"].value value
224
+ end
225
+ def can_handle?(value)
226
+ true # FIXME: this is probably incorrect
227
+ end
228
+
229
+ private :_cmis2rb, :_rb2cmis
230
+ end
231
+ end
232
+ end