cmis_active 0.3.7

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,87 @@
1
+ module ActiveCMIS
2
+ # Default logger: no output
3
+ # @return [Logger]
4
+ def self.default_logger
5
+ @default_logger ||= Logger.new(nil)
6
+ end
7
+
8
+ # server_url and repository_id are required options
9
+ #
10
+ # server_login, server_password and server_auth can be used to authenticate against the server,
11
+ # server_auth is optional and defaults to :basic
12
+ #
13
+ # You can also authenticate to the repository, by replacing server_ with repository_, by default
14
+ # the repository will use the same authentication parameters as the server
15
+ #
16
+ # The amoung of logging can be configured by setting log_level (default WARN), this can be done either
17
+ # by naming a Logger::Severity constant or the equivalent integer
18
+ #
19
+ # The destination of the logger output can be set with log_file (defaults to STDOUT), (should not contain ~)
20
+ #
21
+ # Default locations for the config file are: ./cmis.yml and .cmis.yml in that order
22
+ # @return [Repository]
23
+ def self.connect(config)
24
+ if config.is_a? Hash
25
+ if config.has_key? "log_file"
26
+ trace_file = config["log_file"]
27
+ if trace_file == "-"
28
+ trace_file = STDOUT
29
+ end
30
+ logger = Logger.new(trace_file)
31
+ else
32
+ logger = default_logger
33
+ end
34
+ if config.has_key? "log_level"
35
+ logger.level = (Logger.const_get(config["log_level"].upcase) rescue config["log_level"].to_i)
36
+ else
37
+ logger.level = Logger::WARN
38
+ end
39
+
40
+ if user_name = config["server_login"] and password = config["server_password"]
41
+ auth_type = config["server_auth"] || :basic
42
+ authentication_info = [auth_type, user_name, password]
43
+ end
44
+ server = Server.new(config["server_url"], logger, authentication_info, { :timeout => config["timeout"], :ssl_verfiy => config["ssl_verfiy"] })
45
+ if user_name = config["repository_login"] and password = config["repository_password"]
46
+ auth_type = config["repository_auth"] || :basic
47
+ authentication_info = [auth_type, user_name, password]
48
+ end
49
+ repository = server.repository(config["repository_id"], authentication_info)
50
+ return repository
51
+ else
52
+ raise "Configuration does not have correct format (#{config.class} is not a hash)"
53
+ end
54
+ end
55
+
56
+ # Will search for a given configuration in a file, and return the equivalent Repository
57
+ #
58
+ # The options that can be used are the same as for the connect method
59
+ #
60
+ # Default locations for the config file are: ./cmis.yml and .cmis.yml in that order
61
+ # @return [Repository]
62
+ def self.load_config(config_name, file = nil)
63
+ if file.nil?
64
+ ["cmis.yml", File.join(ENV["HOME"], ".cmis.yml")].each do |sl|
65
+ if File.exist?(sl)
66
+ file ||= sl
67
+ end
68
+ end
69
+ if file.nil?
70
+ raise "No configuration provided, and none found in standard locations"
71
+ end
72
+ elsif !File.exist?(file)
73
+ raise "Configuration file #{file} does not exist"
74
+ end
75
+
76
+ config = YAML.load_file(file)
77
+ if config.is_a? Hash
78
+ if config = config[config_name]
79
+ connect(config)
80
+ else
81
+ raise "Configuration not found in file"
82
+ end
83
+ else
84
+ raise "Configuration file #{file} does not have right format (not a hash)"
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,245 @@
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 = nil)
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 && max_length > 0 && v.length > max_length #xCMIS says maxLength=0
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
+ # Qarning: Precision is ignored?
51
+ class Decimal < CommonBase
52
+ attr_reader :precision, :min_value, :max_value
53
+ def initialize(precision = nil, min_value = nil, max_value = nil)
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 = nil, max_value = nil)
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_i
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_i)
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 = TIME)
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
+
232
+ # Map of XML property elements to the corresponding AtomicTypes
233
+ MAPPING = {
234
+ "propertyString" => ActiveCMIS::AtomicType::String,
235
+ "propertyBoolean" => ActiveCMIS::AtomicType::Boolean,
236
+ "propertyId" => ActiveCMIS::AtomicType::ID,
237
+ "propertyDateTime" => ActiveCMIS::AtomicType::DateTime,
238
+ "propertyInteger" => ActiveCMIS::AtomicType::Integer,
239
+ "propertyDecimal" => ActiveCMIS::AtomicType::Decimal,
240
+ "propertyHtml" => ActiveCMIS::AtomicType::HTML,
241
+ "propertyUri" => ActiveCMIS::AtomicType::URI,
242
+ }
243
+
244
+ end
245
+ end
@@ -0,0 +1,35 @@
1
+ module ActiveCMIS
2
+ # A class used to get and set attributes that have a prefix like cmis: in their attribute IDs
3
+ class AttributePrefix
4
+ # @return [Object] The object that the attribute getting and setting will take place on
5
+ attr_reader :object
6
+ # @return [String]
7
+ attr_reader :prefix
8
+
9
+ # @private
10
+ def initialize(object, prefix)
11
+ @object = object
12
+ @prefix = prefix
13
+ end
14
+
15
+ # For known attributes will act as a getter and setter
16
+ def method_missing(method, *parameters)
17
+ string = method.to_s
18
+ if string[-1] == ?=
19
+ assignment = true
20
+ string = string[0..-2]
21
+ end
22
+ attribute = "#{prefix}:#{string}"
23
+ if object.class.attributes.keys.include? attribute
24
+ if assignment
25
+ object.update(attribute => parameters.first)
26
+ else
27
+ object.attribute(attribute)
28
+ end
29
+ else
30
+ # TODO: perhaps here we should try to look a bit further to see if there is a second :
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,206 @@
1
+ module ActiveCMIS
2
+
3
+ # A Collection represents an atom feed, and can be used to lazily load data through paging
4
+ class Collection
5
+ include Internal::Caching
6
+ include ::Enumerable
7
+
8
+ # The repository that contains this feed
9
+ # @return [Repository]
10
+ attr_reader :repository
11
+ # The basic link that represents the beginning of this feed
12
+ # @return [URI]
13
+ attr_reader :url
14
+
15
+ def initialize(repository, url, first_page = nil, &map_entry)
16
+ @repository = repository
17
+ @url = URI.parse(url)
18
+
19
+ @next = @url
20
+ @elements = []
21
+ @pages = []
22
+
23
+ @map_entry = map_entry || Proc.new do |e|
24
+ ActiveCMIS::Object.from_atom_entry(repository, e)
25
+ end
26
+
27
+ if first_page
28
+ @next = first_page.xpath("at:feed/at:link[@rel = 'next']/@href", NS::COMBINED).first
29
+ @pages[0] = first_page
30
+ end
31
+ end
32
+
33
+ # @return [Integer] The length of the collection
34
+ def length
35
+ receive_page
36
+ if @length.nil?
37
+ i = 1
38
+ while @next
39
+ receive_page
40
+ i += 1
41
+ end
42
+ @elements.length
43
+ else
44
+ @length
45
+ end
46
+ end
47
+ alias size length
48
+ cache :length
49
+
50
+ def empty?
51
+ at(0)
52
+ @elements.empty?
53
+ end
54
+
55
+ # @return [Array]
56
+ def to_a
57
+ while @next
58
+ receive_page
59
+ end
60
+ @elements
61
+ end
62
+
63
+ def at(index)
64
+ index = sanitize_index(index)
65
+ if index < @elements.length
66
+ @elements[index]
67
+ elsif index > length
68
+ nil
69
+ else
70
+ while @next && @elements.length < index
71
+ receive_page
72
+ end
73
+ @elements[index]
74
+ end
75
+ end
76
+
77
+ def [](index, length = nil)
78
+ if length
79
+ index = sanitize_index(index)
80
+ range_get(index, index + length - 1)
81
+ elsif Range === index
82
+ range_get(sanitize_index(index.begin), index.exclude_end? ? sanitize_index(index.end) - 1 : sanitize_index(index.end))
83
+ else
84
+ at(index)
85
+ end
86
+ end
87
+ alias slice []
88
+
89
+ def first
90
+ at(0)
91
+ end
92
+
93
+ # Gets all object and returns last
94
+ def last
95
+ at(-1)
96
+ end
97
+
98
+ # @return [Array]
99
+ def each
100
+ length.times { |i| yield self[i] }
101
+ end
102
+
103
+ # @return [Array]
104
+ def reverse_each
105
+ (length - 1).downto(0) { |i| yield self[i] }
106
+ end
107
+
108
+ # @return [String]
109
+ def inspect
110
+ "#<Collection %s>" % url
111
+ end
112
+
113
+ # @return [String]
114
+ def to_s
115
+ to_a.to_s
116
+ end
117
+
118
+ # @return [Array]
119
+ def uniq
120
+ to_a.uniq
121
+ end
122
+
123
+ # @return [Array]
124
+ def sort
125
+ to_a.sort
126
+ end
127
+
128
+ # @return [Array]
129
+ def reverse
130
+ to_a.reverse
131
+ end
132
+
133
+ ## @return [void]
134
+ def reload
135
+ @pages = []
136
+ @elements = []
137
+ @next = @url
138
+ __reload
139
+ end
140
+
141
+ # Attempts to delete the collection.
142
+ # This may not work on every collection, ActiveCMIS does not (yet) try to check this client side.
143
+ #
144
+ # For folder collections 2 options are available, again no client side checking
145
+ # is done to see whether the collection can handle these options
146
+ #
147
+ # @param [Hash] options
148
+ # @option options [String] :unfileObjects Parameter valid for items collection of folder
149
+ # "delete" (default): Delete all fileable objects
150
+ # "unfile": unfile all fileable objects
151
+ # "deleteSingleFiled": Delete all fileable objects, whose only parent folder is the current folder. Unfile all other objects from this folder
152
+ # @option options [Bool] :continueOnFailure default = false, if false abort on failure of single document/folder, else try to continue with deleting child folders/documents
153
+ def destroy(options = {})
154
+ if options.empty?
155
+ conn.delete(@url)
156
+ else
157
+ unfileObjects = options.delete(:unfileObjects)
158
+ continueOnFailure = options.delete(:continueOnFailure)
159
+ raise ArgumentError("Unknown parameters #{options.keys.join(', ')}") unless options.empty?
160
+
161
+
162
+ # XXX: have less cumbersome code, more generic and more efficient code
163
+ new_url = @url
164
+ new_url = Internal::Utils.append_parameters(new_url, :unfileObjects => unfileObjects) unless unfileObjects.nil?
165
+ new_url = Internal::Utils.append_parameters(new_url, :continueOnFailure => continueOnFailure) unless continueOnFailure.nil?
166
+
167
+ # TODO: check that we can handle 200,202,204 responses correctly
168
+ conn.delete(@url)
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def sanitize_index(index)
175
+ index < 0 ? size + index : index
176
+ end
177
+
178
+ def range_get(from, to)
179
+ (from..to).map { |i| at(i) }
180
+ end
181
+
182
+ def receive_page(i = nil)
183
+ i ||= @pages.length
184
+ @pages[i] ||= begin
185
+ return nil unless @next
186
+ xml = conn.get_xml(@next)
187
+
188
+ @next = xml.xpath("at:feed/at:link[@rel = 'next']/@href", NS::COMBINED).first
189
+ @next = @next.nil? ? nil : @next.text
190
+
191
+ new_elements = xml.xpath('at:feed/at:entry', NS::COMBINED).map(&@map_entry)
192
+ @elements.concat(new_elements)
193
+
194
+ num_items = xml.xpath("at:feed/cra:numItems", NS::COMBINED).first
195
+ @length ||= num_items.text.to_i if num_items # We could also test on the repository
196
+
197
+ xml
198
+ end
199
+ end
200
+
201
+ def conn
202
+ repository.conn
203
+ end
204
+
205
+ end
206
+ end