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}