001/*
002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 *
019 * $Id: MemoryDirectorySession.java 30374 2008-02-20 16:31:28Z gracinet $
020 */
021
022package org.nuxeo.ecm.directory.memory;
023
024import java.io.Serializable;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Set;
033
034import org.nuxeo.ecm.core.api.DataModel;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentModelComparator;
037import org.nuxeo.ecm.core.api.DocumentModelList;
038import org.nuxeo.ecm.core.api.PropertyException;
039import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
040import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
041import org.nuxeo.ecm.core.api.security.SecurityConstants;
042import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
043import org.nuxeo.ecm.core.query.sql.model.OrderByList;
044import org.nuxeo.ecm.core.query.sql.model.Predicate;
045import org.nuxeo.ecm.core.query.sql.model.QueryBuilder;
046import org.nuxeo.ecm.directory.AbstractDirectory;
047import org.nuxeo.ecm.directory.BaseSession;
048import org.nuxeo.ecm.directory.DirectoryException;
049import org.nuxeo.ecm.directory.BaseSession.FieldDetector;
050
051/**
052 * Trivial in-memory implementation of a Directory to use in unit tests.
053 *
054 * @author Florent Guillaume
055 */
056public class MemoryDirectorySession extends BaseSession {
057
058    protected final Map<String, Map<String, Object>> data;
059
060    protected final String passwordField;
061
062    public MemoryDirectorySession(MemoryDirectory directory) {
063        super(directory, null);
064        data = Collections.synchronizedMap(new LinkedHashMap<String, Map<String, Object>>());
065        passwordField = getPasswordField();
066    }
067
068    /** To be implemented with a more specific type. */
069    @Override
070    public MemoryDirectory getDirectory() {
071        return (MemoryDirectory) directory;
072    }
073
074    @Override
075    public boolean authenticate(String username, String password) {
076        Map<String, Object> map = data.get(username);
077        if (map == null) {
078            return false;
079        }
080        String expected = (String) map.get(passwordField);
081        if (expected == null) {
082            return false;
083        }
084        return expected.equals(password);
085    }
086
087    @Override
088    public void close() {
089    }
090
091    public void commit() {
092    }
093
094    public void rollback() {
095        throw new RuntimeException("Not implemented");
096    }
097
098    @Override
099    public DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) {
100        // find id
101        Object rawId = fieldMap.get(getIdField());
102        if (rawId == null) {
103            throw new DirectoryException("Missing id");
104        }
105        String id = String.valueOf(rawId);
106        Map<String, Object> map = data.get(id);
107        if (map != null) {
108            throw new DirectoryException(String.format("Entry with id %s already exists", id));
109        }
110        map = new HashMap<>();
111        data.put(id, map);
112        // put fields in map
113        for (Entry<String, Object> e : fieldMap.entrySet()) {
114            String fieldName = e.getKey();
115            if (!getDirectory().schemaSet.contains(fieldName)) {
116                continue;
117            }
118            map.put(fieldName, e.getValue());
119        }
120        return getEntry(id);
121    }
122
123    @Override
124    protected List<String> updateEntryWithoutReferences(DocumentModel docModel) {
125        String id = docModel.getId();
126        DataModel dataModel = docModel.getDataModel(directory.getSchema());
127
128        Map<String, Object> map = data.get(id);
129        if (map == null) {
130            throw new DirectoryException("UpdateEntry failed: entry '" + id + "' not found");
131        }
132
133        for (String fieldName : getDirectory().schemaSet) {
134            try {
135                if (!dataModel.isDirty(fieldName) || fieldName.equals(getIdField())) {
136                    continue;
137                }
138            } catch (PropertyNotFoundException e) {
139                continue;
140            }
141            // TODO references
142            map.put(fieldName, dataModel.getData(fieldName));
143        }
144        dataModel.getDirtyFields().clear();
145        return new ArrayList<>();
146    }
147
148    @Override
149    protected void deleteEntryWithoutReferences(String id) {
150        checkDeleteConstraints(id);
151        data.remove(id);
152    }
153
154    @Override
155    public DocumentModel createEntry(Map<String, Object> fieldMap) {
156        checkPermission(SecurityConstants.WRITE);
157        return createEntryWithoutReferences(fieldMap);
158    }
159
160    @Override
161    public void updateEntry(DocumentModel docModel) {
162        checkPermission(SecurityConstants.WRITE);
163        updateEntryWithoutReferences(docModel);
164    }
165
166    @Override
167    public void deleteEntry(String id) {
168        checkPermission(SecurityConstants.WRITE);
169        deleteEntryWithoutReferences(id);
170    }
171
172    @Override
173    public DocumentModel getEntry(String id, boolean fetchReferences) {
174        // XXX no references here
175        Map<String, Object> map = data.get(id);
176        if (map == null) {
177            return null;
178        }
179        if (passwordField != null && map.get(passwordField) != null) {
180            map = new HashMap<>(map);
181            map.remove(passwordField);
182        }
183        try {
184            return createEntryModel(null, directory.getSchema(), id, map, isReadOnly());
185        } catch (PropertyException e) {
186            throw new DirectoryException(e);
187        }
188    }
189
190    @Override
191    public DocumentModelList getEntries() {
192        DocumentModelList list = new DocumentModelListImpl();
193        for (String id : data.keySet()) {
194            list.add(getEntry(id));
195        }
196        return list;
197    }
198
199    // given our storage model this doesn't even make sense, as id field is
200    // unique
201    @Override
202    public void deleteEntry(String id, Map<String, String> map) {
203        throw new DirectoryException("Not implemented");
204    }
205
206    @Override
207    public void deleteEntry(DocumentModel docModel) {
208        deleteEntry(docModel.getId());
209    }
210
211    @Override
212    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
213            boolean fetchReferences, int limit, int offset) {
214        DocumentModelList results = new DocumentModelListImpl();
215        // canonicalize filter
216        Map<String, Object> filt = new HashMap<>();
217        for (Entry<String, Serializable> e : filter.entrySet()) {
218            String fieldName = e.getKey();
219            if (!getDirectory().schemaSet.contains(fieldName)) {
220                continue;
221            }
222            filt.put(fieldName, e.getValue());
223        }
224        // do the search
225        data_loop: for (Entry<String, Map<String, Object>> datae : data.entrySet()) {
226            String id = datae.getKey();
227            Map<String, Object> map = datae.getValue();
228            for (Entry<String, Object> e : filt.entrySet()) {
229                String fieldName = e.getKey();
230                Object expected = e.getValue();
231                Object value = map.get(fieldName);
232                if (value == null) {
233                    if (expected != null) {
234                        continue data_loop;
235                    }
236                } else {
237                    if (fulltext != null && fulltext.contains(fieldName)) {
238                        if (!value.toString().toLowerCase().startsWith(expected.toString().toLowerCase())) {
239                            continue data_loop;
240                        }
241                    } else {
242                        if (!value.equals(expected)) {
243                            continue data_loop;
244                        }
245                    }
246                }
247            }
248            // this entry matches
249            results.add(getEntry(id));
250        }
251        // order entries
252        if (orderBy != null && !orderBy.isEmpty()) {
253            getDirectory().orderEntries(results, orderBy);
254        }
255        return applyQueryLimits(results, limit, offset);
256    }
257
258    @Override
259    public DocumentModelList query(QueryBuilder queryBuilder, boolean fetchReferences) {
260        if (!hasPermission(SecurityConstants.READ)) {
261            return new DocumentModelListImpl();
262        }
263        if (FieldDetector.hasField(queryBuilder.predicate(), passwordField)) {
264            throw new DirectoryException("Cannot filter on password");
265        }
266        DocumentModelList results = new DocumentModelListImpl();
267        Predicate expression = queryBuilder.predicate();
268        OrderByList orders = queryBuilder.orders();
269        int limit = Math.max(0, (int) queryBuilder.limit());
270        int offset = Math.max(0, (int) queryBuilder.offset());
271        boolean countTotal = queryBuilder.countTotal();
272
273        // do the search
274        MemoryDirectoryExpressionEvaluator evaluator = new MemoryDirectoryExpressionEvaluator(getDirectory());
275        for (Entry<String, Map<String, Object>> datae : data.entrySet()) {
276            if (evaluator.matchesEntry(expression, datae.getValue())) {
277                results.add(getEntry(datae.getKey()));
278            }
279        }
280        // order entries
281        if (!orders.isEmpty()) {
282            getDirectory().orderEntries(results, AbstractDirectory.makeOrderBy(orders));
283        }
284        results = applyQueryLimits(results, limit, offset);
285        if ((limit != 0 || offset != 0) && !countTotal) {
286            // compat with other directories
287            ((DocumentModelListImpl) results).setTotalSize(-2);
288        }
289        return results;
290    }
291
292    @Override
293    public List<String> queryIds(QueryBuilder queryBuilder) {
294        if (!hasPermission(SecurityConstants.READ)) {
295            return Collections.emptyList();
296        }
297        if (FieldDetector.hasField(queryBuilder.predicate(), passwordField)) {
298            throw new DirectoryException("Cannot filter on password");
299        }
300        DocumentModelList entries = new DocumentModelListImpl(); // needed if we have ordering
301        List<String> ids = new ArrayList<>();
302        Predicate expression = queryBuilder.predicate();
303        OrderByList orders = queryBuilder.orders();
304        boolean order = !orders.isEmpty();
305        int limit = Math.max(0, (int) queryBuilder.limit());
306        int offset = Math.max(0, (int) queryBuilder.offset());
307
308        // do the search
309        MemoryDirectoryExpressionEvaluator evaluator = new MemoryDirectoryExpressionEvaluator(getDirectory());
310        for (Entry<String, Map<String, Object>> datae : data.entrySet()) {
311            if (evaluator.matchesEntry(expression, datae.getValue())) {
312                String id = datae.getKey();
313                if (order) {
314                    entries.add(getEntry(id));
315                } else {
316                    ids.add(id);
317                }
318            }
319        }
320        // order entries if needed
321        if (order) {
322            getDirectory().orderEntries(entries, AbstractDirectory.makeOrderBy(orders));
323            entries.forEach(doc -> ids.add(doc.getId()));
324        }
325        // apply query limits
326        return applyQueryLimits(ids, limit, offset);
327    }
328
329    @Override
330    public DocumentModel createEntry(DocumentModel entry) {
331        Map<String, Object> fieldMap = entry.getProperties(directory.getSchema());
332        return createEntry(fieldMap);
333    }
334
335    @Override
336    public boolean hasEntry(String id) {
337        return data.containsKey(id);
338    }
339
340}