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