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<String, List<DocumentationItem>>();
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<DocumentationItem>();
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<DocumentationItem>();
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<String, List<DocumentationItem>>();
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<DocumentationItem>();
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<String, List<DocumentationItem>>();
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<DocumentationItem>();
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<DocumentationItem>();
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
252        List<DocumentationItem> result = new ArrayList<DocumentationItem>();
253        List<DocumentModel> docs = findDocumentModelVariants(session, item);
254
255        for (DocumentModel doc : docs) {
256            DocumentationItem docItem = doc.getAdapter(DocumentationItem.class);
257            if (docItem != null) {
258                result.add(docItem);
259            }
260        }
261
262        Collections.sort(result);
263        Collections.reverse(result);
264
265        return result;
266    }
267
268    public List<DocumentModel> findDocumentModelVariants(CoreSession session, DocumentationItem item)
269            {
270        String id = item.getId();
271        String type = item.getTargetType();
272        String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE "
273                + DocumentationItem.PROP_DOCUMENTATION_ID + " = " + NXQL.escapeString(id) + " AND "
274                + DocumentationItem.PROP_TARGET_TYPE + " = " + NXQL.escapeString(type) + " AND "
275                + QueryHelper.NOT_DELETED;
276        query += " ORDER BY dc:created";
277        return session.query(query);
278    }
279
280    @Override
281    public DocumentationItem createDocumentationItem(CoreSession session, NuxeoArtifact item, String title,
282            String content, String type, List<String> applicableVersions, boolean approved, String renderingType)
283            {
284
285        DocumentModel doc = session.createDocumentModel(DocumentationItem.TYPE_NAME);
286
287        String name = title + '-' + item.getId();
288        name = IdUtils.generateId(name, "-", true, 64);
289
290        UUID docUUID = UUID.nameUUIDFromBytes(name.getBytes());
291
292        doc.setPathInfo(getDocumentationRoot(session).getPathAsString(), name);
293        doc.setPropertyValue("dc:title", title);
294        Blob blob = Blobs.createBlob(content);
295        blob.setFilename(type);
296        doc.setPropertyValue("file:content", (Serializable) blob);
297        doc.setPropertyValue(DocumentationItem.PROP_TARGET, item.getId());
298        doc.setPropertyValue(DocumentationItem.PROP_TARGET_TYPE, item.getArtifactType());
299        doc.setPropertyValue(DocumentationItem.PROP_DOCUMENTATION_ID, docUUID.toString());
300        doc.setPropertyValue(DocumentationItem.PROP_NUXEO_APPROVED, Boolean.valueOf(approved));
301        doc.setPropertyValue(DocumentationItem.PROP_TYPE, type);
302        doc.setPropertyValue(DocumentationItem.PROP_RENDERING_TYPE, renderingType);
303        doc.setPropertyValue(DocumentationItem.PROP_APPLICABLE_VERSIONS, (Serializable) applicableVersions);
304
305        doc = session.createDocument(doc);
306        session.save();
307
308        return doc.getAdapter(DocumentationItem.class);
309    }
310
311    @Override
312    public void deleteDocumentationItem(CoreSession session, String uuid) {
313        DocumentModel doc = session.getDocument(new IdRef(uuid));
314        // check type
315        if (!doc.getType().equals(DocumentationItem.TYPE_NAME)) {
316            throw new RuntimeException("Invalid documentation item");
317        }
318        // check under our root
319        DocumentModel root = getDocumentationRoot(session);
320        DocumentModel parent = session.getDocument(doc.getParentRef());
321        if (!root.getId().equals(parent.getId())) {
322            throw new RuntimeException("Invalid documentation item");
323        }
324        // ok to delete
325        session.removeDocument(doc.getRef());
326    }
327
328    protected DocumentModel updateDocumentModel(DocumentModel doc, DocumentationItem item) {
329
330        doc.setPropertyValue("dc:title", item.getTitle());
331        Blob content = Blobs.createBlob(item.getContent());
332        content.setFilename(item.getTypeLabel());
333        doc.setPropertyValue("file:content", (Serializable) content);
334        doc.setPropertyValue(DocumentationItem.PROP_DOCUMENTATION_ID, item.getId());
335        doc.setPropertyValue(DocumentationItem.PROP_NUXEO_APPROVED, Boolean.valueOf(item.isApproved()));
336        doc.setPropertyValue(DocumentationItem.PROP_RENDERING_TYPE, item.getRenderingType());
337        doc.setPropertyValue(DocumentationItem.PROP_APPLICABLE_VERSIONS, (Serializable) item.getApplicableVersion());
338
339        List<Map<String, Serializable>> atts = new ArrayList<Map<String, Serializable>>();
340        Map<String, String> attData = item.getAttachments();
341        if (attData != null && attData.size() > 0) {
342            for (String fileName : attData.keySet()) {
343                Map<String, Serializable> fileItem = new HashMap<String, Serializable>();
344                Blob blob = Blobs.createBlob(attData.get(fileName));
345                blob.setFilename(fileName);
346
347                fileItem.put("file", (Serializable) blob);
348                fileItem.put("filename", fileName);
349
350                atts.add(fileItem);
351            }
352            doc.setPropertyValue("files:files", (Serializable) atts);
353        }
354
355        return doc;
356    }
357
358    @Override
359    public DocumentationItem updateDocumentationItem(CoreSession session, DocumentationItem docItem)
360            {
361
362        DocumentModel existingDoc = session.getDocument(new IdRef(docItem.getUUID()));
363        DocumentationItem existingDocItem = existingDoc.getAdapter(DocumentationItem.class);
364
365        List<String> applicableVersions = docItem.getApplicableVersion();
366        List<String> existingApplicableVersions = existingDocItem.getApplicableVersion();
367        List<String> discardedVersion = new ArrayList<String>();
368
369        for (String version : existingApplicableVersions) {
370            if (!applicableVersions.contains(version)) {
371                discardedVersion.add(version);
372            }
373            // XXX check for older versions in case of inconsistent
374            // applicableVersions values ...
375        }
376
377        if (discardedVersion.size() > 0) {
378            // save old version
379            String newName = existingDoc.getName();
380            Collections.sort(discardedVersion);
381            for (String version : discardedVersion) {
382                newName = newName + "_" + version;
383            }
384            newName = IdUtils.generateId(newName, "-", true, 100);
385
386            DocumentModel discardedDoc = session.copy(existingDoc.getRef(), existingDoc.getParentRef(), newName);
387            discardedDoc.setPropertyValue(DocumentationItem.PROP_APPLICABLE_VERSIONS, (Serializable) discardedVersion);
388
389            discardedDoc = session.saveDocument(discardedDoc);
390        }
391
392        existingDoc = updateDocumentModel(existingDoc, docItem);
393        existingDoc = session.saveDocument(existingDoc);
394        session.save();
395        return existingDoc.getAdapter(DocumentationItem.class);
396    }
397
398    protected List<DocumentModel> listCategories() {
399        DirectoryService dm = Framework.getService(DirectoryService.class);
400        try (Session session = dm.open(DIRECTORY_NAME)) {
401            return session.query(Collections.<String, Serializable> emptyMap(), null,
402                    Collections.singletonMap("ordering", "ASC"));
403        }
404    }
405
406    @Override
407    public List<String> getCategoryKeys() {
408        List<String> categories = new ArrayList<String>();
409        for (DocumentModel entry : listCategories()) {
410            categories.add(entry.getId());
411        }
412        return categories;
413    }
414
415    @Override
416    public Map<String, String> getCategories() {
417        Map<String, String> categories = new LinkedHashMap<String, String>();
418        if (!Framework.isTestModeSet()) {
419            for (DocumentModel entry : listCategories()) {
420                String value = (String) entry.getProperty("vocabulary", "label");
421                categories.put(entry.getId(), value);
422            }
423        } else {
424            categories.put("description", "Description");
425            categories.put("codeSample", "Code Sample");
426            categories.put("howTo", "How To");
427        }
428        return categories;
429    }
430
431    @Override
432    public void exportDocumentation(CoreSession session, OutputStream out) {
433        try {
434            String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE " + QueryHelper.NOT_DELETED;
435            DocumentModelList docList = session.query(query);
436            DocumentReader reader = new DocumentModelListReader(docList);
437            DocumentWriter writer = new NuxeoArchiveWriter(out);
438            DocumentPipe pipe = new DocumentPipeImpl(10);
439            pipe.setReader(reader);
440            pipe.setWriter(writer);
441            pipe.run();
442            reader.close();
443            writer.close();
444        } catch (IOException | NuxeoException e) {
445            log.error("Error while exporting documentation", e);
446        }
447    }
448
449    @Override
450    public void importDocumentation(CoreSession session, InputStream is) {
451        try {
452            String importPath = getDocumentationRoot(session).getPathAsString();
453            DocumentReader reader = new NuxeoArchiveReader(is);
454            DocumentWriter writer = new DocumentModelWriter(session, importPath);
455
456            DocumentPipe pipe = new DocumentPipeImpl(10);
457            pipe.setReader(reader);
458            pipe.setWriter(writer);
459            DocumentTransformer rootCutter = new DocumentTransformer() {
460                @Override
461                public boolean transform(ExportedDocument doc) {
462                    doc.setPath(doc.getPath().removeFirstSegments(1));
463                    return true;
464                }
465            };
466            pipe.addTransformer(rootCutter);
467            pipe.run();
468            reader.close();
469            writer.close();
470        } catch (IOException | NuxeoException e) {
471            log.error("Error while importing documentation", e);
472        }
473    }
474
475    @Override
476    public String getDocumentationStats(CoreSession session) {
477        String query = "SELECT * FROM " + DocumentationItem.TYPE_NAME + " WHERE " + QueryHelper.NOT_DELETED;
478        DocumentModelList docList = session.query(query);
479        return docList.size() + " documents";
480    }
481
482    @Override
483    public Map<String, DocumentationItem> getAvailableDescriptions(CoreSession session, String targetType) {
484
485        Map<String, List<DocumentationItem>> itemsByCat = listDocumentationItems(session,
486                DefaultDocumentationType.DESCRIPTION.getValue(), targetType);
487        Map<String, DocumentationItem> result = new HashMap<String, DocumentationItem>();
488
489        if (itemsByCat.size() > 0) {
490            String labelKey = itemsByCat.keySet().iterator().next();
491            List<DocumentationItem> docs = itemsByCat.get(labelKey);
492            for (DocumentationItem doc : docs) {
493                result.put(doc.getTarget(), doc);
494            }
495        }
496
497        return result;
498    }
499
500}