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.HashMap;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.ecm.core.api.CoreInstance;
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DataModel;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentModelList;
037import org.nuxeo.ecm.core.api.IdRef;
038import org.nuxeo.ecm.core.query.sql.model.QueryBuilder;
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.getCoreSession(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) {
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() {
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) {
139        // TODO once references are implemented
140        throw new UnsupportedOperationException();
141    }
142
143    @Override
144    protected List<String> updateEntryWithoutReferences(DocumentModel docModel) {
145        // TODO once references are implemented
146        throw new UnsupportedOperationException();
147    }
148
149    @Override
150    protected void deleteEntryWithoutReferences(String id) {
151        // TODO once references are implemented
152        throw new UnsupportedOperationException();
153    }
154
155    @Override
156    public DocumentModel createEntry(Map<String, Object> fieldMap) {
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 = toStringList(createdDoc.getProperty(schemaName, referenceFieldName));
188            reference.setTargetIdsForSource(docModel.getId(), targetIds);
189        }
190        return docModel;
191    }
192
193    @Override
194    public void updateEntry(DocumentModel docModel) {
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<>();
214                        List<String> updatedRefs = new LinkedList<>();
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 = toStringList(docModel.getProperty(schemaName, referenceFieldName));
234                            reference.setTargetIdsForSource(docModel.getId(), targetIds);
235                        }
236
237                        coreSession.saveDocument(docModel);
238                    }
239
240                }
241            }
242        }
243    }
244
245    @Override
246    public void deleteEntry(DocumentModel docModel) {
247        String id = (String) docModel.getProperty(schemaName, schemaIdField);
248        deleteEntry(id);
249    }
250
251    @Override
252    public void deleteEntry(String id) {
253        if (isReadOnly()) {
254            log.warn(String.format("The directory '%s' is in read-only mode, could not delete entry.",
255                    directory.getName()));
256        } else {
257            if (id == null) {
258                throw new DirectoryException("Can not update entry with a null id ");
259            } else {
260                checkDeleteConstraints(id);
261                DocumentModel docModel = getEntry(id);
262                if (docModel != null) {
263                    coreSession.removeDocument(docModel.getRef());
264                }
265            }
266        }
267    }
268
269    @Override
270    public void deleteEntry(String id, Map<String, String> map) {
271        if (isReadOnly()) {
272            log.warn(String.format("The directory '%s' is in read-only mode, could not delete entry.",
273                    directory.getName()));
274        }
275
276        Map<String, Serializable> props = new HashMap<>(map);
277        props.put(schemaIdField, id);
278
279        DocumentModelList docList = query(props);
280        if (!docList.isEmpty()) {
281            if (docList.size() > 1) {
282                log.warn(
283                        String.format("Found more than one result in getEntry, the first result only will be deleted"));
284            }
285            deleteEntry(docList.get(0));
286        } else {
287            throw new DirectoryException(String.format("Delete entry failed : Entry with id '%s' not found !", id));
288        }
289
290    }
291
292    protected String getMappedPrefixedFieldName(String fieldName) {
293        String backendFieldId = getDirectory().getFieldMapper().getBackendField(fieldName);
294        return getPrefixedFieldName(backendFieldId);
295    }
296
297    @Override
298    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
299            boolean fetchReferences, int limit, int offset) {
300        StringBuilder sbQuery = new StringBuilder("SELECT * FROM ");
301        sbQuery.append(docType);
302        // TODO deal with fetch ref
303        if (!filter.isEmpty() || !fulltext.isEmpty() || (createPath != null && !createPath.isEmpty())) {
304            sbQuery.append(" WHERE ");
305        }
306        int i = 1;
307        boolean hasFilter = false;
308        for (String filterKey : filter.keySet()) {
309            if (!fulltext.contains(filterKey)) {
310                sbQuery.append(getMappedPrefixedFieldName(filterKey));
311                sbQuery.append(" = ");
312                sbQuery.append("'");
313                sbQuery.append(filter.get(filterKey));
314                sbQuery.append("'");
315                if (i < filter.size()) {
316                    sbQuery.append(" AND ");
317                    i++;
318                }
319                hasFilter = true;
320            }
321
322        }
323        if (hasFilter && filter.size() > 0 && fulltext.size() > 0) {
324            sbQuery.append(" AND ");
325        }
326        if (fulltext.size() > 0) {
327
328            Collection<String> fullTextValues = Collections2.transform(fulltext, new Function<String, String>() {
329
330                @Override
331                public String apply(String key) {
332                    return (String) filter.get(key);
333                }
334
335            });
336            sbQuery.append("ecm:fulltext");
337            sbQuery.append(" = ");
338            sbQuery.append("'");
339            sbQuery.append(Joiner.on(" ").join(fullTextValues));
340            sbQuery.append("'");
341        }
342
343        if ((createPath != null && !createPath.isEmpty())) {
344            if (filter.size() > 0 || fulltext.size() > 0) {
345                sbQuery.append(" AND ");
346            }
347            sbQuery.append(" ecm:path STARTSWITH '");
348            sbQuery.append(createPath);
349            sbQuery.append("'");
350        }
351
352        // Filter facetFilter = new FacetFilter(FacetNames.VERSIONABLE, true);
353
354        DocumentModelList resultsDoc = coreSession.query(sbQuery.toString(), null, limit, offset, false);
355
356        if (isReadOnly()) {
357            for (DocumentModel documentModel : resultsDoc) {
358                BaseSession.setReadOnlyEntry(documentModel);
359            }
360        }
361        return resultsDoc;
362
363    }
364
365    @Override
366    public DocumentModelList query(QueryBuilder queryBuilder, boolean fetchReferences) {
367        throw new UnsupportedOperationException();
368    }
369
370    @Override
371    public List<String> queryIds(QueryBuilder queryBuilder) {
372        throw new UnsupportedOperationException();
373    }
374
375    @Override
376    public void close() {
377        getDirectory().removeSession(this);
378    }
379
380    @Override
381    public List<String> getProjection(Map<String, Serializable> filter, String columnName) {
382        throw new UnsupportedOperationException();
383    }
384
385    @Override
386    public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName) {
387        throw new UnsupportedOperationException();
388    }
389
390    @Override
391    public boolean authenticate(String username, String password) {
392        DocumentModel entry = getEntry(username);
393        if (entry == null) {
394            return false;
395        }
396        String storedPassword = (String) entry.getProperty(schemaName, schemaPasswordField);
397        return PasswordHelper.verifyPassword(password, storedPassword);
398    }
399
400    @Override
401    public boolean isAuthenticating() {
402        return schemaPasswordField != null;
403    }
404
405    @Override
406    public boolean hasEntry(String id) {
407        return getEntry(id) != null;
408    }
409}