timfel-active_cmis 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/README.md +34 -0
- data/Rakefile +36 -0
- data/TODO +7 -0
- data/active_cmis.gemspec +77 -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 +87 -0
- data/lib/active_cmis/atomic_types.rb +245 -0
- data/lib/active_cmis/attribute_prefix.rb +35 -0
- data/lib/active_cmis/collection.rb +206 -0
- data/lib/active_cmis/document.rb +299 -0
- data/lib/active_cmis/exceptions.rb +82 -0
- data/lib/active_cmis/folder.rb +36 -0
- data/lib/active_cmis/internal/caching.rb +86 -0
- data/lib/active_cmis/internal/connection.rb +222 -0
- data/lib/active_cmis/internal/utils.rb +82 -0
- data/lib/active_cmis/ns.rb +18 -0
- data/lib/active_cmis/object.rb +563 -0
- data/lib/active_cmis/policy.rb +13 -0
- data/lib/active_cmis/property_definition.rb +179 -0
- data/lib/active_cmis/query_result.rb +40 -0
- data/lib/active_cmis/rel.rb +17 -0
- data/lib/active_cmis/relationship.rb +49 -0
- data/lib/active_cmis/rendition.rb +80 -0
- data/lib/active_cmis/repository.rb +327 -0
- data/lib/active_cmis/server.rb +113 -0
- data/lib/active_cmis/type.rb +200 -0
- data/lib/active_cmis/version.rb +8 -0
- data/lib/active_cmis.rb +31 -0
- metadata +111 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
class PropertyDefinition
|
3
|
+
# @return [String]
|
4
|
+
attr_reader :object_type, :id, :local_name, :local_namespace, :query_name,
|
5
|
+
:display_name, :description, :cardinality, :property_type, :updatability,
|
6
|
+
:default_value
|
7
|
+
# @return [Boolean]
|
8
|
+
attr_reader :inherited, :required, :queryable, :orderable, :choices, :open_choice
|
9
|
+
|
10
|
+
# @private
|
11
|
+
def initialize(object_type, property_definition)
|
12
|
+
@object_type = object_type
|
13
|
+
@property_definition = property_definition
|
14
|
+
params = {}
|
15
|
+
property_type = nil
|
16
|
+
property_definition.map do |node|
|
17
|
+
next unless node.namespace
|
18
|
+
next unless node.namespace.href == NS::CMIS_CORE
|
19
|
+
|
20
|
+
# FIXME: add support for "choices"
|
21
|
+
case node.node_name
|
22
|
+
when "id"
|
23
|
+
@id = node.text
|
24
|
+
when "localName"
|
25
|
+
@local_name = node.text
|
26
|
+
when "localNamespace"
|
27
|
+
@local_namespace = node.text
|
28
|
+
when "displayName"
|
29
|
+
@display_name = node.text
|
30
|
+
when "queryName"
|
31
|
+
@query_name = node.text
|
32
|
+
when "propertyType"
|
33
|
+
# Will be post processed, but we need to know all the parameters before we can pick an atomic type
|
34
|
+
property_type = node.text
|
35
|
+
when "cardinality"
|
36
|
+
@cardinality = node.text
|
37
|
+
when "updatability"
|
38
|
+
@updatability = node.text
|
39
|
+
when "inherited"
|
40
|
+
@inherited = AtomicType::Boolean.xml_to_bool(node.text)
|
41
|
+
when "required"
|
42
|
+
@required = AtomicType::Boolean.xml_to_bool(node.text)
|
43
|
+
when "queryable"
|
44
|
+
@queryable = AtomicType::Boolean.xml_to_bool(node.text)
|
45
|
+
when "orderable"
|
46
|
+
@orderable = AtomicType::Boolean.xml_to_bool(node.text)
|
47
|
+
when "openChoice"
|
48
|
+
@open_choice = AtomicType::Boolean.xml_to_bool(node.text)
|
49
|
+
when "maxValue", "minValue", "resolution", "precision", "maxLength"
|
50
|
+
params[node.node_name] = node.text
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
if required and updatability == "readonly"
|
55
|
+
logger.warn "The server behaved strange: attribute #{self.inspect} required but readonly, will set required to false"
|
56
|
+
@required = false
|
57
|
+
end
|
58
|
+
if id == "cmis:objectTypeId" and updatability != "oncreate"
|
59
|
+
logger.warn "The server behaved strange: cmis:objectTypeId should be updatable on create but #{updatability}"
|
60
|
+
@updatability = "oncreate"
|
61
|
+
end
|
62
|
+
|
63
|
+
@property_type = case property_type.downcase
|
64
|
+
when "string"
|
65
|
+
max_length = params["maxLength"] ? params["maxLength"].to_i : nil
|
66
|
+
AtomicType::String.new(max_length)
|
67
|
+
when "decimal"
|
68
|
+
min_value = params["minValue"] ? params["minValue"].to_f : nil
|
69
|
+
max_value = params["maxValue"] ? params["maxValue"].to_f : nil
|
70
|
+
AtomicType::Decimal.new(params["precision"].to_i, min_value, max_value)
|
71
|
+
when "integer"
|
72
|
+
min_value = params["minValue"] ? params["minValue"].to_i : nil
|
73
|
+
max_value = params["maxValue"] ? params["maxValue"].to_i : nil
|
74
|
+
AtomicType::Integer.new(min_value, max_value)
|
75
|
+
when "datetime"
|
76
|
+
AtomicType::DateTime.new(params["resolution"] || (logger.warn "No resolution for DateTime #{@id}"; "time") )
|
77
|
+
when "html"
|
78
|
+
AtomicType::HTML.new
|
79
|
+
when "id"
|
80
|
+
AtomicType::ID.new
|
81
|
+
when "boolean"
|
82
|
+
AtomicType::Boolean.new
|
83
|
+
when "uri"
|
84
|
+
AtomicType::URI.new
|
85
|
+
else
|
86
|
+
raise "Unknown property type #{property_type}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Boolean] Returns true if the attribute can have multiple values
|
91
|
+
def repeating
|
92
|
+
cardinality == "multi"
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [String]
|
96
|
+
def inspect
|
97
|
+
"#{object_type.display_name}:#{id} => #{property_type}#{"[]" if repeating}"
|
98
|
+
end
|
99
|
+
alias to_s inspect
|
100
|
+
|
101
|
+
# @return [String]
|
102
|
+
def property_name
|
103
|
+
"property#{property_type}"
|
104
|
+
end
|
105
|
+
|
106
|
+
# @private
|
107
|
+
def render_property(xml, value)
|
108
|
+
xml["c"].send(property_name, "propertyDefinitionId" => id) {
|
109
|
+
if repeating
|
110
|
+
value.each do |v|
|
111
|
+
property_type.rb2cmis(xml, v)
|
112
|
+
end
|
113
|
+
else
|
114
|
+
property_type.rb2cmis(xml, value)
|
115
|
+
end
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
# @private
|
120
|
+
# FIXME: should probably also raise error for out of bounds case
|
121
|
+
def validate_ruby_value(value)
|
122
|
+
if updatability == "readonly" # FIXME: what about oncreate?
|
123
|
+
raise "You are trying to update a readonly attribute (#{self})"
|
124
|
+
elsif required && value.nil?
|
125
|
+
raise "You are trying to unset a required attribute (#{self})"
|
126
|
+
elsif repeating != (Array === value)
|
127
|
+
raise "You are ignoring the cardinality for an attribute (#{self})"
|
128
|
+
else
|
129
|
+
if repeating && z = value.detect {|v| !property_type.can_handle?(v)}
|
130
|
+
raise "Can't assign attribute with type #{z.class} to attribute with type #{property_type}"
|
131
|
+
elsif !repeating && !property_type.can_handle?(value)
|
132
|
+
raise "Can't assign attribute with type #{value.class} to attribute with type #{property_type}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# @private
|
138
|
+
def extract_property(properties)
|
139
|
+
elements = properties.children.select do |n|
|
140
|
+
n.node_name == property_name &&
|
141
|
+
n["propertyDefinitionId"] == id &&
|
142
|
+
n.namespace.href == NS::CMIS_CORE
|
143
|
+
end
|
144
|
+
if elements.empty?
|
145
|
+
if required
|
146
|
+
logger.warn "The server behaved strange: attribute #{self.inspect} required but not present among properties"
|
147
|
+
# raise ActiveCMIS::Error.new("The server behaved strange: attribute #{self.inspect} required but not present among properties")
|
148
|
+
end
|
149
|
+
if repeating
|
150
|
+
[]
|
151
|
+
else
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
elsif elements.length == 1
|
155
|
+
values = elements.first.children.select {|node| node.name == 'value' && node.namespace && node.namespace.href == ActiveCMIS::NS::CMIS_CORE}
|
156
|
+
if required && values.empty?
|
157
|
+
logger.warn "The server behaved strange: attribute #{self.inspect} required but not present among properties"
|
158
|
+
#raise ActiveCMIS::Error.new("The server behaved strange: attribute #{self.inspect} required but no values specified")
|
159
|
+
end
|
160
|
+
if !repeating && values.length > 1
|
161
|
+
logger.warn "The server behaved strange: attribute #{self.inspect} required but not present among properties"
|
162
|
+
#raise ActiveCMIS::Error.new("The server behaved strange: attribute #{self.inspect} not repeating but multiple values given")
|
163
|
+
end
|
164
|
+
values
|
165
|
+
else
|
166
|
+
raise "Property is not unique"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# @return [Logger] The logger of the repository
|
171
|
+
def logger
|
172
|
+
repository.logger
|
173
|
+
end
|
174
|
+
# @return [Repository] The repository that the CMIS type is defined in
|
175
|
+
def repository
|
176
|
+
object_type.repository
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
# QueryResults are returned when doing a query
|
3
|
+
# You can retrieve the values they contain either by the properties ID or by the query name.
|
4
|
+
#
|
5
|
+
# Since it is possible that a JOIN is being requested it is not guaranteed that either the query name or the object ID are unique.
|
6
|
+
# If that's not the case then it is impossible to retrieve one of the two values. It is therefore important to choose unique queryNames
|
7
|
+
# Furthermore, it is not possible to guess which type a query is returning, and therefore it is also not possible to know whether a property is repeating or not, it is possible that a repeating property contains 0 or 1 values, in which case nil or that single value are returned. If multiple values are found for a property then an Array with those properties will be returned
|
8
|
+
class QueryResult
|
9
|
+
def initialize(atom_entry)
|
10
|
+
@atom_entry = atom_entry
|
11
|
+
properties = atom_entry.xpath("cra:object/c:properties/c:*", NS::COMBINED)
|
12
|
+
@properties_by_id = {}
|
13
|
+
@properties_by_query_name = {}
|
14
|
+
properties.each do |property|
|
15
|
+
type = ActiveCMIS::AtomicType::MAPPING[property.node_name]
|
16
|
+
converter = type.new
|
17
|
+
|
18
|
+
values = property.xpath("c:value", NS::COMBINED)
|
19
|
+
# FIXME: If attributes are repeating, but have 0-1 value they won't be in array
|
20
|
+
if values.length > 1
|
21
|
+
value = values.map {|v| converter.cmis2rb(v)}
|
22
|
+
elsif !values.empty?
|
23
|
+
value = converter.cmis2rb(values)
|
24
|
+
else
|
25
|
+
value = nil
|
26
|
+
end
|
27
|
+
@properties_by_id[property["propertyDefinitionId"]] = value
|
28
|
+
@properties_by_query_name[property["queryName"]] = value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def property_by_id(name)
|
33
|
+
@properties_by_id[name]
|
34
|
+
end
|
35
|
+
|
36
|
+
def property_by_query_name(name)
|
37
|
+
@properties_by_query_name[name]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
module Rel
|
3
|
+
def self.[](version)
|
4
|
+
if version == '1.0'
|
5
|
+
prefix = "http://docs.oasis-open.org/ns/cmis/link/200908/"
|
6
|
+
{
|
7
|
+
:allowableactions => "#{prefix}allowableactions",
|
8
|
+
:acl => "#{prefix}acl",
|
9
|
+
:relationships => "#{prefix}relationships",
|
10
|
+
:changes => "#{prefix}changes",
|
11
|
+
}
|
12
|
+
else
|
13
|
+
raise ActiveCMIS::Error.new("ActiveCMIS only works with CMIS 1.0, requested version was #{version}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
class Relationship < ::ActiveCMIS::Object
|
3
|
+
# @return [Object]
|
4
|
+
def source
|
5
|
+
Internal::Utils.string_or_id_to_object(repository, attribute("cmis:sourceId"))
|
6
|
+
end
|
7
|
+
cache :source
|
8
|
+
|
9
|
+
# @return [Object]
|
10
|
+
def target
|
11
|
+
Internal::Utils.string_or_id_to_object(repository, attribute("cmis:targetId"))
|
12
|
+
end
|
13
|
+
cache :target
|
14
|
+
|
15
|
+
# Remove the relationship
|
16
|
+
# @return [void]
|
17
|
+
def delete
|
18
|
+
conn.delete(self_link)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @see Object#update
|
22
|
+
# @param (see ActiveCMIS::Object#update)
|
23
|
+
# @return [void]
|
24
|
+
def update(updates = {})
|
25
|
+
super
|
26
|
+
# Potentially necessary if repositories support it
|
27
|
+
# Probably not though
|
28
|
+
|
29
|
+
# Note: we use remove_instance_variable because of the way I implemented the caching
|
30
|
+
if updates["cmis:sourceId"] && instance_variable_defined?("@source")
|
31
|
+
remove_instance_variable "@source"
|
32
|
+
end
|
33
|
+
if updates["cmis:targetId"] && instance_variable_defined?("@target")
|
34
|
+
remove_instance_variable "@target"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return [], a relationship is not fileable
|
39
|
+
# @return [Array()]
|
40
|
+
def parent_folders
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def create_url
|
46
|
+
source.source_relations.url
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
class Rendition
|
3
|
+
# @return [Repository]
|
4
|
+
attr_reader :repository
|
5
|
+
# @return [Numeric,nil] Size of the rendition, may not be given or misleading
|
6
|
+
attr_reader :size
|
7
|
+
# @return [String,nil]
|
8
|
+
attr_reader :rendition_kind
|
9
|
+
# @return [String,nil] The format is equal to the mime type, but may be unset or misleading
|
10
|
+
attr_reader :format
|
11
|
+
# @return [ActiveCMIS::Document] The document to which the rendition belongs
|
12
|
+
attr_reader :document
|
13
|
+
|
14
|
+
# @private
|
15
|
+
def initialize(repository, document, link)
|
16
|
+
@repository = repository
|
17
|
+
@document = document
|
18
|
+
|
19
|
+
@rel = link['rel'] == "alternate"
|
20
|
+
@rendition_kind = link['renditionKind'] if rendition?
|
21
|
+
@format = link['type']
|
22
|
+
if link['href']
|
23
|
+
@url = URI(link['href'])
|
24
|
+
else # For inline content streams
|
25
|
+
@data = link['data']
|
26
|
+
end
|
27
|
+
@size = link['length'] ? link['length'].to_i : nil
|
28
|
+
|
29
|
+
|
30
|
+
@link = link # FIXME: For debugging purposes only, remove
|
31
|
+
end
|
32
|
+
|
33
|
+
# Used to differentiate between rendition and primary content
|
34
|
+
def rendition?
|
35
|
+
@rel == "alternate"
|
36
|
+
end
|
37
|
+
# Used to differentiate between rendition and primary content
|
38
|
+
def primary?
|
39
|
+
@rel.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a hash with the name of the file to which was written, the length, and the content type
|
43
|
+
#
|
44
|
+
# *WARNING*: this loads the complete file in memory and dumps it at once, this should be fixed
|
45
|
+
# @param [String] filename Location to store the content.
|
46
|
+
# @return [Hash]
|
47
|
+
def get_file(file_name)
|
48
|
+
response = get_data
|
49
|
+
File.open(file_name, "w") {|f| f.syswrite response.delete(:data) }
|
50
|
+
response.merge!(:file_name => file_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns a hash with the data of te rendition, the length and the content type
|
54
|
+
#
|
55
|
+
# *WARNING*: this loads all the data in memory
|
56
|
+
# Possible future enhancement could be to allow a block to which data is passed in chunks?r
|
57
|
+
# Not sure that is possible with Net::HTTP though.
|
58
|
+
# @param [String] filename Location to store the content.
|
59
|
+
# @return [Hash]
|
60
|
+
def get_data
|
61
|
+
if @url
|
62
|
+
response = repository.conn.get_response(@url)
|
63
|
+
status = response.code.to_i
|
64
|
+
if 200 <= status && status < 300
|
65
|
+
data = response.body
|
66
|
+
else
|
67
|
+
raise HTTPError.new("Problem downloading rendition: status: #{status}, message: #{response.body}")
|
68
|
+
end
|
69
|
+
content_type = response.content_type
|
70
|
+
content_length = response.content_length || response.body.length # In case content encoding is chunked? ??
|
71
|
+
else
|
72
|
+
data = @data
|
73
|
+
content_type = @format
|
74
|
+
content_length = @data.length
|
75
|
+
end
|
76
|
+
|
77
|
+
{:data => data, :content_type => content_type, :content_length => content_length}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
module ActiveCMIS
|
2
|
+
class Repository
|
3
|
+
# @return [Logger] A logger to which debug output and so on is sent
|
4
|
+
attr_reader :logger
|
5
|
+
|
6
|
+
# @return [ActiveCMIS::Server] The server from which the repository was
|
7
|
+
# requested
|
8
|
+
attr_reader :server
|
9
|
+
|
10
|
+
# @private
|
11
|
+
def initialize(server, connection, logger, initial_data, authentication_info) #:nodoc:
|
12
|
+
@server = server
|
13
|
+
@conn = connection
|
14
|
+
@data = initial_data
|
15
|
+
@logger = logger
|
16
|
+
method, *params = authentication_info
|
17
|
+
if method
|
18
|
+
conn.authenticate(method, *params)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Use authentication to access the CMIS repository
|
23
|
+
# This returns a new Repository object, the existing repository will still
|
24
|
+
# use the previous authentication info.
|
25
|
+
# If the used authentication info (method, username, password) is the same
|
26
|
+
# as for the current Repository object, then self will be returned (unless
|
27
|
+
# the server repository cache is cleared first)
|
28
|
+
#
|
29
|
+
# e.g.: authenticated = repo.authenticate(:basic, "username", "password")
|
30
|
+
# @param method [:basic, :ntlm]
|
31
|
+
# @param username [String]
|
32
|
+
# @param password [String]
|
33
|
+
# @return [Repository]
|
34
|
+
def authenticate(*authentication_info)
|
35
|
+
server.repository(key, authentication_info)
|
36
|
+
end
|
37
|
+
|
38
|
+
# The identifier of the repository
|
39
|
+
# @return [String]
|
40
|
+
def key
|
41
|
+
@key ||= data.xpath('cra:repositoryInfo/c:repositoryId', NS::COMBINED).text
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String]
|
45
|
+
def inspect
|
46
|
+
"<#ActiveCMIS::Repository #{key}>"
|
47
|
+
end
|
48
|
+
|
49
|
+
# The version of the CMIS standard supported by this repository
|
50
|
+
# @return [String]
|
51
|
+
def cmis_version
|
52
|
+
# NOTE: we might want to "version" our xml namespaces depending on the CMIS version
|
53
|
+
# If we do that we need to make this method capable of not using the predefined namespaces
|
54
|
+
#
|
55
|
+
# On the other hand breaking the XML namespace is probably going to break other applications too so the might not change them even when the spec is updated
|
56
|
+
@cmis_version ||= data.xpath("cra:repositoryInfo/c:cmisVersionSupported", NS::COMBINED).text
|
57
|
+
end
|
58
|
+
|
59
|
+
# The name of the repository, meant for display purposes
|
60
|
+
# @return [String]
|
61
|
+
def name
|
62
|
+
@name ||= data.xpath("cra:repositoryInfo/c:repositoryName", NS::COMBINED).text
|
63
|
+
end
|
64
|
+
|
65
|
+
# A description of the repository
|
66
|
+
# @return [String]
|
67
|
+
def description
|
68
|
+
@name ||= data.xpath("cra:repositoryInfo/c:repositoryDescription", NS::COMBINED).text
|
69
|
+
end
|
70
|
+
|
71
|
+
# The name of the vendor of this Repository
|
72
|
+
# @return [String]
|
73
|
+
def vendor
|
74
|
+
@vendor ||= data.xpath("cra:repositoryInfo/c:vendorName", NS::COMBINED).text
|
75
|
+
end
|
76
|
+
|
77
|
+
# The name of the product behind this Repository
|
78
|
+
# @return [String]
|
79
|
+
def product_name
|
80
|
+
@product_name ||= data.xpath("cra:repositoryInfo/c:productName", NS::COMBINED).text
|
81
|
+
end
|
82
|
+
|
83
|
+
# The version of the product behind this Repository
|
84
|
+
# @return [String]
|
85
|
+
def product_version
|
86
|
+
@product_version ||= data.xpath("cra:repositoryInfo/c:productVersion", NS::COMBINED).text
|
87
|
+
end
|
88
|
+
|
89
|
+
# Changelog token representing the most recent change to this repository (will
|
90
|
+
# represent the most recent change at the time that this ActiveCMIS::Repository
|
91
|
+
# was created
|
92
|
+
# @return [String]
|
93
|
+
def latest_changelog_token
|
94
|
+
@changelog_token ||= data.xpath("cra:repositoryInfo/c:latestChangeLogToken", NS::COMBINED).text
|
95
|
+
end
|
96
|
+
|
97
|
+
# A URI that points to a web service for this repository. May not be present
|
98
|
+
# @return [URI, nil]
|
99
|
+
def thin_client_uri
|
100
|
+
@thin_client_uri ||= begin
|
101
|
+
string = data.xpath("cra:repositoryInfo/c:thinClientURI", NS::COMBINED).text
|
102
|
+
URI.parse(string) if string && string != ""
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Finds the object with a given ID in the repository
|
107
|
+
#
|
108
|
+
# @param [String] id
|
109
|
+
# @param parameters A list of parameters used to get (defaults are what you should use)
|
110
|
+
# @return [Object]
|
111
|
+
def object_by_id(id, parameters = {"renditionFilter" => "*", "includeAllowableActions" => "true", "includeACL" => true})
|
112
|
+
ActiveCMIS::Object.from_parameters(self, parameters.merge("id" => id))
|
113
|
+
end
|
114
|
+
|
115
|
+
# @private
|
116
|
+
def object_by_id_url(parameters)
|
117
|
+
template = pick_template("objectbyid")
|
118
|
+
raise "Repository does not define required URI-template 'objectbyid'" unless template
|
119
|
+
url = fill_in_template(template, parameters)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Finds the type with a given ID in the repository
|
123
|
+
# @return [Class]
|
124
|
+
def type_by_id(id)
|
125
|
+
@type_by_id ||= {}
|
126
|
+
if result = @type_by_id[id]
|
127
|
+
result
|
128
|
+
else
|
129
|
+
template = pick_template("typebyid")
|
130
|
+
raise "Repository does not define required URI-template 'typebyid'" unless template
|
131
|
+
url = fill_in_template(template, "id" => id)
|
132
|
+
|
133
|
+
@type_by_id[id] = Type.create(conn, self, conn.get_atom_entry(url))
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
%w[root checkedout unfiled].each do |coll_name|
|
138
|
+
define_method coll_name do
|
139
|
+
iv = :"@#{coll_name}"
|
140
|
+
if instance_variable_defined?(iv)
|
141
|
+
instance_variable_get(iv)
|
142
|
+
else
|
143
|
+
href = data.xpath("app:collection[cra:collectionType[child::text() = '#{coll_name}']]/@href", NS::COMBINED)
|
144
|
+
if href.first
|
145
|
+
result = Collection.new(self, href.first)
|
146
|
+
else
|
147
|
+
result = nil
|
148
|
+
end
|
149
|
+
instance_variable_set(iv, result)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# A collection containing the CMIS base types supported by this repository
|
155
|
+
# @return [Collection<Class>]
|
156
|
+
def base_types
|
157
|
+
@base_types ||= begin
|
158
|
+
query = "app:collection[cra:collectionType[child::text() = 'types']]/@href"
|
159
|
+
href = data.xpath(query, NS::COMBINED)
|
160
|
+
if href.first
|
161
|
+
url = href.first.text
|
162
|
+
Collection.new(self, url) do |entry|
|
163
|
+
id = entry.xpath("cra:type/c:id", NS::COMBINED).text
|
164
|
+
type_by_id id
|
165
|
+
end
|
166
|
+
else
|
167
|
+
raise "Repository has no types collection, this is strange and wrong"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# An array containing all the types used by this repository
|
173
|
+
# @return [<Class>]
|
174
|
+
def types
|
175
|
+
@types ||= base_types.map do |t|
|
176
|
+
t.all_subtypes
|
177
|
+
end.flatten
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns a collection with the changes since the given changeLogToken.
|
181
|
+
#
|
182
|
+
# Completely uncached so use with care
|
183
|
+
#
|
184
|
+
# @param options Keys can be Symbol or String, all options are optional
|
185
|
+
# @option options [String] filter
|
186
|
+
# @option options [String] changeLogToken A token indicating which changes you already know about
|
187
|
+
# @option options [Integer] maxItems For paging
|
188
|
+
# @option options [Boolean] includeAcl
|
189
|
+
# @option options [Boolean] includePolicyIds
|
190
|
+
# @option options [Boolean] includeProperties
|
191
|
+
# @return [Collection]
|
192
|
+
def changes(options = {})
|
193
|
+
query = "at:link[@rel = '#{Rel[cmis_version][:changes]}']/@href"
|
194
|
+
link = data.xpath(query, NS::COMBINED)
|
195
|
+
if link = link.first
|
196
|
+
link = Internal::Utils.append_parameters(link.to_s, options)
|
197
|
+
Collection.new(self, link)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns a collection with the results of a query (if supported by the repository)
|
202
|
+
#
|
203
|
+
# @param [#to_s] query_string A query in the CMIS SQL format (unescaped in any way)
|
204
|
+
# @param [{Symbol => ::Object}] options Optional configuration for the query
|
205
|
+
# @option options [Boolean] :searchAllVersions (false)
|
206
|
+
# @option options [Boolean] :includeAllowableActions (false)
|
207
|
+
# @option options ["none","source","target","both"] :includeRelationships
|
208
|
+
# @option options [String] :renditionFilter ('cmis:none') Possible values: 'cmis:none', '*' (all), comma-separated list of rendition kinds or mimetypes
|
209
|
+
# @option options [Integer] :maxItems used for paging
|
210
|
+
# @option options [Integer] :skipCount (0) used for paging
|
211
|
+
# @return [Collection] A collection with each return value wrapped in a QueryResult
|
212
|
+
def query(query_string, options = {})
|
213
|
+
raise "This repository does not support queries" if capabilities["Query"] == "none"
|
214
|
+
# For the moment we make no difference between metadataonly,fulltextonly,bothseparate and bothcombined
|
215
|
+
# Nor do we look at capabilities["Join"] (none, inneronly, innerandouter)
|
216
|
+
|
217
|
+
# For searchAllVersions need to check capabilities["AllVersionsSearchable"]
|
218
|
+
# includeRelationships, includeAllowableActions and renditionFilter only work if SELECT only contains attributes from 1 object
|
219
|
+
valid_params = ["searchAllVersions", "includeAllowableActions", "includeRelationships", "renditionFilter", "maxItems", "skipCount"]
|
220
|
+
invalid_params = options.keys - valid_params
|
221
|
+
unless invalid_params.empty?
|
222
|
+
raise "Invalid parameters for query: #{invalid_params.join ', '}"
|
223
|
+
end
|
224
|
+
|
225
|
+
# FIXME: options are not respected yet by pick_template
|
226
|
+
url = pick_template("query", :mimetype => "application/atom+xml", :type => "feed")
|
227
|
+
url = fill_in_template(url, options.merge("q" => query_string))
|
228
|
+
Collection.new(self, url) do |entry|
|
229
|
+
QueryResult.new(entry)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# The root folder of the repository (as defined in the CMIS standard)
|
234
|
+
# @return [Folder]
|
235
|
+
def root_folder(reload = false)
|
236
|
+
if reload
|
237
|
+
@root_folder = object_by_id(data.xpath("cra:repositoryInfo/c:rootFolderId", NS::COMBINED).text)
|
238
|
+
else
|
239
|
+
@root_folder ||= object_by_id(data.xpath("cra:repositoryInfo/c:rootFolderId", NS::COMBINED).text)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns an Internal::Connection object, normally you should not use this directly
|
244
|
+
# @return [Internal::Connection]
|
245
|
+
def conn
|
246
|
+
@conn ||= Internal::Connection.new
|
247
|
+
end
|
248
|
+
|
249
|
+
# Describes the capabilities of the repository
|
250
|
+
# @return [Hash{String => String,Boolean}] The hash keys have capability cut of their name
|
251
|
+
def capabilities
|
252
|
+
@capabilities ||= begin
|
253
|
+
capa = {}
|
254
|
+
data.xpath("cra:repositoryInfo/c:capabilities/*", NS::COMBINED).map do |node|
|
255
|
+
# FIXME: conversion should be based on knowledge about data model + transforming bool code should not be duplicated
|
256
|
+
capa[node.name.sub("capability", "")] = case t = node.text
|
257
|
+
when "true", "1"; true
|
258
|
+
when "false", "0"; false
|
259
|
+
else t
|
260
|
+
end
|
261
|
+
end
|
262
|
+
capa
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Responds with true if Private Working Copies are updateable, false otherwise
|
267
|
+
# (if false the PWC object can only be updated during the checkin)
|
268
|
+
def pwc_updatable?
|
269
|
+
capabilities["PWCUpdatable"]
|
270
|
+
end
|
271
|
+
|
272
|
+
# Responds with true if different versions of the same document can
|
273
|
+
# be filed in different folders
|
274
|
+
def version_specific_filing?
|
275
|
+
capabilities["VersionSpecificFiling"]
|
276
|
+
end
|
277
|
+
|
278
|
+
# returns true if ACLs can at least be viewed
|
279
|
+
def acls_readable?
|
280
|
+
["manage", "discover"].include? capabilities["ACL"]
|
281
|
+
end
|
282
|
+
|
283
|
+
# You should probably not use this directly, use :anonymous instead where a user name is required
|
284
|
+
# @return [String]
|
285
|
+
def anonymous_user
|
286
|
+
if acls_readable?
|
287
|
+
data.xpath('cra:repositoryInfo/c:principalAnonymous', NS::COMBINED).text
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# You should probably not use this directly, use :world instead where a user name is required
|
292
|
+
# @return [String]
|
293
|
+
def world_user
|
294
|
+
if acls_readable?
|
295
|
+
data.xpath('cra:repositoryInfo/c:principalAnyone', NS::COMBINED).text
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
private
|
300
|
+
# @private
|
301
|
+
attr_reader :data
|
302
|
+
|
303
|
+
def pick_template(name, options = {})
|
304
|
+
# FIXME: we can have more than 1 template with differing media types
|
305
|
+
# I'm not sure how to pick the right one in the most generic/portable way though
|
306
|
+
# So for the moment we pick the 1st and hope for the best
|
307
|
+
# Options are ignored for the moment
|
308
|
+
data.xpath("n:uritemplate[n:type[child::text() = '#{name}']][1]/n:template", "n" => NS::CMIS_REST).text
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
# The type parameter should contain the type of the uri-template
|
313
|
+
#
|
314
|
+
# The keys of the values hash should be strings,
|
315
|
+
# if a key is not in the hash it is presumed to be equal to the empty string
|
316
|
+
# The values will be percent-encoded in the fill_in_template method
|
317
|
+
# If a given key is not present in the template it will be ignored silently
|
318
|
+
#
|
319
|
+
# e.g. fill_in_template("objectbyid", "id" => "@root@", "includeACL" => true)
|
320
|
+
# -> 'http://example.org/repo/%40root%40?includeRelationships&includeACL=true'
|
321
|
+
def fill_in_template(template, values)
|
322
|
+
result = template.gsub(/\{([^}]+)\}/) do |match|
|
323
|
+
Internal::Utils.percent_encode(values[$1].to_s)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|