001/*
002 * (C) Copyright 2006-2010 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thierry Delprat
016 */
017package org.nuxeo.apidoc.documentation;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.UUID;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.nuxeo.apidoc.api.DocumentationItem;
034import org.nuxeo.apidoc.api.NuxeoArtifact;
035import org.nuxeo.apidoc.api.QueryHelper;
036import org.nuxeo.apidoc.search.ArtifactSearcher;
037import org.nuxeo.apidoc.search.ArtifactSearcherImpl;
038import org.nuxeo.apidoc.security.SecurityConstants;
039import org.nuxeo.common.utils.IdUtils;
040import org.nuxeo.ecm.core.api.Blob;
041import org.nuxeo.ecm.core.api.Blobs;
042import org.nuxeo.ecm.core.api.CoreSession;
043import org.nuxeo.ecm.core.api.DocumentModel;
044import org.nuxeo.ecm.core.api.DocumentModelList;
045import org.nuxeo.ecm.core.api.DocumentRef;
046import org.nuxeo.ecm.core.api.IdRef;
047import org.nuxeo.ecm.core.api.NuxeoException;
048import org.nuxeo.ecm.core.api.PathRef;
049import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
050import org.nuxeo.ecm.core.api.security.ACE;
051import org.nuxeo.ecm.core.api.security.ACL;
052import org.nuxeo.ecm.core.api.security.ACP;
053import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
054import org.nuxeo.ecm.core.io.DocumentPipe;
055import org.nuxeo.ecm.core.io.DocumentReader;
056import org.nuxeo.ecm.core.io.DocumentTransformer;
057import org.nuxeo.ecm.core.io.DocumentWriter;
058import org.nuxeo.ecm.core.io.ExportedDocument;
059import org.nuxeo.ecm.core.io.impl.DocumentPipeImpl;
060import org.nuxeo.ecm.core.io.impl.plugins.DocumentModelWriter;
061import org.nuxeo.ecm.core.io.impl.plugins.NuxeoArchiveReader;
062import org.nuxeo.ecm.core.io.impl.plugins.NuxeoArchiveWriter;
063import org.nuxeo.ecm.core.query.sql.NXQL;
064import org.nuxeo.ecm.directory.Session;
065import org.nuxeo.ecm.directory.api.DirectoryService;
066import org.nuxeo.runtime.api.Framework;
067import org.nuxeo.runtime.model.DefaultComponent;
068
069public class DocumentationComponent extends DefaultComponent implements DocumentationService {
070
071    public static final String DIRECTORY_NAME = "documentationTypes";
072
073    public static final String Root_PATH = "/";
074
075    public static final String Root_NAME = "nuxeo-api-doc";
076
077    public static final String Read_Grp = SecurityConstants.Read_Group;
078
079    public static final String Write_Grp = SecurityConstants.Write_Group;
080
081    protected static final Log log = LogFactory.getLog(DocumentationComponent.class);
082
083    protected final ArtifactSearcher searcher = new ArtifactSearcherImpl();
084
085    class UnrestrictedRootCreator extends UnrestrictedSessionRunner {
086
087        protected DocumentRef rootRef;
088
089        public DocumentRef getRootRef() {
090            return rootRef;
091        }
092
093        UnrestrictedRootCreator(CoreSession session) {
094            super(session);
095        }
096
097        @Override
098        public void run() {
099            DocumentModel root = session.createDocumentModel(Root_PATH, Root_NAME, "Folder");
100            root.setProperty("dublincore", "title", Root_NAME);
101            root = session.createDocument(root);
102
103            ACL acl = new ACLImpl();
104            acl.add(new ACE(Write_Grp, "Write", true));
105            acl.add(new ACE(Read_Grp, "Read", true));
106            ACP acp = root.getACP();
107            acp.addACL(acl);
108            session.setACP(root.getRef(), acp, true);
109
110            rootRef = root.getRef();
111            // flush caches
112            session.save();
113        }
114
115    }
116
117    protected DocumentModel getDocumentationRoot(CoreSession session) {
118
119        DocumentRef rootRef = new PathRef(Root_PATH + Root_NAME);
120
121        if (session.exists(rootRef)) {
122            return session.getDocument(rootRef);
123        }
124
125        UnrestrictedRootCreator creator = new UnrestrictedRootCreator(session);
126
127        creator.runUnrestricted();
128
129        // flush caches
130        session.save();
131        return session.getDocument(creator.getRootRef());
132    }
133
134    @Override
135    @SuppressWarnings("unchecked")
136    public <T> T getAdapter(Class<T> adapter) {
137        if (adapter.isAssignableFrom(DocumentationService.class)) {
138            return (T) this;
139        } else if (adapter.isAssignableFrom(ArtifactSearcher.class)) {
140            return (T) searcher;
141        }
142        return null;
143    }
144
145    @Override
146    public Map<String, List<DocumentationItem>> listDocumentationItems(CoreSession session, String category,
147            String targetType) {
148
149        String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE " + QueryHelper.NOT_DELETED;
150
151        if (category != null) {
152            query += " AND " + DocumentationItem.PROP_TYPE + " = " + NXQL.escapeString(category);
153        }
154        if (targetType != null) {
155            query += " AND " + DocumentationItem.PROP_TARGET_TYPE + " = " + NXQL.escapeString(targetType);
156        }
157
158        query += " ORDER BY " + DocumentationItem.PROP_DOCUMENTATION_ID + ", dc:modified";
159        List<DocumentModel> docs = session.query(query);
160
161        Map<String, List<DocumentationItem>> sortMap = new HashMap<String, List<DocumentationItem>>();
162        for (DocumentModel doc : docs) {
163            DocumentationItem item = doc.getAdapter(DocumentationItem.class);
164
165            List<DocumentationItem> alternatives = sortMap.get(item.getId());
166            if (alternatives == null) {
167                alternatives = new ArrayList<DocumentationItem>();
168                alternatives.add(item);
169                sortMap.put(item.getId(), alternatives);
170            } else {
171                alternatives.add(item);
172            }
173        }
174
175        List<DocumentationItem> result = new ArrayList<DocumentationItem>();
176
177        for (String documentationId : sortMap.keySet()) {
178            DocumentationItem bestDoc = sortMap.get(documentationId).get(0);
179            result.add(bestDoc);
180        }
181
182        Map<String, List<DocumentationItem>> sortedResult = new HashMap<String, List<DocumentationItem>>();
183        Map<String, String> categories = getCategories();
184
185        for (DocumentationItem item : result) {
186            String key = item.getType();
187            String label = categories.get(key);
188
189            if (sortedResult.containsKey(label)) {
190                sortedResult.get(label).add(item);
191            } else {
192                List<DocumentationItem> items = new ArrayList<DocumentationItem>();
193                items.add(item);
194                sortedResult.put(label, items);
195            }
196        }
197
198        return sortedResult;
199    }
200
201    @Override
202    public List<DocumentationItem> findDocumentItems(CoreSession session, NuxeoArtifact nxItem) {
203
204        String id = nxItem.getId();
205        String type = nxItem.getArtifactType();
206        String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE " + DocumentationItem.PROP_TARGET
207                + " = " + NXQL.escapeString(id) + " AND " + DocumentationItem.PROP_TARGET_TYPE + " = "
208                + NXQL.escapeString(type) + " AND " + QueryHelper.NOT_DELETED + " ORDER BY "
209                + DocumentationItem.PROP_DOCUMENTATION_ID + ", dc:modified";
210        List<DocumentModel> docs = session.query(query);
211
212        Map<String, List<DocumentationItem>> sortMap = new HashMap<String, List<DocumentationItem>>();
213        for (DocumentModel doc : docs) {
214            DocumentationItem item = doc.getAdapter(DocumentationItem.class);
215
216            List<DocumentationItem> alternatives = sortMap.get(item.getId());
217            if (alternatives == null) {
218                alternatives = new ArrayList<DocumentationItem>();
219                alternatives.add(item);
220                sortMap.put(item.getId(), alternatives);
221            } else {
222                alternatives.add(item);
223            }
224        }
225
226        List<DocumentationItem> result = new ArrayList<DocumentationItem>();
227
228        for (String documentationId : sortMap.keySet()) {
229            DocumentationItem bestDoc = findBestMatch(nxItem, sortMap.get(documentationId));
230            result.add(bestDoc);
231        }
232        return result;
233    }
234
235    protected DocumentationItem findBestMatch(NuxeoArtifact nxItem, List<DocumentationItem> docItems) {
236        for (DocumentationItem docItem : docItems) {
237            // get first possible because already sorted on modification date
238            if (docItem.getApplicableVersion().contains(nxItem.getVersion())) {
239                return docItem;
240            }
241        }
242        // XXX may be find the closest match ?
243        return docItems.get(0);
244    }
245
246    @Override
247    public List<DocumentationItem> findDocumentationItemVariants(CoreSession session, DocumentationItem item)
248            {
249
250        List<DocumentationItem> result = new ArrayList<DocumentationItem>();
251        List<DocumentModel> docs = findDocumentModelVariants(session, item);
252
253        for (DocumentModel doc : docs) {
254            DocumentationItem docItem = doc.getAdapter(DocumentationItem.class);
255            if (docItem != null) {
256                result.add(docItem);
257            }
258        }
259
260        Collections.sort(result);
261        Collections.reverse(result);
262
263        return result;
264    }
265
266    public List<DocumentModel> findDocumentModelVariants(CoreSession session, DocumentationItem item)
267            {
268        String id = item.getId();
269        String type = item.getTargetType();
270        String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE "
271                + DocumentationItem.PROP_DOCUMENTATION_ID + " = " + NXQL.escapeString(id) + " AND "
272                + DocumentationItem.PROP_TARGET_TYPE + " = " + NXQL.escapeString(type) + " AND "
273                + QueryHelper.NOT_DELETED;
274        query += " ORDER BY dc:created";
275        return session.query(query);
276    }
277
278    @Override
279    public DocumentationItem createDocumentationItem(CoreSession session, NuxeoArtifact item, String title,
280            String content, String type, List<String> applicableVersions, boolean approved, String renderingType)
281            {
282
283        DocumentModel doc = session.createDocumentModel(DocumentationItem.TYPE_NAME);
284
285        String name = title + '-' + item.getId();
286        name = IdUtils.generateId(name, "-", true, 64);
287
288        UUID docUUID = UUID.nameUUIDFromBytes(name.getBytes());
289
290        doc.setPathInfo(getDocumentationRoot(session).getPathAsString(), name);
291        doc.setPropertyValue("dc:title", title);
292        Blob blob = Blobs.createBlob(content);
293        blob.setFilename(type);
294        doc.setPropertyValue("file:content", (Serializable) blob);
295        doc.setPropertyValue(DocumentationItem.PROP_TARGET, item.getId());
296        doc.setPropertyValue(DocumentationItem.PROP_TARGET_TYPE, item.getArtifactType());
297        doc.setPropertyValue(DocumentationItem.PROP_DOCUMENTATION_ID, docUUID.toString());
298        doc.setPropertyValue(DocumentationItem.PROP_NUXEO_APPROVED, Boolean.valueOf(approved));
299        doc.setPropertyValue(DocumentationItem.PROP_TYPE, type);
300        doc.setPropertyValue(DocumentationItem.PROP_RENDERING_TYPE, renderingType);
301        doc.setPropertyValue(DocumentationItem.PROP_APPLICABLE_VERSIONS, (Serializable) applicableVersions);
302
303        doc = session.createDocument(doc);
304        session.save();
305
306        return doc.getAdapter(DocumentationItem.class);
307    }
308
309    @Override
310    public void deleteDocumentationItem(CoreSession session, String uuid) {
311        DocumentModel doc = session.getDocument(new IdRef(uuid));
312        // check type
313        if (!doc.getType().equals(DocumentationItem.TYPE_NAME)) {
314            throw new RuntimeException("Invalid documentation item");
315        }
316        // check under our root
317        DocumentModel root = getDocumentationRoot(session);
318        DocumentModel parent = session.getDocument(doc.getParentRef());
319        if (!root.getId().equals(parent.getId())) {
320            throw new RuntimeException("Invalid documentation item");
321        }
322        // ok to delete
323        session.removeDocument(doc.getRef());
324    }
325
326    protected DocumentModel updateDocumentModel(DocumentModel doc, DocumentationItem item) {
327
328        doc.setPropertyValue("dc:title", item.getTitle());
329        Blob content = Blobs.createBlob(item.getContent());
330        content.setFilename(item.getTypeLabel());
331        doc.setPropertyValue("file:content", (Serializable) content);
332        doc.setPropertyValue(DocumentationItem.PROP_DOCUMENTATION_ID, item.getId());
333        doc.setPropertyValue(DocumentationItem.PROP_NUXEO_APPROVED, Boolean.valueOf(item.isApproved()));
334        doc.setPropertyValue(DocumentationItem.PROP_RENDERING_TYPE, item.getRenderingType());
335        doc.setPropertyValue(DocumentationItem.PROP_APPLICABLE_VERSIONS, (Serializable) item.getApplicableVersion());
336
337        List<Map<String, Serializable>> atts = new ArrayList<Map<String, Serializable>>();
338        Map<String, String> attData = item.getAttachments();
339        if (attData != null && attData.size() > 0) {
340            for (String fileName : attData.keySet()) {
341                Map<String, Serializable> fileItem = new HashMap<String, Serializable>();
342                Blob blob = Blobs.createBlob(attData.get(fileName));
343                blob.setFilename(fileName);
344
345                fileItem.put("file", (Serializable) blob);
346                fileItem.put("filename", fileName);
347
348                atts.add(fileItem);
349            }
350            doc.setPropertyValue("files:files", (Serializable) atts);
351        }
352
353        return doc;
354    }
355
356    @Override
357    public DocumentationItem updateDocumentationItem(CoreSession session, DocumentationItem docItem)
358            {
359
360        DocumentModel existingDoc = session.getDocument(new IdRef(docItem.getUUID()));
361        DocumentationItem existingDocItem = existingDoc.getAdapter(DocumentationItem.class);
362
363        List<String> applicableVersions = docItem.getApplicableVersion();
364        List<String> existingApplicableVersions = existingDocItem.getApplicableVersion();
365        List<String> discardedVersion = new ArrayList<String>();
366
367        for (String version : existingApplicableVersions) {
368            if (!applicableVersions.contains(version)) {
369                discardedVersion.add(version);
370            }
371            // XXX check for older versions in case of inconsistent
372            // applicableVersions values ...
373        }
374
375        if (discardedVersion.size() > 0) {
376            // save old version
377            String newName = existingDoc.getName();
378            Collections.sort(discardedVersion);
379            for (String version : discardedVersion) {
380                newName = newName + "_" + version;
381            }
382            newName = IdUtils.generateId(newName, "-", true, 100);
383
384            DocumentModel discardedDoc = session.copy(existingDoc.getRef(), existingDoc.getParentRef(), newName);
385            discardedDoc.setPropertyValue(DocumentationItem.PROP_APPLICABLE_VERSIONS, (Serializable) discardedVersion);
386
387            discardedDoc = session.saveDocument(discardedDoc);
388        }
389
390        existingDoc = updateDocumentModel(existingDoc, docItem);
391        existingDoc = session.saveDocument(existingDoc);
392        session.save();
393        return existingDoc.getAdapter(DocumentationItem.class);
394    }
395
396    protected List<DocumentModel> listCategories() {
397        DirectoryService dm = Framework.getService(DirectoryService.class);
398        try (Session session = dm.open(DIRECTORY_NAME)) {
399            return session.query(Collections.<String, Serializable> emptyMap(), null,
400                    Collections.singletonMap("ordering", "ASC"));
401        }
402    }
403
404    @Override
405    public List<String> getCategoryKeys() {
406        List<String> categories = new ArrayList<String>();
407        for (DocumentModel entry : listCategories()) {
408            categories.add(entry.getId());
409        }
410        return categories;
411    }
412
413    @Override
414    public Map<String, String> getCategories() {
415        Map<String, String> categories = new LinkedHashMap<String, String>();
416        if (!Framework.isTestModeSet()) {
417            for (DocumentModel entry : listCategories()) {
418                String value = (String) entry.getProperty("vocabulary", "label");
419                categories.put(entry.getId(), value);
420            }
421        } else {
422            categories.put("description", "Description");
423            categories.put("codeSample", "Code Sample");
424            categories.put("howTo", "How To");
425        }
426        return categories;
427    }
428
429    @Override
430    public void exportDocumentation(CoreSession session, OutputStream out) {
431        try {
432            String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE " + QueryHelper.NOT_DELETED;
433            DocumentModelList docList = session.query(query);
434            DocumentReader reader = new DocumentModelListReader(docList);
435            DocumentWriter writer = new NuxeoArchiveWriter(out);
436            DocumentPipe pipe = new DocumentPipeImpl(10);
437            pipe.setReader(reader);
438            pipe.setWriter(writer);
439            pipe.run();
440            reader.close();
441            writer.close();
442        } catch (IOException | NuxeoException e) {
443            log.error("Error while exporting documentation", e);
444        }
445    }
446
447    @Override
448    public void importDocumentation(CoreSession session, InputStream is) {
449        try {
450            String importPath = getDocumentationRoot(session).getPathAsString();
451            DocumentReader reader = new NuxeoArchiveReader(is);
452            DocumentWriter writer = new DocumentModelWriter(session, importPath);
453
454            DocumentPipe pipe = new DocumentPipeImpl(10);
455            pipe.setReader(reader);
456            pipe.setWriter(writer);
457            DocumentTransformer rootCutter = new DocumentTransformer() {
458                @Override
459                public boolean transform(ExportedDocument doc) {
460                    doc.setPath(doc.getPath().removeFirstSegments(1));
461                    return true;
462                }
463            };
464            pipe.addTransformer(rootCutter);
465            pipe.run();
466            reader.close();
467            writer.close();
468        } catch (IOException | NuxeoException e) {
469            log.error("Error while importing documentation", e);
470        }
471    }
472
473    @Override
474    public String getDocumentationStats(CoreSession session) {
475        String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE " + QueryHelper.NOT_DELETED;
476        DocumentModelList docList = session.query(query);
477        return docList.size() + " documents";
478    }
479
480    @Override
481    public Map<String, DocumentationItem> getAvailableDescriptions(CoreSession session, String targetType) {
482
483        Map<String, List<DocumentationItem>> itemsByCat = listDocumentationItems(session,
484                DefaultDocumentationType.DESCRIPTION.getValue(), targetType);
485        Map<String, DocumentationItem> result = new HashMap<String, DocumentationItem>();
486
487        if (itemsByCat.size() > 0) {
488            String labelKey = itemsByCat.keySet().iterator().next();
489            List<DocumentationItem> docs = itemsByCat.get(labelKey);
490            for (DocumentationItem doc : docs) {
491                result.put(doc.getTarget(), doc);
492            }
493        }
494
495        return result;
496    }
497
498}