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,82 @@
1
+ module ActiveCMIS
2
+ # The base class for all CMIS exceptions,
3
+ # HTTP communication errors and the like are not catched by this
4
+ class Error < StandardError
5
+ # === Cause
6
+ # One or more of the input parameters to the service method is missing or invalid
7
+ class InvalidArgument < Error; end
8
+
9
+ # === Cause
10
+ # The service call has specified an object that does not exist in the Repository
11
+ class ObjectNotFound < Error; end
12
+
13
+ # === Cause
14
+ # The service method invoked requires an optional capability not supported by the repository
15
+ class NotSupported < Error; end
16
+
17
+ # === Cause
18
+ # The caller of the service method does not have sufficient permissions to perform the operation
19
+ class PermissionDenied < Error; end
20
+
21
+ # === Cause
22
+ # Any cause not expressible by another CMIS exception
23
+ class Runtime < Error; end
24
+
25
+ # === Intent
26
+ # The operation violates a Repository- or Object-level constraint defined in the CMIS domain model
27
+ #
28
+ # === Methods
29
+ # see the CMIS specification
30
+ class Constraint < Error; end
31
+ # === Intent
32
+ # The operation attempts to set the content stream for a Document
33
+ # that already has a content stream without explicitly specifying the
34
+ # "overwriteFlag" parameter
35
+ #
36
+ # === Methods
37
+ # see the CMIS specification
38
+ class ContentAlreadyExists < Error; end
39
+ # === Intent
40
+ # The property filter or rendition filter input to the operation is not valid
41
+ #
42
+ # === Methods
43
+ # see the CMIS specification
44
+ class FilterNotValid < Error; end
45
+ # === Intent
46
+ # The repository is not able to store the object that the user is creating/updating due to a name constraint violation
47
+ #
48
+ # === Methods
49
+ # see the CMIS specification
50
+ class NameConstraintViolation < Error; end
51
+ # === Intent
52
+ # The repository is not able to store the object that the user is creating/updating due to an internal storage problam
53
+ #
54
+ # === Methods
55
+ # see the CMIS specification
56
+ class Storage < Error; end
57
+ # === Intent
58
+ #
59
+ #
60
+ # === Methods
61
+ # see the CMIS specification
62
+ class StreamNotSupported < Error; end
63
+ # === Intent
64
+ #
65
+ #
66
+ # === Methods
67
+ # see the CMIS specification
68
+ class UpdateConflict < Error; end
69
+ # === Intent
70
+ #
71
+ #
72
+ # === Methods
73
+ # see the CMIS specification
74
+ class Versioning < Error; end
75
+ end
76
+
77
+ class HTTPError < StandardError
78
+ class ServerError < HTTPError; end
79
+ class ClientError < HTTPError; end
80
+ class AuthenticationError < HTTPError; end
81
+ end
82
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveCMIS
2
+ class Folder < ActiveCMIS::Object
3
+ # Returns a collection of all items contained in this folder (1 level deep)
4
+ # @return [Collection<Document,Folder,Policy>]
5
+ def items
6
+ item_feed = Internal::Utils.extract_links(data, 'down', 'application/atom+xml','type' => 'feed')
7
+ raise "No child feed link for folder" if item_feed.empty?
8
+ Collection.new(repository, item_feed.first)
9
+ end
10
+ cache :items
11
+
12
+ private
13
+ def create_url
14
+ if f = parent_folders.first
15
+ f.items.url
16
+ else
17
+ raise "Not possible"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,86 @@
1
+ module ActiveCMIS
2
+ module Internal
3
+ module Caching
4
+ def self.included(cl)
5
+ cl.extend ClassMethods
6
+ end
7
+
8
+ # A module for internal use only.
9
+ module ClassMethods
10
+
11
+ # Creates a proxy method for the given method names that caches the result.
12
+ #
13
+ # Parameters are passed and ignored, cached values will be returned regardless of the parameters.
14
+ # @param [Symbol, <Symbol>] Names of methods that will be cached
15
+ # @return [void]
16
+ def cache(*names)
17
+ (@cached_methods ||= []).concat(names).uniq!
18
+ names.each do |name|
19
+ alias_method("#{name}__uncached", name)
20
+ class_eval <<-RUBY, __FILE__, __LINE__+1
21
+ if private_method_defined? :"#{name}"
22
+ private_method = true
23
+ end
24
+ def #{name}(*a, &b)
25
+ if defined? @#{name}
26
+ @#{name}
27
+ else
28
+ @#{name} = #{name}__uncached(*a, &b)
29
+ end
30
+ end
31
+ if private_method
32
+ private :"#{name}__uncached"
33
+ private :"#{name}"
34
+ end
35
+ RUBY
36
+ end
37
+ reloadable
38
+ end
39
+
40
+ # Creates methods to retrieve attributes with the given names.
41
+ #
42
+ # If the given attribute does not yet exist the method #load_from_data will be called
43
+ #
44
+ # @param [Symbol, <Symbol>] Names of desired attributes
45
+ # @return [void]
46
+ def cached_reader(*names)
47
+ (@cached_methods ||= []).concat(names).uniq!
48
+ names.each do |name|
49
+ define_method "#{name}" do
50
+ if instance_variable_defined? "@#{name}"
51
+ instance_variable_get("@#{name}")
52
+ else
53
+ load_from_data # FIXME: make flexible?
54
+ instance_variable_get("@#{name}")
55
+ end
56
+ end
57
+ end
58
+ reloadable
59
+ end
60
+
61
+ private
62
+ def reloadable
63
+ class_eval <<-RUBY, __FILE__, __LINE__
64
+ def __reload
65
+ #{@cached_methods.inspect}.map do |method|
66
+ :"@\#{method}"
67
+ end.select do |iv|
68
+ instance_variable_defined? iv
69
+ end.each do |iv|
70
+ remove_instance_variable iv
71
+ end + (defined?(super) ? super : [])
72
+ end
73
+ private :__reload
74
+ RUBY
75
+ unless instance_methods.include? "reload"
76
+ class_eval <<-RUBY, __FILE__, __LINE__
77
+ def reload
78
+ __reload
79
+ end
80
+ RUBY
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,171 @@
1
+ module ActiveCMIS
2
+ module Internal
3
+ class Connection
4
+ # @return [String, nil] The user that is used with the authentication to the server
5
+ attr_reader :user
6
+ # @return [Logger] A logger used to send debug and info messages
7
+ attr_reader :logger
8
+
9
+ # @param [Logger] Initialize with a logger of your choice
10
+ def initialize(logger)
11
+ @logger = logger || ActiveCMIS.default_logger
12
+ end
13
+
14
+ # Use authentication to access the CMIS repository
15
+ #
16
+ # @param method [Symbol] Currently only :basic is supported
17
+ # @param params The parameters that need to be sent to the Net::HTTP authentication method used, username and password for basic authentication
18
+ # @return [void]
19
+ # @example Basic authentication
20
+ # repo.authenticate(:basic, "username", "password")
21
+ def authenticate(method, *params)
22
+ case method
23
+ when :basic
24
+ @authentication = {:method => :basic_auth, :params => params}
25
+ @user = params.first
26
+ else raise "Authentication method not supported"
27
+ end
28
+ end
29
+
30
+ # The return value is the unparsed body, unless an error occured
31
+ # If an error occurred, exceptions are thrown (see _ActiveCMIS::Exception
32
+ #
33
+ # @private
34
+ # @return [String] returns the body of the request, unless an error occurs
35
+ def get(url)
36
+ uri = normalize_url(url)
37
+
38
+ req = Net::HTTP::Get.new(uri.request_uri)
39
+ handle_request(uri, req)
40
+ end
41
+
42
+ # Does not throw errors, returns the full response (includes status code and headers)
43
+ # @private
44
+ # @return [Net::HTTP::Response]
45
+ def get_response(url)
46
+ logger.debug "GET (response) #{url}"
47
+ uri = normalize_url(url)
48
+
49
+ req = Net::HTTP::Get.new(uri.request_uri)
50
+ http = authenticate_request(uri, req)
51
+ response = http.request(req)
52
+ logger.debug "GOT (#{response.code}) #{url}"
53
+ response
54
+ end
55
+
56
+ # Returns the parsed body of the result
57
+ # @private
58
+ # @return [Nokogiri::XML::Document]
59
+ def get_xml(url)
60
+ Nokogiri::XML.parse(get(url))
61
+ end
62
+
63
+ # @private
64
+ # @return [Nokogiri::XML::Node]
65
+ def get_atom_entry(url)
66
+ # FIXME: add validation that first child is really an entry
67
+ get_xml(url).child
68
+ end
69
+
70
+ # @private
71
+ def put(url, body, headers = {})
72
+ uri = normalize_url(url)
73
+
74
+ req = Net::HTTP::Put.new(uri.request_uri)
75
+ headers.each {|k,v| req.add_field k, v}
76
+ assign_body(req, body)
77
+ handle_request(uri, req)
78
+ end
79
+
80
+ # @private
81
+ def post(url, body, headers = {})
82
+ uri = normalize_url(url)
83
+
84
+ req = Net::HTTP::Post.new(uri.request_uri)
85
+ headers.each {|k,v| req.add_field k, v}
86
+ assign_body(req, body)
87
+ handle_request(uri, req)
88
+ end
89
+
90
+ # Does not throw errors, returns the full response (includes status code and headers)
91
+ # @private
92
+ def post_response(url, body, headers = {})
93
+ logger.debug "POST (response) #{url}"
94
+ uri = normalize_url(url)
95
+
96
+ req = Net::HTTP::Post.new(uri.request_uri)
97
+ headers.each {|k,v| req.add_field k, v}
98
+ assign_body(req, body)
99
+
100
+ http = authenticate_request(uri, req)
101
+ response = http.request(req)
102
+ logger.debug "POSTED (#{response.code}) #{url}"
103
+ response
104
+ end
105
+
106
+ # @private
107
+ def delete(url)
108
+ uri = normalize_url(url)
109
+
110
+ req = Net::HTTP::Delete.new(uri.request_uri)
111
+ handle_request(uri, req)
112
+ end
113
+
114
+ private
115
+ def normalize_url(url)
116
+ case url
117
+ when URI; url
118
+ else URI.parse(url.to_s)
119
+ end
120
+ end
121
+
122
+ def authenticate_request(uri, req)
123
+ http = Net::HTTP.new(uri.host, uri.port)
124
+ if uri.scheme == 'https'
125
+ http.use_ssl = true
126
+ end
127
+ if auth = @authentication
128
+ req.send(auth[:method], *auth[:params])
129
+ end
130
+ http
131
+ end
132
+
133
+ def assign_body(req, body)
134
+ if body.respond_to? :length
135
+ req.body = body
136
+ else
137
+ req.body_stream = body
138
+ if body.respond_to? :stat
139
+ req["Content-Length"] = body.stat.size.to_s
140
+ elsif req["Content-Size"].nil?
141
+ req["Transfer-Encoding"] = 'chunked'
142
+ end
143
+ end
144
+ end
145
+
146
+ def handle_request(uri, req)
147
+ logger.debug "#{req.method} #{uri}"
148
+ http = authenticate_request(uri, req)
149
+ response = http.request(req)
150
+ status = response.code.to_i
151
+ logger.debug "RECEIVED #{response.code}"
152
+ if 200 <= status && status < 300
153
+ return response.body
154
+ else
155
+ # Problem: some codes 400, 405, 403, 409, 500 have multiple meanings
156
+ logger.error "Error occurred when handling request:\n#{response.body}"
157
+ case status
158
+ when 400; raise Error::InvalidArgument.new(response.body)
159
+ # FIXME: can also be filterNotValid
160
+ when 404; raise Error::ObjectNotFound.new(response.body)
161
+ when 403; raise Error::PermissionDenied.new(response.body)
162
+ # FIXME: can also be streamNotSupported (?? shouldn't that be 405??)
163
+ when 405; raise Error::NotSupported.new(response.body)
164
+ else
165
+ raise HTTPError.new("A HTTP #{status} error occured, for more precision update the code:\n" + response.body)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,69 @@
1
+ module ActiveCMIS
2
+ module Internal
3
+ # @private
4
+ module Utils
5
+ # @private
6
+ def self.escape_url_parameter(parameter)
7
+ control = "\x00-\x1F\x7F"
8
+ space = " "
9
+ delims = "<>#%\""
10
+ unwise = '{}|\\\\^\[\]`'
11
+ query = ";/?:@&=+,$"
12
+ URI.escape(parameter, /[#{control+space+delims+unwise+query}]/o)
13
+ end
14
+
15
+ # Given an url (string or URI) returns that url with the given parameters appended
16
+ #
17
+ # This method does not perform any encoding on the paramter or key values.
18
+ # This method does not check the existing parameters for duplication in keys
19
+ # @private
20
+ def self.append_parameters(uri, parameters)
21
+ uri = case uri
22
+ when String; string = true; URI.parse(uri)
23
+ when URI; uri.dup
24
+ end
25
+ uri.query = [uri.query, *parameters.map {|key, value| "#{key}=#{value}"} ].compact.join "&"
26
+ if string
27
+ uri.to_s
28
+ else
29
+ uri
30
+ end
31
+ end
32
+
33
+ # FIXME?? percent_encode and escape_url_parameter serve nearly the same purpose, replace one?
34
+ # @private
35
+ def self.percent_encode(string)
36
+ URI.escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
37
+ end
38
+
39
+ # Returns id if id is already an object, object_by_id if id is a string, nil otherwise
40
+ # @private
41
+ def self.string_or_id_to_object(id)
42
+ case id
43
+ when String; repository.object_by_id(id)
44
+ when ::ActiveCMIS::Object; id
45
+ end
46
+ end
47
+
48
+ # @private
49
+ def self.extract_links(xml, rel, type_main = nil, type_params = {})
50
+ links = xml.xpath("at:link[@rel = '#{rel}']", NS::COMBINED)
51
+
52
+ if type_main
53
+ type_main = Regexp.escape(type_main)
54
+ if type_params.empty?
55
+ regex = /#{type_main}/
56
+ else
57
+ parameters = type_params.map {|k,v| "#{Regexp.escape(k)}=#{Regexp.escape(v)}" }.join(";\s*")
58
+ regex = /#{type_main};\s*#{parameters}/
59
+ end
60
+ links = links.select do |node|
61
+ regex === node.attribute("type").to_s
62
+ end
63
+ end
64
+
65
+ links.map {|l| l.attribute("href").to_s}
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveCMIS
2
+ # This module defines namespaces that often occur in the REST/Atompub API to CMIS
3
+ module NS
4
+ CMIS_CORE = "http://docs.oasis-open.org/ns/cmis/core/200908/"
5
+ CMIS_REST = "http://docs.oasis-open.org/ns/cmis/restatom/200908/"
6
+ CMIS_MESSAGING = "http://docs.oasis-open.org/ns/cmis/messaging/200908/"
7
+ APP = "http://www.w3.org/2007/app"
8
+ ATOM = "http://www.w3.org/2005/Atom"
9
+
10
+ COMBINED = {
11
+ "xmlns:c" => CMIS_CORE,
12
+ "xmlns:cra" => CMIS_REST,
13
+ "xmlns:cm" => CMIS_MESSAGING,
14
+ "xmlns:app" => APP,
15
+ "xmlns:at" => ATOM
16
+ }
17
+ end
18
+ end