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}