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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/cmis_server/atom_pub/entries_controller.rb +2 -1
  3. data/app/controllers/cmis_server/atom_pub/folder_collection_controller.rb +55 -11
  4. data/app/controllers/cmis_server/atom_pub/query_controller.rb +44 -16
  5. data/app/controllers/cmis_server/atom_pub/service_documents_controller.rb +1 -1
  6. data/app/services/cmis_server/discovery_service.rb +58 -9
  7. data/app/services/cmis_server/navigation_service.rb +62 -3
  8. data/app/services/cmis_server/object_service.rb +112 -13
  9. data/app/views/cmis_server/atom_pub/entries/_cmis_folder_links.atom_entry.builder +1 -1
  10. data/app/views/cmis_server/atom_pub/entries/_object_entry.atom_entry.builder +4 -3
  11. data/app/views/cmis_server/atom_pub/entries/type_entry.atom_entry.builder +1 -1
  12. data/app/views/cmis_server/atom_pub/feeds/feed.atom_feed.builder +3 -0
  13. data/app/views/cmis_server/atom_pub/query_results_feed.atom_feed.builder +59 -0
  14. data/app/views/cmis_server/atom_pub/queryable_types_feed.atom_feed.builder +35 -0
  15. data/app/views/cmis_server/atom_pub/service_documents/_workspace.atom_service.builder +1 -1
  16. data/config/initializers/cmis_core_configuration.rb +31 -8
  17. data/lib/cmis_server/atom_pub/entry_parser.rb +57 -22
  18. data/lib/cmis_server/cmis_object.rb +32 -2
  19. data/lib/cmis_server/configuration.rb +4 -3
  20. data/lib/cmis_server/connectors/CORE_CONNECTOR_QUERIES.md +180 -0
  21. data/lib/cmis_server/connectors/core_connector.rb +189 -69
  22. data/lib/cmis_server/constants.rb +3 -2
  23. data/lib/cmis_server/document_adapter.rb +135 -0
  24. data/lib/cmis_server/document_object.rb +1 -1
  25. data/lib/cmis_server/engine.rb +95 -0
  26. data/lib/cmis_server/exceptions.rb +19 -0
  27. data/lib/cmis_server/folder_adapter.rb +126 -0
  28. data/lib/cmis_server/property.rb +40 -4
  29. data/lib/cmis_server/query/parser.rb +11 -0
  30. data/lib/cmis_server/query/{parser.racc.rb → parser_racc.rb} +2 -2
  31. data/lib/cmis_server/query/{parser.rex.rb → parser_rex.rb} +6 -1
  32. data/lib/cmis_server/query/simple_parser.rb +276 -0
  33. data/lib/cmis_server/query/statement.rb +3 -389
  34. data/lib/cmis_server/query/statement.rb.bak +395 -0
  35. data/lib/cmis_server/query.rb +11 -2
  36. data/lib/cmis_server/renderable_collection.rb +23 -2
  37. data/lib/cmis_server/repository.rb +1 -1
  38. data/lib/cmis_server/version.rb +1 -1
  39. data/lib/cmis_server.rb +13 -0
  40. 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
@@ -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 ? self.property_definition.value_for(@object) : @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
- @object =object
24
- @property_definition=property_definition
25
- @value =value
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
@@ -0,0 +1,11 @@
1
+ module CmisServer
2
+ module Query
3
+ class Parser
4
+ def self.parse(sql)
5
+ # Utiliser le SimpleParser au lieu du ParserRacc qui ne fonctionne pas
6
+ result = SimpleParser.parse(sql)
7
+ result
8
+ end
9
+ end
10
+ end
11
+ end
@@ -6,11 +6,11 @@
6
6
 
7
7
  require 'racc/parser.rb'
8
8
 
9
- require File.dirname(__FILE__) + '/parser.rex.rb'
9
+ require File.dirname(__FILE__) + '/parser_rex'
10
10
 
11
11
  module CmisServer
12
12
  module Query
13
- class Parser < Racc::Parser
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
- class CmisServer::Query::Parser < Racc::Parser
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