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