df3247f76b720d8d7c4c701db835496027dc0c42
[matthijs/upstream/django-ldapdb.git] / ldapdb / backends / ldap / compiler.py
1 # -*- coding: utf-8 -*-
2
3 # django-ldapdb
4 # Copyright (c) 2009-2010, Bolloré telecom
5 # All rights reserved.
6
7 # See AUTHORS file for a full list of contributors.
8
9 # Redistribution and use in source and binary forms, with or without modification,
10 # are permitted provided that the following conditions are met:
11
12 #     1. Redistributions of source code must retain the above copyright notice, 
13 #        this list of conditions and the following disclaimer.
14 #     
15 #     2. Redistributions in binary form must reproduce the above copyright 
16 #        notice, this list of conditions and the following disclaimer in the
17 #        documentation and/or other materials provided with the distribution.
18
19 #     3. Neither the name of Bolloré telecom nor the names of its contributors
20 #        may be used to endorse or promote products derived from this software
21 #        without specific prior written permission.
22
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
27 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
30 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 #
34
35 import ldap
36
37 from django.db.models.sql import compiler
38
39 def get_lookup_operator(lookup_type):
40     if lookup_type == 'gte':
41         return '>='
42     elif lookup_type == 'lte':
43         return '<='
44     else:
45         return '='
46
47 def where_as_sql(self, qn=None, connection=None):
48     bits = []
49     for item in self.children:
50         if hasattr(item, 'as_sql'):
51             sql, params = where_as_sql(item, qn=qn, connection=connection)
52             bits.append(sql)
53             continue
54
55         constraint, lookup_type, y, values = item
56         comp = get_lookup_operator(lookup_type)
57         if lookup_type == 'in':
58             equal_bits = [ "(%s%s%s)" % (constraint.col, comp, value) for value in values ]
59             clause = '(|%s)' % ''.join(equal_bits)
60         else:
61             clause = "(%s%s%s)" % (constraint.col, comp, values)
62
63         bits.append(clause)
64
65     if not len(bits):
66         return '', []
67
68     if len(bits) == 1:
69         sql_string = bits[0]
70     elif self.connector == AND:
71         sql_string = '(&%s)' % ''.join(bits)
72     elif self.connector == OR:
73         sql_string = '(|%s)' % ''.join(bits)
74     else:
75         raise Exception("Unhandled WHERE connector: %s" % self.connector)
76
77     if self.negated:
78         sql_string = ('(!%s)' % sql_string)
79
80     return sql_string, []
81
82 class SQLCompiler(object):
83     def __init__(self, query, connection, using):
84         self.query = query
85         self.connection = connection
86         self.using = using
87
88     def _ldap_filter(self):
89         filterstr = ''.join(['(objectClass=%s)' % cls for cls in self.query.model.object_classes])
90         sql, params = where_as_sql(self.query.where)
91         filterstr += sql
92         return '(&%s)' % filterstr
93
94     def results_iter(self):
95         if self.query.select_fields:
96             fields = self.query.select_fields
97         else:
98             fields = self.query.model._meta.fields
99
100         attrlist = [ x.db_column for x in fields if x.db_column ]
101
102         try:
103             vals = self.connection.search_s(
104                 self.query.model.base_dn,
105                 self.query.model.search_scope,
106                 filterstr=self._ldap_filter(),
107                 attrlist=attrlist,
108             )
109         except ldap.NO_SUCH_OBJECT:
110             return
111
112         # perform sorting
113         if self.query.extra_order_by:
114             ordering = self.query.extra_order_by
115         elif not self.query.default_ordering:
116             ordering = self.query.order_by
117         else:
118             ordering = self.query.order_by or self.query.model._meta.ordering
119         def cmpvals(x, y):
120             for fieldname in ordering:
121                 if fieldname.startswith('-'):
122                     fieldname = fieldname[1:]
123                     negate = True
124                 else:
125                     negate = False
126                 field = self.query.model._meta.get_field(fieldname)
127                 attr_x = field.from_ldap(x[1].get(field.db_column, []), connection=self.connection)
128                 attr_y = field.from_ldap(y[1].get(field.db_column, []), connection=self.connection)
129                 # perform case insensitive comparison
130                 if hasattr(attr_x, 'lower'):
131                     attr_x = attr_x.lower()
132                 if hasattr(attr_y, 'lower'):
133                     attr_y = attr_y.lower()
134                 val = negate and cmp(attr_y, attr_x) or cmp(attr_x, attr_y)
135                 if val:
136                     return val
137             return 0
138         vals = sorted(vals, cmp=cmpvals)
139
140         # process results
141         pos = 0
142         for dn, attrs in vals:
143             # FIXME : This is not optimal, we retrieve more results than we need
144             # but there is probably no other options as we can't perform ordering
145             # server side.
146             if (self.query.low_mark and pos < self.query.low_mark) or \
147                (self.query.high_mark is not None and pos >= self.query.high_mark):
148                 pos += 1
149                 continue
150             row = []
151             for field in iter(fields):
152                 if field.attname == 'dn':
153                     row.append(dn)
154                 elif hasattr(field, 'from_ldap'):
155                     row.append(field.from_ldap(attrs.get(field.db_column, []), connection=self.connection))
156                 else:
157                     row.append(None)
158             yield row
159             pos += 1
160
161 class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
162     pass
163
164 class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
165     def execute_sql(self, result_type=compiler.MULTI):
166         try:
167             vals = self.connection.search_s(
168                 self.query.model.base_dn,
169                 self.query.model.search_scope,
170                 filterstr=self._ldap_filter(),
171                 attrlist=[],
172             )
173         except ldap.NO_SUCH_OBJECT:
174             return
175
176         # FIXME : there is probably a more efficient way to do this 
177         for dn, attrs in vals:
178             self.connection.delete_s(dn)
179
180 class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
181     pass
182
183 class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):
184     pass
185
186 class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler):
187     pass
188