001/*
002 * (C) Copyright 2014-2016 Nuxeo SA (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 *     Maxime Hilaire
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.directory.core;
021
022import java.io.Serializable;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.nuxeo.ecm.core.api.CoreInstance;
034import org.nuxeo.ecm.core.api.CoreSession;
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.IdRef;
039import org.nuxeo.ecm.core.schema.types.Field;
040import org.nuxeo.ecm.directory.BaseSession;
041import org.nuxeo.ecm.directory.DirectoryException;
042import org.nuxeo.ecm.directory.PasswordHelper;
043import org.nuxeo.ecm.directory.Reference;
044
045import com.google.common.base.Function;
046import com.google.common.base.Joiner;
047import com.google.common.collect.Collections2;
048
049/**
050 * Session class for directory on repository
051 *
052 * @since 8.2
053 */
054public class CoreDirectorySession extends BaseSession {
055
056    protected final String schemaIdField;
057
058    protected final String schemaPasswordField;
059
060    protected final CoreSession coreSession;
061
062    protected final String createPath;
063
064    protected final String docType;
065
066    protected static final String UUID_FIELD = "ecm:uuid";
067
068    private final static Log log = LogFactory.getLog(CoreDirectorySession.class);
069
070    public CoreDirectorySession(CoreDirectory directory) {
071        super(directory, null);
072        CoreDirectoryDescriptor descriptor = directory.getDescriptor();
073        coreSession = CoreInstance.openCoreSession(descriptor.getRepositoryName());
074        schemaIdField = directory.getFieldMapper().getBackendField(getIdField());
075        schemaPasswordField = directory.getFieldMapper().getBackendField(getPasswordField());
076        docType = descriptor.docType;
077        createPath = descriptor.getCreatePath();
078    }
079
080    @Override
081    public CoreDirectory getDirectory() {
082        return (CoreDirectory) directory;
083    }
084
085    @Override
086    public DocumentModel getEntry(String id, boolean fetchReferences) throws DirectoryException {
087        if (UUID_FIELD.equals(getIdField())) {
088            IdRef ref = new IdRef(id);
089            if (coreSession.exists(ref)) {
090                DocumentModel document = coreSession.getDocument(new IdRef(id));
091                return docType.equals(document.getType()) ? document : null;
092            } else {
093                return null;
094            }
095        }
096
097        StringBuilder sbQuery = new StringBuilder("SELECT * FROM ");
098        sbQuery.append(docType);
099        sbQuery.append(" WHERE ");
100        sbQuery.append(getDirectory().getField(schemaIdField).getName().getPrefixedName());
101        sbQuery.append(" = '");
102        sbQuery.append(id);
103        sbQuery.append("' AND ecm:path STARTSWITH '");
104        sbQuery.append(createPath);
105        sbQuery.append("'");
106
107        DocumentModelList listDoc = coreSession.query(sbQuery.toString());
108        // TODO : deal with references
109        if (!listDoc.isEmpty()) {
110            // Should have only one
111            if (listDoc.size() > 1) {
112                log.warn(String.format(
113                        "Found more than one result in getEntry, the first result only will be returned"));
114            }
115            DocumentModel docResult = listDoc.get(0);
116            if (isReadOnly()) {
117                BaseSession.setReadOnlyEntry(docResult);
118            }
119            return docResult;
120        }
121        return null;
122    }
123
124    @Override
125    public DocumentModelList getEntries() throws DirectoryException {
126        throw new UnsupportedOperationException();
127    }
128
129    private String getPrefixedFieldName(String fieldName) {
130        if (UUID_FIELD.equals(fieldName)) {
131            return fieldName;
132        }
133        Field schemaField = getDirectory().getField(fieldName);
134        return schemaField.getName().getPrefixedName();
135    }
136
137    @Override
138    public DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) throws DirectoryException {
139        // TODO once references are implemented
140        throw new UnsupportedOperationException();
141    }
142
143    @Override
144    protected List<String> updateEntryWithoutReferences(DocumentModel docModel) throws DirectoryException {
145        // TODO once references are implemented
146        throw new UnsupportedOperationException();
147    }
148
149    @Override
150    protected void deleteEntryWithoutReferences(String id) throws DirectoryException {
151        // TODO once references are implemented
152        throw new UnsupportedOperationException();
153    }
154
155    @Override
156    public DocumentModel createEntry(Map<String, Object> fieldMap) throws DirectoryException {
157        if (isReadOnly()) {
158            log.warn(String.format("The directory '%s' is in read-only mode, could not create entry.",
159                    directory.getName()));
160            return null;
161        }
162        // TODO : deal with auto-versionning
163        // TODO : deal with encrypted password
164        // TODO : deal with references
165        Map<String, Object> properties = new HashMap<>();
166        List<String> createdRefs = new LinkedList<>();
167        for (String fieldId : fieldMap.keySet()) {
168            if (getDirectory().isReference(fieldId)) {
169                createdRefs.add(fieldId);
170            }
171            Object value = fieldMap.get(fieldId);
172            properties.put(getMappedPrefixedFieldName(fieldId), value);
173        }
174
175        String rawid = (String) properties.get(getPrefixedFieldName(schemaIdField));
176        if (rawid == null && (!UUID_FIELD.equals(getIdField()))) {
177            throw new DirectoryException(String.format("Entry is missing id field '%s'", schemaIdField));
178        }
179
180        DocumentModel docModel = coreSession.createDocumentModel(createPath, rawid, docType);
181
182        docModel.setProperties(schemaName, properties);
183        DocumentModel createdDoc = coreSession.createDocument(docModel);
184
185        for (String referenceFieldName : createdRefs) {
186            Reference reference = directory.getReference(referenceFieldName);
187            List<String> targetIds = (List<String>) createdDoc.getProperty(schemaName, referenceFieldName);
188            reference.setTargetIdsForSource(docModel.getId(), targetIds);
189        }
190        return docModel;
191    }
192
193    @Override
194    public void updateEntry(DocumentModel docModel) throws DirectoryException {
195        if (isReadOnly()) {
196            log.warn(String.format("The directory '%s' is in read-only mode, could not update entry.",
197                    directory.getName()));
198        } else {
199
200            if (!isReadOnlyEntry(docModel)) {
201
202                String id = (String) docModel.getProperty(schemaName, getIdField());
203                if (id == null) {
204                    throw new DirectoryException(
205                            "Can not update entry with a null id for document ref " + docModel.getRef());
206                } else {
207                    if (getEntry(id) == null) {
208                        throw new DirectoryException(
209                                String.format("Update entry failed : Entry with id '%s' not found !", id));
210                    } else {
211
212                        DataModel dataModel = docModel.getDataModel(schemaName);
213                        Map<String, Object> updatedProps = new HashMap<String, Object>();
214                        List<String> updatedRefs = new LinkedList<String>();
215
216                        for (String field : docModel.getProperties(schemaName).keySet()) {
217                            String schemaField = getMappedPrefixedFieldName(field);
218                            if (!dataModel.isDirty(schemaField)) {
219                                if (getDirectory().isReference(field)) {
220                                    updatedRefs.add(field);
221                                } else {
222                                    updatedProps.put(schemaField, docModel.getProperties(schemaName).get(field));
223                                }
224                            }
225
226                        }
227
228                        docModel.setProperties(schemaName, updatedProps);
229
230                        // update reference fields
231                        for (String referenceFieldName : updatedRefs) {
232                            Reference reference = directory.getReference(referenceFieldName);
233                            List<String> targetIds = (List<String>) docModel.getProperty(schemaName,
234                                    referenceFieldName);
235                            reference.setTargetIdsForSource(docModel.getId(), targetIds);
236                        }
237
238                        coreSession.saveDocument(docModel);
239                    }
240
241                }
242            }
243        }
244    }
245
246    @Override
247    public void deleteEntry(DocumentModel docModel) throws DirectoryException {
248        String id = (String) docModel.getProperty(schemaName, schemaIdField);
249        deleteEntry(id);
250    }
251
252    @Override
253    public void deleteEntry(String id) throws DirectoryException {
254        if (isReadOnly()) {
255            log.warn(String.format("The directory '%s' is in read-only mode, could not delete entry.",
256                    directory.getName()));
257        } else {
258            if (id == null) {
259                throw new DirectoryException("Can not update entry with a null id ");
260            } else {
261                checkDeleteConstraints(id);
262                DocumentModel docModel = getEntry(id);
263                if (docModel != null) {
264                    coreSession.removeDocument(docModel.getRef());
265                }
266            }
267        }
268    }
269
270    @Override
271    public void deleteEntry(String id, Map<String, String> map) throws DirectoryException {
272        if (isReadOnly()) {
273            log.warn(String.format("The directory '%s' is in read-only mode, could not delete entry.",
274                    directory.getName()));
275        }
276
277        Map<String, Serializable> props = new HashMap<>(map);
278        props.put(schemaIdField, id);
279
280        DocumentModelList docList = query(props);
281        if (!docList.isEmpty()) {
282            if (docList.size() > 1) {
283                log.warn(
284                        String.format("Found more than one result in getEntry, the first result only will be deleted"));
285            }
286            deleteEntry(docList.get(0));
287        } else {
288            throw new DirectoryException(String.format("Delete entry failed : Entry with id '%s' not found !", id));
289        }
290
291    }
292
293    protected String getMappedPrefixedFieldName(String fieldName) {
294        String backendFieldId = getDirectory().getFieldMapper().getBackendField(fieldName);
295        return getPrefixedFieldName(backendFieldId);
296    }
297
298    @Override
299    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
300            boolean fetchReferences, int limit, int offset) throws DirectoryException {
301        StringBuilder sbQuery = new StringBuilder("SELECT * FROM ");
302        sbQuery.append(docType);
303        // TODO deal with fetch ref
304        if (!filter.isEmpty() || !fulltext.isEmpty() || (createPath != null && !createPath.isEmpty())) {
305            sbQuery.append(" WHERE ");
306        }
307        int i = 1;
308        boolean hasFilter = false;
309        for (String filterKey : filter.keySet()) {
310            if (!fulltext.contains(filterKey)) {
311                sbQuery.append(getMappedPrefixedFieldName(filterKey));
312                sbQuery.append(" = ");
313                sbQuery.append("'");
314                sbQuery.append(filter.get(filterKey));
315                sbQuery.append("'");
316                if (i < filter.size()) {
317                    sbQuery.append(" AND ");
318                    i++;
319                }
320                hasFilter = true;
321            }
322
323        }
324        if (hasFilter && filter.size() > 0 && fulltext.size() > 0) {
325            sbQuery.append(" AND ");
326        }
327        if (fulltext.size() > 0) {
328
329            Collection<String> fullTextValues = Collections2.transform(fulltext, new Function<String, String>() {
330
331                @Override
332                public String apply(String key) {
333                    return (String) filter.get(key);
334                }
335
336            });
337            sbQuery.append("ecm:fulltext");
338            sbQuery.append(" = ");
339            sbQuery.append("'");
340            sbQuery.append(Joiner.on(" ").join(fullTextValues));
341            sbQuery.append("'");
342        }
343
344        if ((createPath != null && !createPath.isEmpty())) {
345            if (filter.size() > 0 || fulltext.size() > 0) {
346                sbQuery.append(" AND ");
347            }
348            sbQuery.append(" ecm:path STARTSWITH '");
349            sbQuery.append(createPath);
350            sbQuery.append("'");
351        }
352
353        // Filter facetFilter = new FacetFilter(FacetNames.VERSIONABLE, true);
354
355        DocumentModelList resultsDoc = coreSession.query(sbQuery.toString(), null, limit, offset, false);
356
357        if (isReadOnly()) {
358            for (DocumentModel documentModel : resultsDoc) {
359                BaseSession.setReadOnlyEntry(documentModel);
360            }
361        }
362        return resultsDoc;
363
364    }
365
366    @Override
367    public void close() throws DirectoryException {
368        coreSession.close();
369        getDirectory().removeSession(this);
370    }
371
372    @Override
373    public List<String> getProjection(Map<String, Serializable> filter, String columnName) throws DirectoryException {
374        throw new UnsupportedOperationException();
375    }
376
377    @Override
378    public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName)
379            throws DirectoryException {
380        throw new UnsupportedOperationException();
381    }
382
383    @Override
384    public boolean authenticate(String username, String password) {
385        DocumentModel entry = getEntry(username);
386        if (entry == null) {
387            return false;
388        }
389        String storedPassword = (String) entry.getProperty(schemaName, schemaPasswordField);
390        return PasswordHelper.verifyPassword(password, storedPassword);
391    }
392
393    @Override
394    public boolean isAuthenticating() {
395        return schemaPasswordField != null;
396    }
397
398    @Override
399    public boolean hasEntry(String id) {
400        return getEntry(id) != null;
401    }
402}