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 static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
025
026import java.io.Serializable;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.Set;
034
035import org.nuxeo.ecm.core.api.DataModel;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.ecm.core.api.DocumentModelList;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.PropertyException;
040import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
041import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
042import org.nuxeo.ecm.core.api.security.SecurityConstants;
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;
049
050/**
051 * Trivial in-memory implementation of a Directory to use in unit tests.
052 *
053 * @author Florent Guillaume
054 */
055public class MemoryDirectorySession extends BaseSession {
056
057    protected final Map<String, Map<String, Object>> data;
058
059    protected final String passwordField;
060
061    protected NuxeoException closeStackTrace;
062
063    public MemoryDirectorySession(MemoryDirectory directory) {
064        super(directory, null);
065        data = directory.data;
066        passwordField = getPasswordField();
067    }
068
069    /** To be implemented with a more specific type. */
070    @Override
071    public MemoryDirectory getDirectory() {
072        return (MemoryDirectory) directory;
073    }
074
075    @Override
076    public boolean authenticate(String username, String password) {
077        checkClose();
078        Map<String, Object> map = data.get(username);
079        if (map == null) {
080            return false;
081        }
082        String expected = (String) map.get(passwordField);
083        if (expected == null) {
084            return false;
085        }
086        return expected.equals(password);
087    }
088
089    @Override
090    public void close() {
091        NuxeoException exc = new NuxeoException("debug close stack trace");
092        if (closeStackTrace == null) {
093            closeStackTrace = exc;
094        } else {
095            closeStackTrace.addSuppressed(exc);
096        }
097        getDirectory().removeSession(this);
098    }
099
100    protected void checkClose() {
101        if (closeStackTrace != null) {
102            NuxeoException exc = new NuxeoException("Session is closed");
103            exc.addSuppressed(closeStackTrace);
104            throw exc;
105        }
106    }
107
108    public void commit() {
109    }
110
111    public void rollback() {
112        throw new RuntimeException("Not implemented");
113    }
114
115    @Override
116    public DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) {
117        checkClose();
118        // find id
119        Object rawId = fieldMap.get(getIdField());
120        if (rawId == null) {
121            throw new DirectoryException("Missing id");
122        }
123        String id = String.valueOf(rawId);
124        Map<String, Object> map = data.get(id);
125        if (map != null) {
126            throw new DirectoryException(
127                    String.format("Entry with id %s already exists in directory %s", id, directory.getName()),
128                    SC_CONFLICT);
129        }
130        map = new HashMap<>();
131        data.put(id, map);
132        // put fields in map
133        for (Entry<String, Object> e : fieldMap.entrySet()) {
134            String fieldName = e.getKey();
135            if (!getDirectory().schemaSet.contains(fieldName)) {
136                continue;
137            }
138            map.put(fieldName, e.getValue());
139        }
140        return getEntry(id);
141    }
142
143    @Override
144    protected List<String> updateEntryWithoutReferences(DocumentModel docModel) {
145        checkClose();
146        String id = docModel.getId();
147        DataModel dataModel = docModel.getDataModel(directory.getSchema());
148
149        Map<String, Object> map = data.get(id);
150        if (map == null) {
151            throw new DirectoryException("UpdateEntry failed: entry '" + id + "' not found");
152        }
153
154        for (String fieldName : getDirectory().schemaSet) {
155            try {
156                if (!dataModel.isDirty(fieldName) || fieldName.equals(getIdField())) {
157                    continue;
158                }
159            } catch (PropertyNotFoundException e) {
160                continue;
161            }
162            // TODO references
163            map.put(fieldName, dataModel.getData(fieldName));
164        }
165        dataModel.getDirtyFields().clear();
166        return new ArrayList<>();
167    }
168
169    @Override
170    protected void deleteEntryWithoutReferences(String id) {
171        checkClose();
172        checkDeleteConstraints(id);
173        data.remove(id);
174    }
175
176    @Override
177    public DocumentModel createEntry(Map<String, Object> fieldMap) {
178        checkClose();
179        checkPermission(SecurityConstants.WRITE);
180        return createEntryWithoutReferences(fieldMap);
181    }
182
183    @Override
184    public void updateEntry(DocumentModel docModel) {
185        checkClose();
186        checkPermission(SecurityConstants.WRITE);
187        updateEntryWithoutReferences(docModel);
188    }
189
190    @Override
191    public void deleteEntry(String id) {
192        checkClose();
193        checkPermission(SecurityConstants.WRITE);
194        deleteEntryWithoutReferences(id);
195    }
196
197    @Override
198    public DocumentModel getEntry(String id, boolean fetchReferences) {
199        checkClose();
200        // XXX no references here
201        Map<String, Object> map = data.get(id);
202        if (map == null) {
203            return null;
204        }
205        if (passwordField != null && map.get(passwordField) != null) {
206            map = new HashMap<>(map);
207            map.remove(passwordField);
208        }
209        try {
210            return createEntryModel(null, directory.getSchema(), id, map, isReadOnly());
211        } catch (PropertyException e) {
212            throw new DirectoryException(e);
213        }
214    }
215
216    @Override
217    public DocumentModelList getEntries() {
218        checkClose();
219        DocumentModelList list = new DocumentModelListImpl();
220        for (String id : data.keySet()) {
221            list.add(getEntry(id));
222        }
223        return list;
224    }
225
226    // given our storage model this doesn't even make sense, as id field is
227    // unique
228    @Override
229    public void deleteEntry(String id, Map<String, String> map) {
230        checkClose();
231        throw new DirectoryException("Not implemented");
232    }
233
234    @Override
235    public void deleteEntry(DocumentModel docModel) {
236        checkClose();
237        deleteEntry(docModel.getId());
238    }
239
240    @Override
241    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
242            boolean fetchReferences, int limit, int offset) {
243        checkClose();
244        DocumentModelList results = new DocumentModelListImpl();
245        // canonicalize filter
246        Map<String, Object> filt = new HashMap<>();
247        for (Entry<String, Serializable> e : filter.entrySet()) {
248            String fieldName = e.getKey();
249            if (!getDirectory().schemaSet.contains(fieldName)) {
250                continue;
251            }
252            filt.put(fieldName, e.getValue());
253        }
254        // do the search
255        data_loop: for (Entry<String, Map<String, Object>> datae : data.entrySet()) {
256            String id = datae.getKey();
257            Map<String, Object> map = datae.getValue();
258            for (Entry<String, Object> e : filt.entrySet()) {
259                String fieldName = e.getKey();
260                Object expected = e.getValue();
261                Object value = map.get(fieldName);
262                if (value == null) {
263                    if (expected != null) {
264                        continue data_loop;
265                    }
266                } else {
267                    if (fulltext != null && fulltext.contains(fieldName)) {
268                        if (!value.toString().toLowerCase().startsWith(expected.toString().toLowerCase())) {
269                            continue data_loop;
270                        }
271                    } else {
272                        if (!value.equals(expected)) {
273                            continue data_loop;
274                        }
275                    }
276                }
277            }
278            // this entry matches
279            results.add(getEntry(id));
280        }
281        // order entries
282        if (orderBy != null && !orderBy.isEmpty()) {
283            getDirectory().orderEntries(results, orderBy);
284        }
285        return applyQueryLimits(results, limit, offset);
286    }
287
288    @Override
289    public DocumentModelList query(QueryBuilder queryBuilder, boolean fetchReferences) {
290        checkClose();
291        if (!hasPermission(SecurityConstants.READ)) {
292            return new DocumentModelListImpl();
293        }
294        if (FieldDetector.hasField(queryBuilder.predicate(), passwordField)) {
295            throw new DirectoryException("Cannot filter on password");
296        }
297        DocumentModelList results = new DocumentModelListImpl();
298        Predicate expression = queryBuilder.predicate();
299        OrderByList orders = queryBuilder.orders();
300        int limit = Math.max(0, (int) queryBuilder.limit());
301        int offset = Math.max(0, (int) queryBuilder.offset());
302        boolean countTotal = queryBuilder.countTotal();
303
304        // do the search
305        MemoryDirectoryExpressionEvaluator evaluator = new MemoryDirectoryExpressionEvaluator(getDirectory());
306        for (Entry<String, Map<String, Object>> datae : data.entrySet()) {
307            if (evaluator.matchesEntry(expression, datae.getValue())) {
308                results.add(getEntry(datae.getKey()));
309            }
310        }
311        // order entries
312        if (!orders.isEmpty()) {
313            getDirectory().orderEntries(results, AbstractDirectory.makeOrderBy(orders));
314        }
315        results = applyQueryLimits(results, limit, offset);
316        if ((limit != 0 || offset != 0) && !countTotal) {
317            // compat with other directories
318            ((DocumentModelListImpl) results).setTotalSize(-2);
319        }
320        return results;
321    }
322
323    @Override
324    public List<String> queryIds(QueryBuilder queryBuilder) {
325        checkClose();
326        if (!hasPermission(SecurityConstants.READ)) {
327            return Collections.emptyList();
328        }
329        if (FieldDetector.hasField(queryBuilder.predicate(), passwordField)) {
330            throw new DirectoryException("Cannot filter on password");
331        }
332        DocumentModelList entries = new DocumentModelListImpl(); // needed if we have ordering
333        List<String> ids = new ArrayList<>();
334        Predicate expression = queryBuilder.predicate();
335        OrderByList orders = queryBuilder.orders();
336        boolean order = !orders.isEmpty();
337        int limit = Math.max(0, (int) queryBuilder.limit());
338        int offset = Math.max(0, (int) queryBuilder.offset());
339
340        // do the search
341        MemoryDirectoryExpressionEvaluator evaluator = new MemoryDirectoryExpressionEvaluator(getDirectory());
342        for (Entry<String, Map<String, Object>> datae : data.entrySet()) {
343            if (evaluator.matchesEntry(expression, datae.getValue())) {
344                String id = datae.getKey();
345                if (order) {
346                    entries.add(getEntry(id));
347                } else {
348                    ids.add(id);
349                }
350            }
351        }
352        // order entries if needed
353        if (order) {
354            getDirectory().orderEntries(entries, AbstractDirectory.makeOrderBy(orders));
355            entries.forEach(doc -> ids.add(doc.getId()));
356        }
357        // apply query limits
358        return applyQueryLimits(ids, limit, offset);
359    }
360
361    @Override
362    public DocumentModel createEntry(DocumentModel entry) {
363        checkClose();
364        Map<String, Object> fieldMap = entry.getProperties(directory.getSchema());
365        return createEntry(fieldMap);
366    }
367
368    @Override
369    public boolean hasEntry(String id) {
370        checkClose();
371        return data.containsKey(id);
372    }
373
374}