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.query.sql.model.QueryBuilder;
041import org.nuxeo.ecm.core.schema.types.Field;
042import org.nuxeo.ecm.directory.BaseSession;
043import org.nuxeo.ecm.directory.DirectoryException;
044import org.nuxeo.ecm.directory.PasswordHelper;
045import org.nuxeo.ecm.directory.Reference;
046
047import com.google.common.base.Function;
048import com.google.common.base.Joiner;
049import com.google.common.collect.Collections2;
050
051/**
052 * Session class for directory on repository
053 *
054 * @since 8.2
055 */
056public class CoreDirectorySession extends BaseSession {
057
058    protected final String schemaIdField;
059
060    protected final String schemaPasswordField;
061
062    protected final CoreSession coreSession;
063
064    protected final String createPath;
065
066    protected final String docType;
067
068    protected static final String UUID_FIELD = "ecm:uuid";
069
070    private final static Log log = LogFactory.getLog(CoreDirectorySession.class);
071
072    public CoreDirectorySession(CoreDirectory directory) {
073        super(directory, null);
074        CoreDirectoryDescriptor descriptor = directory.getDescriptor();
075        coreSession = CoreInstance.openCoreSession(descriptor.getRepositoryName());
076        schemaIdField = directory.getFieldMapper().getBackendField(getIdField());
077        schemaPasswordField = directory.getFieldMapper().getBackendField(getPasswordField());
078        docType = descriptor.docType;
079        createPath = descriptor.getCreatePath();
080    }
081
082    @Override
083    public CoreDirectory getDirectory() {
084        return (CoreDirectory) directory;
085    }
086
087    @Override
088    public DocumentModel getEntry(String id, boolean fetchReferences) {
089        if (UUID_FIELD.equals(getIdField())) {
090            IdRef ref = new IdRef(id);
091            if (coreSession.exists(ref)) {
092                DocumentModel document = coreSession.getDocument(new IdRef(id));
093                return docType.equals(document.getType()) ? document : null;
094            } else {
095                return null;
096            }
097        }
098
099        StringBuilder sbQuery = new StringBuilder("SELECT * FROM ");
100        sbQuery.append(docType);
101        sbQuery.append(" WHERE ");
102        sbQuery.append(getDirectory().getField(schemaIdField).getName().getPrefixedName());
103        sbQuery.append(" = '");
104        sbQuery.append(id);
105        sbQuery.append("' AND ecm:path STARTSWITH '");
106        sbQuery.append(createPath);
107        sbQuery.append("'");
108
109        DocumentModelList listDoc = coreSession.query(sbQuery.toString());
110        // TODO : deal with references
111        if (!listDoc.isEmpty()) {
112            // Should have only one
113            if (listDoc.size() > 1) {
114                log.warn(String.format(
115                        "Found more than one result in getEntry, the first result only will be returned"));
116            }
117            DocumentModel docResult = listDoc.get(0);
118            if (isReadOnly()) {
119                BaseSession.setReadOnlyEntry(docResult);
120            }
121            return docResult;
122        }
123        return null;
124    }
125
126    @Override
127    public DocumentModelList getEntries() {
128        throw new UnsupportedOperationException();
129    }
130
131    private String getPrefixedFieldName(String fieldName) {
132        if (UUID_FIELD.equals(fieldName)) {
133            return fieldName;
134        }
135        Field schemaField = getDirectory().getField(fieldName);
136        return schemaField.getName().getPrefixedName();
137    }
138
139    @Override
140    public DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) {
141        // TODO once references are implemented
142        throw new UnsupportedOperationException();
143    }
144
145    @Override
146    protected List<String> updateEntryWithoutReferences(DocumentModel docModel) {
147        // TODO once references are implemented
148        throw new UnsupportedOperationException();
149    }
150
151    @Override
152    protected void deleteEntryWithoutReferences(String id) {
153        // TODO once references are implemented
154        throw new UnsupportedOperationException();
155    }
156
157    @Override
158    public DocumentModel createEntry(Map<String, Object> fieldMap) {
159        if (isReadOnly()) {
160            log.warn(String.format("The directory '%s' is in read-only mode, could not create entry.",
161                    directory.getName()));
162            return null;
163        }
164        // TODO : deal with auto-versionning
165        // TODO : deal with encrypted password
166        // TODO : deal with references
167        Map<String, Object> properties = new HashMap<>();
168        List<String> createdRefs = new LinkedList<>();
169        for (String fieldId : fieldMap.keySet()) {
170            if (getDirectory().isReference(fieldId)) {
171                createdRefs.add(fieldId);
172            }
173            Object value = fieldMap.get(fieldId);
174            properties.put(getMappedPrefixedFieldName(fieldId), value);
175        }
176
177        String rawid = (String) properties.get(getPrefixedFieldName(schemaIdField));
178        if (rawid == null && (!UUID_FIELD.equals(getIdField()))) {
179            throw new DirectoryException(String.format("Entry is missing id field '%s'", schemaIdField));
180        }
181
182        DocumentModel docModel = coreSession.createDocumentModel(createPath, rawid, docType);
183
184        docModel.setProperties(schemaName, properties);
185        DocumentModel createdDoc = coreSession.createDocument(docModel);
186
187        for (String referenceFieldName : createdRefs) {
188            Reference reference = directory.getReference(referenceFieldName);
189            List<String> targetIds = toStringList(createdDoc.getProperty(schemaName, referenceFieldName));
190            reference.setTargetIdsForSource(docModel.getId(), targetIds);
191        }
192        return docModel;
193    }
194
195    @Override
196    public void updateEntry(DocumentModel docModel) {
197        if (isReadOnly()) {
198            log.warn(String.format("The directory '%s' is in read-only mode, could not update entry.",
199                    directory.getName()));
200        } else {
201
202            if (!isReadOnlyEntry(docModel)) {
203
204                String id = (String) docModel.getProperty(schemaName, getIdField());
205                if (id == null) {
206                    throw new DirectoryException(
207                            "Can not update entry with a null id for document ref " + docModel.getRef());
208                } else {
209                    if (getEntry(id) == null) {
210                        throw new DirectoryException(
211                                String.format("Update entry failed : Entry with id '%s' not found !", id));
212                    } else {
213
214                        DataModel dataModel = docModel.getDataModel(schemaName);
215                        Map<String, Object> updatedProps = new HashMap<String, Object>();
216                        List<String> updatedRefs = new LinkedList<String>();
217
218                        for (String field : docModel.getProperties(schemaName).keySet()) {
219                            String schemaField = getMappedPrefixedFieldName(field);
220                            if (!dataModel.isDirty(schemaField)) {
221                                if (getDirectory().isReference(field)) {
222                                    updatedRefs.add(field);
223                                } else {
224                                    updatedProps.put(schemaField, docModel.getProperties(schemaName).get(field));
225                                }
226                            }
227
228                        }
229
230                        docModel.setProperties(schemaName, updatedProps);
231
232                        // update reference fields
233                        for (String referenceFieldName : updatedRefs) {
234                            Reference reference = directory.getReference(referenceFieldName);
235                            List<String> targetIds = toStringList(docModel.getProperty(schemaName, 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) {
249        String id = (String) docModel.getProperty(schemaName, schemaIdField);
250        deleteEntry(id);
251    }
252
253    @Override
254    public void deleteEntry(String id) {
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) {
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) {
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 DocumentModelList query(QueryBuilder queryBuilder, boolean fetchReferences) {
369        throw new UnsupportedOperationException();
370    }
371
372    @Override
373    public List<String> queryIds(QueryBuilder queryBuilder) {
374        throw new UnsupportedOperationException();
375    }
376
377    @Override
378    public void close() {
379        ((CloseableCoreSession) coreSession).close();
380        getDirectory().removeSession(this);
381    }
382
383    @Override
384    public List<String> getProjection(Map<String, Serializable> filter, String columnName) {
385        throw new UnsupportedOperationException();
386    }
387
388    @Override
389    public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName) {
390        throw new UnsupportedOperationException();
391    }
392
393    @Override
394    public boolean authenticate(String username, String password) {
395        DocumentModel entry = getEntry(username);
396        if (entry == null) {
397            return false;
398        }
399        String storedPassword = (String) entry.getProperty(schemaName, schemaPasswordField);
400        return PasswordHelper.verifyPassword(password, storedPassword);
401    }
402
403    @Override
404    public boolean isAuthenticating() {
405        return schemaPasswordField != null;
406    }
407
408    @Override
409    public boolean hasEntry(String id) {
410        return getEntry(id) != null;
411    }
412}