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.
- checksums.yaml +7 -0
- data/AUTHORS +9 -0
- data/LICENSE +26 -0
- data/README.md +55 -0
- data/Rakefile +34 -0
- data/TODO +7 -0
- data/VERSION.yml +5 -0
- data/active_cmis.gemspec +79 -0
- data/lib/active_cmis.rb +30 -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 +356 -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 +241 -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 +86 -0
- data/lib/active_cmis/repository.rb +327 -0
- data/lib/active_cmis/server.rb +121 -0
- data/lib/active_cmis/type.rb +200 -0
- data/lib/active_cmis/version.rb +10 -0
- metadata +132 -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,86 @@
|
|
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] file_name 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
|
+
elsif 300 <= status && status < 400
|
67
|
+
location = response["location"]
|
68
|
+
new_url = URI.parse(location)
|
69
|
+
new_url = @url + location if new_url.relative?
|
70
|
+
@url = new_url
|
71
|
+
get_data
|
72
|
+
else
|
73
|
+
raise HTTPError.new("Problem downloading rendition: status: #{status}, message: #{response.body}")
|
74
|
+
end
|
75
|
+
content_type = response.content_type
|
76
|
+
content_length = response.content_length || response.body.length # In case content encoding is chunked? ??
|
77
|
+
else
|
78
|
+
data = @data
|
79
|
+
content_type = @format
|
80
|
+
content_length = @data.length
|
81
|
+
end
|
82
|
+
|
83
|
+
{:data => data, :content_type => content_type, :content_length => content_length}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
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
|