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