cmis_server 1.2.1 → 1.3.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.
- checksums.yaml +4 -4
- data/app/controllers/cmis_server/atom_pub/entries_controller.rb +2 -1
- data/app/controllers/cmis_server/atom_pub/folder_collection_controller.rb +55 -11
- data/app/controllers/cmis_server/atom_pub/query_controller.rb +44 -16
- data/app/controllers/cmis_server/atom_pub/service_documents_controller.rb +1 -1
- data/app/services/cmis_server/discovery_service.rb +58 -9
- data/app/services/cmis_server/navigation_service.rb +62 -3
- data/app/services/cmis_server/object_service.rb +112 -13
- data/app/views/cmis_server/atom_pub/entries/_cmis_folder_links.atom_entry.builder +1 -1
- data/app/views/cmis_server/atom_pub/entries/_object_entry.atom_entry.builder +4 -3
- data/app/views/cmis_server/atom_pub/entries/type_entry.atom_entry.builder +1 -1
- data/app/views/cmis_server/atom_pub/feeds/feed.atom_feed.builder +3 -0
- data/app/views/cmis_server/atom_pub/query_results_feed.atom_feed.builder +59 -0
- data/app/views/cmis_server/atom_pub/queryable_types_feed.atom_feed.builder +35 -0
- data/app/views/cmis_server/atom_pub/service_documents/_workspace.atom_service.builder +1 -1
- data/config/initializers/cmis_core_configuration.rb +31 -8
- data/lib/cmis_server/atom_pub/entry_parser.rb +57 -22
- data/lib/cmis_server/cmis_object.rb +32 -2
- data/lib/cmis_server/configuration.rb +4 -3
- data/lib/cmis_server/connectors/CORE_CONNECTOR_QUERIES.md +180 -0
- data/lib/cmis_server/connectors/core_connector.rb +189 -69
- data/lib/cmis_server/constants.rb +3 -2
- data/lib/cmis_server/document_adapter.rb +135 -0
- data/lib/cmis_server/document_object.rb +1 -1
- data/lib/cmis_server/engine.rb +95 -0
- data/lib/cmis_server/exceptions.rb +19 -0
- data/lib/cmis_server/folder_adapter.rb +126 -0
- data/lib/cmis_server/property.rb +40 -4
- data/lib/cmis_server/query/parser.rb +11 -0
- data/lib/cmis_server/query/{parser.racc.rb → parser_racc.rb} +2 -2
- data/lib/cmis_server/query/{parser.rex.rb → parser_rex.rb} +6 -1
- data/lib/cmis_server/query/simple_parser.rb +276 -0
- data/lib/cmis_server/query/statement.rb +3 -389
- data/lib/cmis_server/query/statement.rb.bak +395 -0
- data/lib/cmis_server/query.rb +11 -2
- data/lib/cmis_server/renderable_collection.rb +23 -2
- data/lib/cmis_server/repository.rb +1 -1
- data/lib/cmis_server/version.rb +1 -1
- data/lib/cmis_server.rb +13 -0
- metadata +12 -4
@@ -0,0 +1,126 @@
|
|
1
|
+
module CmisServer
|
2
|
+
class FolderAdapter
|
3
|
+
attr_reader :object, :context
|
4
|
+
|
5
|
+
def initialize(object, context:)
|
6
|
+
@object = object
|
7
|
+
@context = context
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.class_adapter(context:)
|
11
|
+
new(nil, context: context)
|
12
|
+
end
|
13
|
+
|
14
|
+
def find(id)
|
15
|
+
if id == 'root_folder' || id == 'root_folder_id' || id == 'core_root' || id == 'root'
|
16
|
+
# Utiliser le connector pour récupérer l'objet root virtuel
|
17
|
+
connector = CmisServer::Connectors::ConnectorFactory.create_connector(
|
18
|
+
user: @context.is_a?(Hash) ? @context[:current_user] : @context.current_user
|
19
|
+
)
|
20
|
+
root_object = connector.find_object_by_id(id)
|
21
|
+
|
22
|
+
if root_object && root_object.respond_to?(:is_virtual) && root_object.is_virtual
|
23
|
+
# Convertir l'objet virtuel en FolderObject CMIS
|
24
|
+
folder_obj = virtual_folder_to_folder_object(root_object)
|
25
|
+
else
|
26
|
+
# Fallback : utiliser le root folder par défaut
|
27
|
+
folder_obj = CmisServer::FolderObject.root_folder
|
28
|
+
end
|
29
|
+
self.class.new(folder_obj, context: @context)
|
30
|
+
else
|
31
|
+
# Chercher un Tagset dans Core
|
32
|
+
tagset = ::Tagset.find(id)
|
33
|
+
# Convertir le Tagset en FolderObject CMIS
|
34
|
+
folder_obj = tagset_to_folder_object(tagset)
|
35
|
+
self.class.new(folder_obj, context: @context)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def save!
|
40
|
+
# Créer un espace Core (Tagset)
|
41
|
+
tagset = ::Tagset.new
|
42
|
+
|
43
|
+
# Mapper les propriétés CMIS vers Core
|
44
|
+
if @object && @object.properties
|
45
|
+
# Extraire la valeur si c'est un objet Property
|
46
|
+
name_value = @object.properties['cmis:name']
|
47
|
+
name_value = name_value.value if name_value.respond_to?(:value)
|
48
|
+
tagset.title = name_value || 'Untitled Folder'
|
49
|
+
end
|
50
|
+
|
51
|
+
# Définir le responsable (obligatoire)
|
52
|
+
current_user = @context.is_a?(Hash) ? @context[:current_user] : @context.current_user
|
53
|
+
if current_user
|
54
|
+
tagset.responsible = current_user.id.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
# Gérer la hiérarchie si un parent est spécifié
|
58
|
+
if @object && @object.properties && @object.properties['cmis:parentId']
|
59
|
+
parent_value = @object.properties['cmis:parentId']
|
60
|
+
parent_value = parent_value.value if parent_value.respond_to?(:value)
|
61
|
+
if parent_value && parent_value != 'root_folder'
|
62
|
+
tagset.parent_ids = [parent_value.to_s]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Sauvegarder l'espace
|
67
|
+
if tagset.save
|
68
|
+
# Mettre à jour l'objet CMIS avec l'ID du tagset créé
|
69
|
+
@object.cmis_object_id = tagset.id.to_s if @object
|
70
|
+
true
|
71
|
+
else
|
72
|
+
raise "Failed to save folder: #{tagset.errors.full_messages.join(', ')}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def cmis_object_id
|
77
|
+
@object.respond_to?(:cmis_object_id) ? @object.cmis_object_id : @object.id
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_renderable_object
|
81
|
+
CmisServer::RenderableObject.new(base_object: @object)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def tagset_to_folder_object(tagset)
|
87
|
+
# Créer un FolderObject à partir d'un Tagset Core
|
88
|
+
folder_type = CmisServer::TypeRegistry.get_type('cmis:folder')
|
89
|
+
properties = {
|
90
|
+
'cmis:objectId' => CmisServer::Property.new(id: 'cmis:objectId', value: tagset.id.to_s),
|
91
|
+
'cmis:objectTypeId' => CmisServer::Property.new(id: 'cmis:objectTypeId', value: 'cmis:folder'),
|
92
|
+
'cmis:name' => CmisServer::Property.new(id: 'cmis:name', value: tagset.title),
|
93
|
+
'cmis:createdBy' => CmisServer::Property.new(id: 'cmis:createdBy', value: tagset.responsible),
|
94
|
+
'cmis:parentId' => CmisServer::Property.new(id: 'cmis:parentId', value: tagset.parent_ids&.first || 'root_folder')
|
95
|
+
}
|
96
|
+
|
97
|
+
folder = CmisServer::FolderObject.new(type: folder_type, properties: properties)
|
98
|
+
folder.cmis_object_id = tagset.id.to_s
|
99
|
+
folder.cmis_name = tagset.title
|
100
|
+
folder
|
101
|
+
end
|
102
|
+
|
103
|
+
def virtual_folder_to_folder_object(virtual_obj)
|
104
|
+
# Créer un FolderObject à partir d'un objet virtuel (comme le root)
|
105
|
+
folder_type = CmisServer::TypeRegistry.get_type('cmis:folder') || CmisServer::FolderType.base
|
106
|
+
properties = {
|
107
|
+
'cmis:objectId' => CmisServer::Property.new(id: 'cmis:objectId', value: virtual_obj.id.to_s),
|
108
|
+
'cmis:objectTypeId' => CmisServer::Property.new(id: 'cmis:objectTypeId', value: 'cmis:folder'),
|
109
|
+
'cmis:name' => CmisServer::Property.new(id: 'cmis:name', value: virtual_obj.title),
|
110
|
+
'cmis:description' => CmisServer::Property.new(id: 'cmis:description', value: virtual_obj.description),
|
111
|
+
'cmis:createdBy' => CmisServer::Property.new(id: 'cmis:createdBy', value: virtual_obj.responsible),
|
112
|
+
'cmis:creationDate' => CmisServer::Property.new(id: 'cmis:creationDate', value: virtual_obj.created_at),
|
113
|
+
'cmis:lastModificationDate' => CmisServer::Property.new(id: 'cmis:lastModificationDate', value: virtual_obj.updated_at)
|
114
|
+
}
|
115
|
+
|
116
|
+
folder = CmisServer::FolderObject.new(type: folder_type, properties: properties)
|
117
|
+
folder.cmis_object_id = virtual_obj.id.to_s
|
118
|
+
folder.cmis_name = virtual_obj.title
|
119
|
+
folder.cmis_description = virtual_obj.description
|
120
|
+
folder.cmis_created_by = virtual_obj.responsible
|
121
|
+
folder.cmis_creation_date = virtual_obj.created_at
|
122
|
+
folder.cmis_last_modification_date = virtual_obj.updated_at
|
123
|
+
folder
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/cmis_server/property.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
|
1
4
|
module CmisServer
|
2
5
|
class Property
|
3
6
|
|
@@ -12,7 +15,11 @@ module CmisServer
|
|
12
15
|
end
|
13
16
|
|
14
17
|
def value
|
15
|
-
self.property_definition.value
|
18
|
+
if self.property_definition.respond_to?(:value) && self.property_definition.value
|
19
|
+
self.property_definition.value_for(@object)
|
20
|
+
else
|
21
|
+
@value
|
22
|
+
end
|
16
23
|
end
|
17
24
|
|
18
25
|
def set_default
|
@@ -20,9 +27,38 @@ module CmisServer
|
|
20
27
|
end
|
21
28
|
|
22
29
|
def initialize(property_definition, value=nil, object=nil)
|
23
|
-
|
24
|
-
|
25
|
-
|
30
|
+
# Support des deux formes d'arguments
|
31
|
+
if property_definition.is_a?(Hash) && property_definition[:id]
|
32
|
+
# Forme avec arguments nommés : Property.new(id: 'cmis:name', value: 'foo')
|
33
|
+
# Déterminer le type en fonction de la valeur
|
34
|
+
type_class = case property_definition[:value]
|
35
|
+
when String then String
|
36
|
+
when Integer then Integer
|
37
|
+
when Float then Float
|
38
|
+
when Time, DateTime then DateTime
|
39
|
+
when TrueClass, FalseClass then OpenStruct.new(name: 'Boolean')
|
40
|
+
else String
|
41
|
+
end
|
42
|
+
|
43
|
+
# Créer un type object si ce n'est pas déjà un OpenStruct
|
44
|
+
type = type_class.is_a?(OpenStruct) ? type_class : OpenStruct.new(name: type_class.name)
|
45
|
+
|
46
|
+
@property_definition = OpenStruct.new(
|
47
|
+
id: property_definition[:id],
|
48
|
+
type: type,
|
49
|
+
query_name: property_definition[:id],
|
50
|
+
display_name: property_definition[:id].split(':').last.humanize,
|
51
|
+
local_name: property_definition[:id].split(':').last,
|
52
|
+
value: nil # Les PropertyDefinition n'ont pas de value fixe
|
53
|
+
)
|
54
|
+
@value = property_definition[:value]
|
55
|
+
@object = property_definition[:object]
|
56
|
+
else
|
57
|
+
# Forme classique : Property.new(property_def, value, object)
|
58
|
+
@object =object
|
59
|
+
@property_definition=property_definition
|
60
|
+
@value =value
|
61
|
+
end
|
26
62
|
end
|
27
63
|
|
28
64
|
end
|
@@ -6,11 +6,11 @@
|
|
6
6
|
|
7
7
|
require 'racc/parser.rb'
|
8
8
|
|
9
|
-
require File.dirname(__FILE__) + '/
|
9
|
+
require File.dirname(__FILE__) + '/parser_rex'
|
10
10
|
|
11
11
|
module CmisServer
|
12
12
|
module Query
|
13
|
-
class
|
13
|
+
class ParserRacc < Racc::Parser
|
14
14
|
|
15
15
|
module_eval(<<'...end parser.racc/module_eval...', 'parser.racc', 219)
|
16
16
|
|
@@ -5,7 +5,10 @@
|
|
5
5
|
#++
|
6
6
|
|
7
7
|
require 'racc/parser'
|
8
|
-
|
8
|
+
|
9
|
+
module CmisServer
|
10
|
+
module Query
|
11
|
+
class ParserRex < Racc::Parser
|
9
12
|
require 'strscan'
|
10
13
|
|
11
14
|
class ScanError < StandardError ; end
|
@@ -236,3 +239,5 @@ class CmisServer::Query::Parser < Racc::Parser
|
|
236
239
|
end # def _next_token
|
237
240
|
|
238
241
|
end # class
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
module CmisServer
|
2
|
+
module Query
|
3
|
+
# Simple CMIS SQL parser for basic queries
|
4
|
+
class SimpleParser
|
5
|
+
class ParseError < StandardError; end
|
6
|
+
|
7
|
+
# Parse a CMIS SQL statement
|
8
|
+
def self.parse(sql)
|
9
|
+
result = new(sql).parse
|
10
|
+
result
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(sql)
|
14
|
+
@sql = sql.strip
|
15
|
+
@tokens = tokenize(@sql)
|
16
|
+
@position = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse
|
20
|
+
statement = ParsedStatement.new
|
21
|
+
|
22
|
+
# Parse SELECT clause
|
23
|
+
expect_keyword('SELECT')
|
24
|
+
statement.select_list = parse_select_list
|
25
|
+
|
26
|
+
# Parse FROM clause
|
27
|
+
expect_keyword('FROM')
|
28
|
+
statement.from_clause = parse_from_clause
|
29
|
+
|
30
|
+
# Parse optional WHERE clause
|
31
|
+
if current_token_is?('WHERE')
|
32
|
+
consume_token
|
33
|
+
statement.where_clause = parse_where_clause
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parse optional ORDER BY clause
|
37
|
+
if current_token_is?('ORDER')
|
38
|
+
consume_token
|
39
|
+
expect_keyword('BY')
|
40
|
+
statement.order_by = parse_order_by
|
41
|
+
end
|
42
|
+
|
43
|
+
statement
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def tokenize(sql)
|
49
|
+
# Simple tokenizer - splits on whitespace and special characters
|
50
|
+
# Include : in word characters to handle cmis:document style identifiers
|
51
|
+
sql.scan(/[\w:]+\.[\w:]+|[\w:]+|'[^']*'|"[^"]*"|>=|<=|<>|!=|[<>=(),*]/)
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_token
|
55
|
+
@tokens[@position]
|
56
|
+
end
|
57
|
+
|
58
|
+
def consume_token
|
59
|
+
token = @tokens[@position]
|
60
|
+
@position += 1
|
61
|
+
token
|
62
|
+
end
|
63
|
+
|
64
|
+
def current_token_is?(expected)
|
65
|
+
current_token&.upcase == expected.upcase
|
66
|
+
end
|
67
|
+
|
68
|
+
def expect_keyword(keyword)
|
69
|
+
unless current_token_is?(keyword)
|
70
|
+
raise ParseError, "Expected #{keyword} but got #{current_token}"
|
71
|
+
end
|
72
|
+
consume_token
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_select_list
|
76
|
+
select_list = []
|
77
|
+
|
78
|
+
if current_token == '*'
|
79
|
+
consume_token
|
80
|
+
select_list << SelectItem.new('*')
|
81
|
+
else
|
82
|
+
loop do
|
83
|
+
# Parse property name (may include alias like d.cmis:name)
|
84
|
+
property = consume_token
|
85
|
+
if current_token == '.'
|
86
|
+
consume_token
|
87
|
+
property += ".#{consume_token}"
|
88
|
+
end
|
89
|
+
|
90
|
+
select_list << SelectItem.new(property)
|
91
|
+
|
92
|
+
break unless current_token == ','
|
93
|
+
consume_token # consume comma
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
select_list
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_from_clause
|
101
|
+
# Parse table name
|
102
|
+
table_name = consume_token
|
103
|
+
from_clause = FromClause.new(table_name)
|
104
|
+
|
105
|
+
# Parse optional alias
|
106
|
+
if current_token && !%w[WHERE ORDER].include?(current_token.upcase)
|
107
|
+
from_clause.alias = consume_token
|
108
|
+
end
|
109
|
+
|
110
|
+
from_clause
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_where_clause
|
114
|
+
# Simple WHERE clause parser
|
115
|
+
# For now, just parse simple conditions like "cmis:name = 'test'"
|
116
|
+
conditions = []
|
117
|
+
|
118
|
+
loop do
|
119
|
+
left = parse_expression
|
120
|
+
operator = consume_token
|
121
|
+
right = parse_expression
|
122
|
+
|
123
|
+
conditions << WhereCondition.new(left, operator, right)
|
124
|
+
|
125
|
+
# Check for AND/OR
|
126
|
+
if current_token_is?('AND') || current_token_is?('OR')
|
127
|
+
connector = consume_token
|
128
|
+
conditions << connector
|
129
|
+
else
|
130
|
+
break
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
WhereClause.new(conditions)
|
135
|
+
end
|
136
|
+
|
137
|
+
def parse_expression
|
138
|
+
expr = consume_token
|
139
|
+
|
140
|
+
# Handle property with alias (e.g., d.cmis:name)
|
141
|
+
if current_token == '.'
|
142
|
+
consume_token
|
143
|
+
expr += ".#{consume_token}"
|
144
|
+
end
|
145
|
+
|
146
|
+
# Remove quotes from string literals
|
147
|
+
if expr =~ /^['"](.*)['"]$/
|
148
|
+
expr = $1
|
149
|
+
end
|
150
|
+
|
151
|
+
expr
|
152
|
+
end
|
153
|
+
|
154
|
+
def parse_order_by
|
155
|
+
order_items = []
|
156
|
+
|
157
|
+
loop do
|
158
|
+
property = consume_token
|
159
|
+
if current_token == '.'
|
160
|
+
consume_token
|
161
|
+
property += ".#{consume_token}"
|
162
|
+
end
|
163
|
+
|
164
|
+
direction = 'ASC'
|
165
|
+
if current_token_is?('ASC') || current_token_is?('DESC')
|
166
|
+
direction = consume_token.upcase
|
167
|
+
end
|
168
|
+
|
169
|
+
order_items << OrderByItem.new(property, direction)
|
170
|
+
|
171
|
+
break unless current_token == ','
|
172
|
+
consume_token
|
173
|
+
end
|
174
|
+
|
175
|
+
order_items
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Data structures for parsed statement
|
180
|
+
class ParsedStatement
|
181
|
+
attr_accessor :select_list, :from_clause, :where_clause, :order_by
|
182
|
+
|
183
|
+
def initialize
|
184
|
+
@select_list = []
|
185
|
+
@from_clause = nil
|
186
|
+
@where_clause = nil
|
187
|
+
@order_by = []
|
188
|
+
end
|
189
|
+
|
190
|
+
# Convert to format expected by existing code
|
191
|
+
def query_expression
|
192
|
+
OpenStruct.new(
|
193
|
+
from: OpenStruct.new(
|
194
|
+
tables: [@from_clause].map { |fc|
|
195
|
+
OpenStruct.new(
|
196
|
+
name: fc.table_name,
|
197
|
+
is_a?: ->(klass) { klass == CmisServer::Query::Statement::Table }
|
198
|
+
)
|
199
|
+
}
|
200
|
+
),
|
201
|
+
where: @where_clause
|
202
|
+
)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class SelectItem
|
207
|
+
attr_accessor :property
|
208
|
+
|
209
|
+
def initialize(property)
|
210
|
+
@property = property
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class FromClause
|
215
|
+
attr_accessor :table_name, :alias
|
216
|
+
|
217
|
+
def initialize(table_name)
|
218
|
+
@table_name = table_name
|
219
|
+
@alias = nil
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
class WhereClause
|
224
|
+
attr_accessor :conditions
|
225
|
+
|
226
|
+
def initialize(conditions)
|
227
|
+
@conditions = conditions
|
228
|
+
end
|
229
|
+
|
230
|
+
# Convert to hash format for the connector
|
231
|
+
def to_h
|
232
|
+
# For now, convert simple conditions to a hash
|
233
|
+
result = {}
|
234
|
+
|
235
|
+
@conditions.each do |cond|
|
236
|
+
next unless cond.is_a?(WhereCondition)
|
237
|
+
|
238
|
+
# Map CMIS properties to search parameters
|
239
|
+
property = cond.left.sub(/^\w+\./, '') # Remove alias if present
|
240
|
+
|
241
|
+
case property
|
242
|
+
when 'cmis:name'
|
243
|
+
result['name'] = cond.right
|
244
|
+
when 'cmis:objectTypeId'
|
245
|
+
result['type_id'] = cond.right
|
246
|
+
when 'cmis:parentId'
|
247
|
+
result['parent_id'] = cond.right
|
248
|
+
else
|
249
|
+
result[property] = cond.right
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
result
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class WhereCondition
|
258
|
+
attr_accessor :left, :operator, :right
|
259
|
+
|
260
|
+
def initialize(left, operator, right)
|
261
|
+
@left = left
|
262
|
+
@operator = operator
|
263
|
+
@right = right
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
class OrderByItem
|
268
|
+
attr_accessor :property, :direction
|
269
|
+
|
270
|
+
def initialize(property, direction)
|
271
|
+
@property = property
|
272
|
+
@direction = direction
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|