[REF] expression: cosmetic changes.
[odoo/odoo.git] / openerp / osv / expression.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 ##############################################################################
4 #
5 #    OpenERP, Open Source Management Solution
6 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 from openerp.tools import flatten, reverse_enumerate
24 import fields
25
26 #.apidoc title: Domain Expressions
27
28 NOT_OPERATOR = '!'
29 OR_OPERATOR = '|'
30 AND_OPERATOR = '&'
31
32 TRUE_LEAF = (1, '=', 1)
33 FALSE_LEAF = (0, '=', 1)
34
35 TRUE_DOMAIN = [TRUE_LEAF]
36 FALSE_DOMAIN = [FALSE_LEAF]
37
38 def normalize(domain):
39     """Returns a normalized version of ``domain_expr``, where all implicit '&' operators
40        have been made explicit. One property of normalized domain expressions is that they
41        can be easily combined together as if they were single domain components.
42     """
43     assert isinstance(domain, (list, tuple)), "Domains to normalize must have a 'domain' form: a list or tuple of domain components"
44     if not domain:
45         return TRUE_DOMAIN
46     result = []
47     expected = 1                            # expected number of expressions
48     op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
49     for token in domain:
50         if expected == 0:                   # more than expected, like in [A, B]
51             result[0:0] = ['&']             # put an extra '&' in front
52             expected = 1
53         result.append(token)
54         if isinstance(token, (list, tuple)): # domain term
55             expected -= 1
56         else:
57             expected += op_arity.get(token, 0) - 1
58     assert expected == 0
59     return result
60
61 def combine(operator, unit, zero, domains):
62     """Returns a new domain expression where all domain components from ``domains``
63        have been added together using the binary operator ``operator``. The given
64        domains must be normalized.
65
66        :param unit: the identity element of the domains "set" with regard to the operation
67                     performed by ``operator``, i.e the domain component ``i`` which, when
68                     combined with any domain ``x`` via ``operator``, yields ``x``. 
69                     E.g. [(1,'=',1)] is the typical unit for AND_OPERATOR: adding it
70                     to any domain component gives the same domain.
71        :param zero: the absorbing element of the domains "set" with regard to the operation
72                     performed by ``operator``, i.e the domain component ``z`` which, when
73                     combined with any domain ``x`` via ``operator``, yields ``z``. 
74                     E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
75                     you see it in a domain component the resulting domain is the zero.
76        :param domains: a list of normalized domains.
77     """
78     result = []
79     count = 0
80     for domain in domains:
81         if domain == unit:
82             continue
83         if domain == zero:
84             return zero
85         if domain:
86             result += domain
87             count += 1
88     result = [operator] * (count - 1) + result
89     return result
90
91 def AND(domains):
92     """ AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
93     return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
94
95 def OR(domains):
96     """ OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
97     return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
98
99 def is_operator(element):
100     return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR]
101
102 # TODO change the share wizard to use this function.
103 def is_leaf(element, internal=False):
104     OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
105     INTERNAL_OPS = OPS + ('inselect',)
106     return (isinstance(element, tuple) or isinstance(element, list)) \
107        and len(element) == 3 \
108        and (((not internal) and element[1] in OPS) \
109             or (internal and element[1] in INTERNAL_OPS))
110
111 def select_from_where(cr, s, f, w, ids, op):
112     # todo: merge into parent query as sub-query
113     res = []
114     if ids:
115         if op in ['<','>','>=','<=']:
116             cr.execute('SELECT "%s" FROM "%s" WHERE "%s" %s %%s' % \
117                 (s, f, w, op), (ids[0],)) # TODO shouldn't this be min/max(ids) ?
118             res = [r[0] for r in cr.fetchall()]
119         else: # TODO op is supposed to be 'in'? It is called with child_of...
120             for i in range(0, len(ids), cr.IN_MAX):
121                 subids = ids[i:i+cr.IN_MAX]
122                 cr.execute('SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % \
123                     (s, f, w), (tuple(subids),))
124                 res.extend([r[0] for r in cr.fetchall()])
125     return res
126
127 def select_distinct_from_where_not_null(cr, s, f):
128     cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % (s, f, s))
129     return [r[0] for r in cr.fetchall()]
130
131 class expression(object):
132     """
133     parse a domain expression
134     use a real polish notation
135     leafs are still in a ('foo', '=', 'bar') format
136     For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
137     """
138
139     def __init__(self, cr, uid, exp, table, context):
140         # check if the expression is valid
141         for x in exp:
142             if not (is_operator(x) or is_leaf(x)):
143                 raise ValueError('Bad domain expression: %r, %r is not a valid operator or a valid term.' % (exp, x))
144         self.__field_tables = {}  # used to store the table to use for the sql generation. key = index of the leaf
145         self.__all_tables = set()
146         self.__joins = []
147         self.__main_table = None # 'root' table. set by parse()
148         # assign self.__exp with the normalized, parsed domain.
149         self.parse(cr, uid, normalize(exp), table, context)
150
151     # TODO used only for osv_memory
152     @property
153     def exp(self):
154         return self.__exp[:]
155
156     def parse(self, cr, uid, exp, table, context):
157         """ transform the leafs of the expression """
158         self.__exp = exp
159
160         def child_of_domain(left, right, table, parent=None, prefix=''):
161             ids = right
162             if table._parent_store and (not table.pool._init):
163 # TODO: Improve where joins are implemented for many with '.', replace by:
164 # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
165                 doms = []
166                 for o in table.browse(cr, uid, ids, context=context):
167                     if doms:
168                         doms.insert(0, OR_OPERATOR)
169                     doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
170                 if prefix:
171                     return [(left, 'in', table.search(cr, uid, doms, context=context))]
172                 return doms
173             else:
174                 def rg(ids, table, parent):
175                     if not ids:
176                         return []
177                     ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context)
178                     return ids + rg(ids2, table, parent)
179                 return [(left, 'in', rg(ids, table, parent or table._parent_name))]
180
181         # TODO rename this function as it is not strictly for 'child_of', but also for 'in'...
182         def child_of_right_to_ids(value, operator, field_obj):
183             """ Normalize a single id, or a string, or a list of ids to a list of ids.
184             """
185             if isinstance(value, basestring):
186                 return [x[0] for x in field_obj.name_search(cr, uid, value, [], operator, context=context, limit=None)]
187             elif isinstance(value, (int, long)):
188                 return [value]
189             else:
190                 return list(value)
191
192         self.__main_table = table
193         self.__all_tables.add(table)
194
195         i = -1
196         while i + 1<len(self.__exp):
197             i += 1
198             e = self.__exp[i]
199             if is_operator(e) or e == TRUE_LEAF or e == FALSE_LEAF:
200                 continue
201             left, operator, right = e
202             operator = operator.lower()
203             working_table = table # The table containing the field (the name provided in the left operand)
204             fargs = left.split('.', 1)
205
206             # If the field is _inherits'd, search for the working_table,
207             # and extract the field.
208             if fargs[0] in table._inherit_fields:
209                 while True:
210                     field = working_table._columns.get(fargs[0])
211                     if field:
212                         self.__field_tables[i] = working_table
213                         break
214                     next_table = working_table.pool.get(working_table._inherit_fields[fargs[0]][0])
215                     if next_table not in self.__all_tables:
216                         self.__joins.append('%s.%s=%s.%s' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name]))
217                         self.__all_tables.add(next_table)
218                     working_table = next_table
219             # Or (try to) directly extract the field.
220             else:
221                 field = working_table._columns.get(fargs[0])
222
223             if not field:
224                 if left == 'id' and operator == 'child_of':
225                     ids2 = child_of_right_to_ids(right, 'ilike', table)
226                     dom = child_of_domain(left, ids2, working_table)
227                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
228                 continue
229
230             field_obj = table.pool.get(field._obj)
231             if len(fargs) > 1:
232                 if field._type == 'many2one':
233                     right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
234                     if right == []:
235                         self.__exp[i] = FALSE_LEAF
236                     else:
237                         self.__exp[i] = (fargs[0], 'in', right)
238                 # Making search easier when there is a left operand as field.o2m or field.m2m
239                 if field._type in ['many2many', 'one2many']:
240                     right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
241                     right1 = table.search(cr, uid, [(fargs[0], 'in', right)], context=context)
242                     if right1 == []:
243                         self.__exp[i] = FALSE_LEAF
244                     else:
245                         self.__exp[i] = ('id', 'in', right1)
246
247                 if not isinstance(field, fields.property):
248                     continue
249
250             if field._properties and not field.store:
251                 # this is a function field that is not stored
252                 if not field._fnct_search:
253                     # the function field doesn't provide a search function and doesn't store
254                     # values in the database, so we must ignore it : we generate a dummy leaf
255                     self.__exp[i] = TRUE_LEAF
256                 else:
257                     subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
258                     if not subexp:
259                         self.__exp[i] = TRUE_LEAF
260                     else:
261                         # we assume that the expression is valid
262                         # we create a dummy leaf for forcing the parsing of the resulting expression
263                         self.__exp[i] = AND_OPERATOR
264                         self.__exp.insert(i + 1, TRUE_LEAF)
265                         for j, se in enumerate(subexp):
266                             self.__exp.insert(i + 2 + j, se)
267             # else, the value of the field is store in the database, so we search on it
268
269             elif field._type == 'one2many':
270                 # Applying recursivity on field(one2many)
271                 if operator == 'child_of':
272                     if field._obj != working_table._name:
273                         ids2 = child_of_right_to_ids(right, 'ilike', field_obj)
274                         dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
275                     else:
276                         ids2 = child_of_right_to_ids(right, 'ilike', field_obj)
277                         dom = child_of_domain('id', ids2, working_table, parent=left)
278                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
279
280                 else:
281                     call_null = True
282
283                     if right is not False:
284                         if isinstance(right, basestring):
285                             ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context, limit=None)]
286                             if ids2:
287                                 operator = 'in'
288                         else:
289                             if not isinstance(right, list):
290                                 ids2 = [right]
291                             else:
292                                 ids2 = right
293                         if not ids2:
294                             if operator in ['like','ilike','in','=']:
295                                 #no result found with given search criteria
296                                 call_null = False
297                                 self.__exp[i] = FALSE_LEAF
298                         else:
299                             call_null = False
300                             o2m_op = 'in'
301                             if operator in  ['not like','not ilike','not in','<>','!=']:
302                                 o2m_op = 'not in'
303                             self.__exp[i] = ('id', o2m_op, select_from_where(cr, field._fields_id, field_obj._table, 'id', ids2, operator))
304
305                     if call_null:
306                         o2m_op = 'not in'
307                         if operator in  ['not like','not ilike','not in','<>','!=']:
308                             o2m_op = 'in'
309                         self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, field_obj._table) or [0])
310
311             elif field._type == 'many2many':
312                 #FIXME
313                 if operator == 'child_of':
314                     def _rec_convert(ids):
315                         if field_obj == table:
316                             return ids
317                         return select_from_where(cr, field._id1, field._rel, field._id2, ids, operator)
318
319                     ids2 = child_of_right_to_ids(right, 'ilike', field_obj)
320                     dom = child_of_domain('id', ids2, field_obj)
321                     ids2 = field_obj.search(cr, uid, dom, context=context)
322                     self.__exp[i] = ('id', 'in', _rec_convert(ids2))
323                 else:
324                     call_null_m2m = True
325                     if right is not False:
326                         if isinstance(right, basestring):
327                             res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context)]
328                             if res_ids:
329                                 operator = 'in'
330                         else:
331                             if not isinstance(right, list):
332                                 res_ids = [right]
333                             else:
334                                 res_ids = right
335                         if not res_ids:
336                             if operator in ['like','ilike','in','=']:
337                                 #no result found with given search criteria
338                                 call_null_m2m = False
339                                 self.__exp[i] = FALSE_LEAF
340                             else:
341                                 operator = 'in' # operator changed because ids are directly related to main object
342                         else:
343                             call_null_m2m = False
344                             m2m_op = 'in'
345                             if operator in  ['not like','not ilike','not in','<>','!=']:
346                                 m2m_op = 'not in'
347                             self.__exp[i] = ('id', m2m_op, select_from_where(cr, field._id1, field._rel, field._id2, res_ids, operator) or [0])
348
349                     if call_null_m2m:
350                         m2m_op = 'not in'
351                         if operator in  ['not like','not ilike','not in','<>','!=']:
352                             m2m_op = 'in'
353                         self.__exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, field._id1, field._rel) or [0])
354
355             elif field._type == 'many2one':
356                 if operator == 'child_of':
357                     if field._obj != working_table._name:
358                         ids2 = child_of_right_to_ids(right, 'ilike', field_obj)
359                         dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
360                     else:
361                         ids2 = child_of_right_to_ids(right, 'ilike', field_obj)
362                         dom = child_of_domain('id', ids2, working_table, parent=left)
363                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
364                 else:
365                     def _get_expression(field_obj, cr, uid, left, right, operator, context=None):
366                         if context is None:
367                             context = {}
368                         c = context.copy()
369                         c['active_test'] = False
370                         #Special treatment to ill-formed domains
371                         operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
372
373                         dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'}
374                         if isinstance(right, tuple):
375                             right = list(right)
376                         if (not isinstance(right, list)) and operator in ['not in','in']:
377                             operator = dict_op[operator]
378                         elif isinstance(right, list) and operator in ['<>','!=','=']: #for domain (FIELD,'=',['value1','value2'])
379                             operator = dict_op[operator]
380                         res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)]
381                         if not res_ids:
382                            return FALSE_LEAF
383                         else:
384                             return (left, 'in', res_ids)
385
386                     m2o_str = False
387                     if right:
388                         if isinstance(right, basestring): # and not isinstance(field, fields.related):
389                             m2o_str = True
390                         elif isinstance(right, (list, tuple)):
391                             m2o_str = True
392                             for ele in right:
393                                 if not isinstance(ele, basestring):
394                                     m2o_str = False
395                                     break
396                     elif right == []:
397                         m2o_str = False
398                         if operator in ('not in', '!=', '<>'):
399                             # (many2one not in []) should return all records
400                             self.__exp[i] = TRUE_LEAF
401                         else:
402                             self.__exp[i] = FALSE_LEAF
403                     else:
404                         new_op = '='
405                         if operator in  ['not like','not ilike','not in','<>','!=']:
406                             new_op = '!='
407                         #Is it ok to put 'left' and not 'id' ?
408                         self.__exp[i] = (left, new_op, False)
409
410                     if m2o_str:
411                         self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context)
412             else:
413                 # other field type
414                 # add the time part to datetime field when it's not there:
415                 if field._type == 'datetime' and self.__exp[i][2] and len(self.__exp[i][2]) == 10:
416
417                     self.__exp[i] = list(self.__exp[i])
418
419                     if operator in ('>', '>='):
420                         self.__exp[i][2] += ' 00:00:00'
421                     elif operator in ('<', '<='):
422                         self.__exp[i][2] += ' 23:59:59'
423
424                     self.__exp[i] = tuple(self.__exp[i])
425
426                 if field.translate:
427                     if operator in ('like', 'ilike', 'not like', 'not ilike'):
428                         right = '%%%s%%' % right
429
430                     operator = operator == '=like' and 'like' or operator
431
432                     query1 = '( SELECT res_id'          \
433                              '    FROM ir_translation'  \
434                              '   WHERE name = %s'       \
435                              '     AND lang = %s'       \
436                              '     AND type = %s'
437                     instr = ' %s'
438                     #Covering in,not in operators with operands (%s,%s) ,etc.
439                     if operator in ['in','not in']:
440                         instr = ','.join(['%s'] * len(right))
441                         query1 += '     AND value ' + operator +  ' ' +" (" + instr + ")"   \
442                              ') UNION ('                \
443                              '  SELECT id'              \
444                              '    FROM "' + working_table._table + '"'       \
445                              '   WHERE "' + left + '" ' + operator + ' ' +" (" + instr + "))"
446                     else:
447                         query1 += '     AND value ' + operator + instr +   \
448                              ') UNION ('                \
449                              '  SELECT id'              \
450                              '    FROM "' + working_table._table + '"'       \
451                              '   WHERE "' + left + '" ' + operator + instr + ")"
452
453                     query2 = [working_table._name + ',' + left,
454                               context.get('lang', False) or 'en_US',
455                               'model',
456                               right,
457                               right,
458                              ]
459
460                     self.__exp[i] = ('id', 'inselect', (query1, query2))
461
462     def __leaf_to_sql(self, leaf, table):
463         left, operator, right = leaf
464
465         if leaf == TRUE_LEAF:
466             query = 'TRUE'
467             params = []
468
469         elif leaf == FALSE_LEAF:
470             query = 'FALSE'
471             params = []
472
473         elif operator == 'inselect':
474             query = '(%s.%s in (%s))' % (table._table, left, right[0])
475             params = right[1]
476
477         elif operator in ['in', 'not in']:
478             params = right and right[:] or []
479             len_before = len(params)
480             for i in range(len_before)[::-1]:
481                 if params[i] == False:
482                     del params[i]
483
484             len_after = len(params)
485             check_nulls = len_after != len_before
486             query = 'FALSE'
487
488             # TODO this code seems broken: 'not in [False]' will become 'true',
489             # i.e. 'not null or null', while I expect it to be 'not null'.
490             if len_after:
491                 if left == 'id':
492                     instr = ','.join(['%s'] * len_after)
493                 else:
494                     instr = ','.join([table._columns[left]._symbol_set[0]] * len_after)
495                 query = '(%s.%s %s (%s))' % (table._table, left, operator, instr)
496             else:
497                 # the case for [field, 'in', []] or [left, 'not in', []]
498                 if operator == 'in':
499                     query = '(%s.%s IS NULL)' % (table._table, left)
500                 else:
501                     query = '(%s.%s IS NOT NULL)' % (table._table, left)
502             if check_nulls:
503                 query = '(%s OR %s.%s IS NULL)' % (query, table._table, left)
504
505         elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '='):
506             query = '(%s.%s IS NULL or %s.%s = false )' % (table._table, left, table._table, left)
507             params = []
508
509         elif (((right == False) and (type(right)==bool)) or (right is None)) and (operator == '='):
510             query = '%s.%s IS NULL ' % (table._table, left)
511             params = []
512
513         elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator in ['<>', '!=']):
514             query = '(%s.%s IS NOT NULL and %s.%s != false)' % (table._table, left, table._table, left)
515             params = []
516
517         elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']):
518             query = '%s.%s IS NOT NULL' % (table._table, left)
519             params = []
520
521         elif (operator == '=?'):
522             if (right is False or right is None):
523                 query = 'TRUE'
524                 params = []
525             elif left in table._columns:
526                 format = table._columns[left]._symbol_set[0]
527                 query = '(%s.%s = %s)' % (table._table, left, format)
528                 params = table._columns[left]._symbol_set[1](right)
529             else:
530                 query = "(%s.%s = '%%s')" % (table._table, left)
531                 params = right
532
533         elif left == 'id':
534             query = '%s.id %s %%s' % (table._table, operator)
535             params = right
536
537         else:
538             like = operator in ('like', 'ilike', 'not like', 'not ilike')
539
540             op = {'=like':'like','=ilike':'ilike'}.get(operator, operator)
541             if left in table._columns:
542                 format = like and '%s' or table._columns[left]._symbol_set[0]
543                 query = '(%s.%s %s %s)' % (table._table, left, op, format)
544             else:
545                 query = "(%s.%s %s '%s')" % (table._table, left, op, right)
546
547             add_null = False
548             if like:
549                 if isinstance(right, str):
550                     str_utf8 = right
551                 elif isinstance(right, unicode):
552                     str_utf8 = right.encode('utf-8')
553                 else:
554                     str_utf8 = str(right)
555                 params = '%%%s%%' % str_utf8
556                 add_null = not str_utf8
557             elif left in table._columns:
558                 params = table._columns[left]._symbol_set[1](right)
559
560             if add_null:
561                 query = '(%s OR %s.%s IS NULL)' % (query, table._table, left)
562
563         if isinstance(params, basestring):
564             params = [params]
565         return (query, params)
566
567
568     def to_sql(self):
569         stack = []
570         params = []
571         # Process the domain from right to left, using a stack, to generate a SQL expression.
572         for i, e in reverse_enumerate(self.__exp):
573             if is_leaf(e, internal=True):
574                 table = self.__field_tables.get(i, self.__main_table)
575                 q, p = self.__leaf_to_sql(e, table)
576                 params.insert(0, p)
577                 stack.append(q)
578             else:
579                 if e == NOT_OPERATOR:
580                     stack.append('(NOT (%s))' % (stack.pop(),))
581                 else:
582                     ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
583                     q1 = stack.pop()
584                     q2 = stack.pop()
585                     stack.append('(%s %s %s)' % (q1, ops[e], q2,))
586
587         assert len(stack) == 1
588         query = stack[0]
589         joins = ' AND '.join(self.__joins)
590         if joins:
591             query = '(%s) AND %s' % (joins, query)
592         return (query, flatten(params))
593
594     def get_tables(self):
595         return ['"%s"' % t._table for t in self.__all_tables]
596
597 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
598