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}