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.
- data/.gitignore +3 -0
- data/LICENSE +26 -0
- data/README.md +30 -0
- data/Rakefile +36 -0
- data/TODO +8 -0
- data/VERSION +1 -0
- data/lib/active_cmis.rb +27 -0
- data/lib/active_cmis/acl.rb +181 -0
- data/lib/active_cmis/acl_entry.rb +26 -0
- data/lib/active_cmis/active_cmis.rb +74 -0
- data/lib/active_cmis/atomic_types.rb +232 -0
- data/lib/active_cmis/attribute_prefix.rb +35 -0
- data/lib/active_cmis/collection.rb +175 -0
- data/lib/active_cmis/document.rb +314 -0
- data/lib/active_cmis/exceptions.rb +82 -0
- data/lib/active_cmis/folder.rb +21 -0
- data/lib/active_cmis/internal/caching.rb +86 -0
- data/lib/active_cmis/internal/connection.rb +171 -0
- data/lib/active_cmis/internal/utils.rb +69 -0
- data/lib/active_cmis/ns.rb +18 -0
- data/lib/active_cmis/object.rb +543 -0
- data/lib/active_cmis/policy.rb +13 -0
- data/lib/active_cmis/property_definition.rb +175 -0
- data/lib/active_cmis/rel.rb +17 -0
- data/lib/active_cmis/relationship.rb +47 -0
- data/lib/active_cmis/rendition.rb +65 -0
- data/lib/active_cmis/repository.rb +258 -0
- data/lib/active_cmis/server.rb +88 -0
- data/lib/active_cmis/type.rb +193 -0
- metadata +110 -0
@@ -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
|