001/*
002 * (C) Copyright 2006-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 *     Nuxeo - initial API and implementation
018 */
019package org.nuxeo.ecm.webapp.documentsLists;
020
021import java.io.Serializable;
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.commons.codec.binary.Base64;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.DocumentModelList;
035import org.nuxeo.ecm.core.api.DocumentNotFoundException;
036import org.nuxeo.ecm.core.api.DocumentRef;
037import org.nuxeo.ecm.core.api.DocumentSecurityException;
038import org.nuxeo.ecm.core.api.IdRef;
039import org.nuxeo.ecm.core.api.PathRef;
040import org.nuxeo.ecm.directory.DirectoryException;
041import org.nuxeo.ecm.directory.Session;
042import org.nuxeo.ecm.directory.api.DirectoryService;
043import org.nuxeo.ecm.platform.ui.web.directory.DirectoryHelper;
044
045/**
046 * Manage DocumentsLists Persistence. Uses a SQL Directory as storage Backend.
047 *
048 * @author tiry
049 */
050public class DocumentsListsPersistenceManager {
051
052    private static final String DIR_NAME = "documentsLists";
053
054    private static final String ID_SEP = ":";
055
056    private static final String DIR_COL_USERID = "userid";
057
058    private static final String DIR_COL_LISTID = "listid";
059
060    private static final String DIR_COL_REF = "ref";
061
062    private static final String DIR_COL_REFTYPE = "reftype";
063
064    private static final String DIR_COL_REPO = "repo";
065
066    // Hey, you never know !
067    private static final boolean ENABLE_SANITY_CHECK = true;
068
069    private static final boolean FIX_SANITY_ERROR = true;
070
071    private static final Log log = LogFactory.getLog(DocumentsListsPersistenceManager.class);
072
073    private DirectoryService directoryService;
074
075    private Session dirSession;
076
077    private String directorySchema;
078
079    private static String getIdForEntry(String userName, String listName, DocumentModel doc) {
080        String ref = doc.getRef().toString();
081        int refType = doc.getRef().type();
082        String repoId = doc.getRepositoryName();
083
084        return getIdForEntry(userName, listName, ref, refType, repoId);
085    }
086
087    private static String getIdForEntry(String userName, String listName, String ref, int refType, String repoId) {
088        StringBuilder sb = new StringBuilder();
089        sb.append(listName);
090        sb.append(ID_SEP);
091        sb.append(userName);
092        sb.append(ID_SEP);
093        sb.append(refType);
094        sb.append(ID_SEP);
095        sb.append(ref);
096        sb.append(ID_SEP);
097        sb.append(repoId);
098
099        byte[] idDigest;
100        try {
101            idDigest = MessageDigest.getInstance("MD5").digest(sb.toString().getBytes());
102        } catch (NoSuchAlgorithmException e) {
103            // should never append
104            return sb.toString();
105        }
106        return Base64.encodeBase64String(idDigest);
107    }
108
109    private boolean initPersistentService() {
110        if (dirSession != null) {
111            return true;
112        }
113
114        if (directoryService == null) {
115            directoryService = DirectoryHelper.getDirectoryService();
116            if (directoryService == null) {
117                return false;
118            }
119        }
120
121        try {
122            dirSession = directoryService.open(DIR_NAME);
123            directorySchema = directoryService.getDirectorySchema(DIR_NAME);
124        } catch (DirectoryException e) {
125            dirSession = null;
126            log.error("Unable to open directory " + DIR_NAME + " : " + e.getMessage());
127            return false;
128        }
129        return true;
130    }
131
132    private void releasePersistenceService() {
133        // for now directory sessions are lost during passivation of the DirectoryFacade
134        // this can't be tested on the client side
135        // => release directorySession after each call ...
136
137        if (directoryService == null) {
138            dirSession = null;
139            return;
140        }
141        if (dirSession != null) {
142            try {
143                dirSession.close();
144            } catch (DirectoryException e) {
145                // do nothing
146            }
147        }
148        dirSession = null;
149    }
150
151    private static DocumentModel getDocModel(CoreSession session, String ref, long refType, String repoId) {
152
153        if (!session.getRepositoryName().equals(repoId)) {
154            log.error("Multiple repository management is not handled in current implementation");
155            return null;
156        }
157        DocumentRef docRef;
158        if (refType == DocumentRef.ID) {
159            docRef = new IdRef(ref);
160        } else if (refType == DocumentRef.PATH) {
161            docRef = new PathRef(ref);
162        } else {
163            log.error("Unknown reference type");
164            return null;
165        }
166
167        try {
168            return session.getDocument(docRef);
169        } catch (DocumentSecurityException | DocumentNotFoundException e) {
170            log.warn("document with ref " + ref + " was not found : " + e.getMessage());
171            return null;
172        }
173    }
174
175    public List<DocumentModel> loadPersistentDocumentsLists(CoreSession currentSession, String userName,
176            String listName) {
177        List<DocumentModel> docList = new ArrayList<>();
178        if (!initPersistentService()) {
179            return docList;
180        }
181        try {
182            Map<String, Serializable> filter = new HashMap<>();
183            filter.put(DIR_COL_LISTID, listName);
184            filter.put(DIR_COL_USERID, userName);
185
186            DocumentModelList entries;
187            try {
188                entries = dirSession.query(filter);
189            } catch (DirectoryException e) {
190                log.error(e, e);
191                return docList;
192            }
193
194            for (DocumentModel entry : entries) {
195                String ref = (String) entry.getProperty(directorySchema, DIR_COL_REF);
196                long reftype = (Long) entry.getProperty(directorySchema, DIR_COL_REFTYPE);
197                String repo = (String) entry.getProperty(directorySchema, DIR_COL_REPO);
198
199                DocumentModel doc = getDocModel(currentSession, ref, reftype, repo);
200
201                if (doc != null) {
202                    if (ENABLE_SANITY_CHECK) {
203                        if (docList.contains(doc)) {
204                            log.warn("Document " + doc.getRef().toString() + " is duplicated in persistent list "
205                                    + listName);
206                            if (FIX_SANITY_ERROR) {
207                                try {
208                                    dirSession.deleteEntry(entry.getId());
209                                } catch (DirectoryException e) {
210                                    log.error("Sanity fix failed", e);
211                                }
212                            }
213                        } else {
214                            docList.add(doc);
215                        }
216                    } else {
217                        docList.add(doc);
218                    }
219                } else {
220                    // not found => do the remove
221                    try {
222                        dirSession.deleteEntry(entry.getId());
223                    } catch (DirectoryException e) {
224                        log.error("Unable to remove non existing document model entry : " + entry.getId(), e);
225                    }
226                }
227            }
228            return docList;
229        } finally {
230            releasePersistenceService();
231        }
232    }
233
234    public Boolean addDocumentToPersistentList(String userName, String listName, DocumentModel doc) {
235        if (!initPersistentService()) {
236            return false;
237        }
238        try {
239            Map<String, Object> fields = new HashMap<>();
240            fields.put(DIR_COL_LISTID, listName);
241            fields.put(DIR_COL_USERID, userName);
242            fields.put(DIR_COL_REF, doc.getRef().toString());
243            fields.put(DIR_COL_REFTYPE, (long) doc.getRef().type());
244            fields.put(DIR_COL_REPO, doc.getRepositoryName());
245            String id = getIdForEntry(userName, listName, doc);
246            fields.put("id", id);
247            try {
248                if (ENABLE_SANITY_CHECK) {
249                    DocumentModel badEntry = dirSession.getEntry(id);
250                    if (badEntry != null) {
251                        log.warn("Entry with id " + id + " is already present : please check DB integrity");
252                        if (FIX_SANITY_ERROR) {
253                            dirSession.deleteEntry(id);
254                        }
255                    }
256                }
257                dirSession.createEntry(fields);
258            } catch (DirectoryException e) {
259                log.error("Unable to create entry", e);
260                return false;
261            }
262            return true;
263        } finally {
264            releasePersistenceService();
265        }
266    }
267
268    public boolean removeDocumentFromPersistentList(String userName, String listName, DocumentModel doc) {
269        if (!initPersistentService()) {
270            return false;
271        }
272        try {
273            String entryId = getIdForEntry(userName, listName, doc);
274            try {
275                dirSession.deleteEntry(entryId);
276            } catch (DirectoryException e) {
277                log.error("Unable to delete entry", e);
278                return false;
279            }
280            return true;
281        } finally {
282            releasePersistenceService();
283        }
284    }
285
286    public boolean clearPersistentList(String userName, String listName) {
287        if (!initPersistentService()) {
288            return false;
289        }
290        try {
291            Map<String, Serializable> filter = new HashMap<>();
292            filter.put(DIR_COL_LISTID, listName);
293            filter.put(DIR_COL_USERID, userName);
294            try {
295                DocumentModelList entriesToDelete = dirSession.query(filter);
296                for (DocumentModel entry : entriesToDelete) {
297                    dirSession.deleteEntry(entry.getId());
298                }
299            } catch (DirectoryException e) {
300                log.error("Unable to clear DocumentList", e);
301                return false;
302            }
303            return true;
304        } finally {
305            releasePersistenceService();
306        }
307    }
308
309}