cmis_active 0.3.7

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