001/* 002 * (C) Copyright 2006-2015 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.browse; 020 021import static org.nuxeo.apidoc.snapshot.DistributionSnapshot.PROP_RELEASED; 022 023import java.io.File; 024import java.io.FileOutputStream; 025import java.io.IOException; 026import java.io.OutputStream; 027import java.io.Serializable; 028import java.sql.Date; 029import java.text.ParseException; 030import java.time.Instant; 031import java.time.LocalDate; 032import java.time.ZoneId; 033import java.util.HashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.Optional; 037import java.util.regex.Matcher; 038import java.util.regex.Pattern; 039import java.util.stream.Collectors; 040 041import javax.naming.NamingException; 042import javax.transaction.HeuristicMixedException; 043import javax.transaction.HeuristicRollbackException; 044import javax.transaction.NotSupportedException; 045import javax.transaction.RollbackException; 046import javax.transaction.SystemException; 047import javax.transaction.UserTransaction; 048import javax.ws.rs.GET; 049import javax.ws.rs.POST; 050import javax.ws.rs.Path; 051import javax.ws.rs.PathParam; 052import javax.ws.rs.Produces; 053import javax.ws.rs.core.Response; 054 055import org.apache.commons.lang3.StringUtils; 056import org.apache.commons.logging.Log; 057import org.apache.commons.logging.LogFactory; 058import org.nuxeo.apidoc.documentation.DocumentationService; 059import org.nuxeo.apidoc.export.ArchiveFile; 060import org.nuxeo.apidoc.listener.AttributesExtractorStater; 061import org.nuxeo.apidoc.snapshot.DistributionSnapshot; 062import org.nuxeo.apidoc.snapshot.DistributionSnapshotDesc; 063import org.nuxeo.apidoc.snapshot.SnapshotFilter; 064import org.nuxeo.apidoc.snapshot.SnapshotManager; 065import org.nuxeo.apidoc.snapshot.SnapshotManagerComponent; 066import org.nuxeo.apidoc.snapshot.SnapshotResolverHelper; 067import org.nuxeo.apidoc.worker.ExtractXmlAttributesWorker; 068import org.nuxeo.common.Environment; 069import org.nuxeo.ecm.core.api.Blob; 070import org.nuxeo.ecm.core.api.CoreSession; 071import org.nuxeo.ecm.core.api.DocumentModel; 072import org.nuxeo.ecm.core.api.IterableQueryResult; 073import org.nuxeo.ecm.core.api.NuxeoException; 074import org.nuxeo.ecm.core.api.NuxeoPrincipal; 075import org.nuxeo.ecm.core.query.QueryFilter; 076import org.nuxeo.ecm.core.query.sql.NXQL; 077import org.nuxeo.ecm.core.work.api.Work; 078import org.nuxeo.ecm.core.work.api.WorkManager; 079import org.nuxeo.ecm.webengine.forms.FormData; 080import org.nuxeo.ecm.webengine.model.Resource; 081import org.nuxeo.ecm.webengine.model.WebObject; 082import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException; 083import org.nuxeo.ecm.webengine.model.impl.ModuleRoot; 084import org.nuxeo.runtime.api.Framework; 085import org.nuxeo.runtime.transaction.TransactionHelper; 086 087@Path("/distribution") 088// needed for 5.4.1 089@WebObject(type = "distribution") 090public class Distribution extends ModuleRoot { 091 092 public static final String DIST_ID = "distId"; 093 094 protected static final Log log = LogFactory.getLog(Distribution.class); 095 096 protected static final Pattern VERSION_REGEX = Pattern.compile("^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:-.*)?$", 097 Pattern.CASE_INSENSITIVE); 098 099 // handle errors 100 @Override 101 public Object handleError(Throwable t) { 102 if (t instanceof WebResourceNotFoundException) { 103 return Response.status(404).entity(getTemplate("error/error_404.ftl")).type("text/html").build(); 104 } else { 105 return super.handleError(t); 106 } 107 } 108 109 protected SnapshotManager getSnapshotManager() { 110 return Framework.getService(SnapshotManager.class); 111 } 112 113 public String getNavigationPoint() { 114 String currentUrl = getContext().getURL(); 115 String navPoint = "somewhere"; 116 117 if (currentUrl.contains("/listBundles")) { 118 navPoint = "listBundles"; 119 } else if (currentUrl.contains("/listSeamComponents")) { 120 navPoint = "listSeamComponents"; 121 } else if (currentUrl.contains("/viewSeamComponent")) { 122 navPoint = "viewSeamComponent"; 123 } else if (currentUrl.contains("/listComponents")) { 124 navPoint = "listComponents"; 125 } else if (currentUrl.contains("/listServices")) { 126 navPoint = "listServices"; 127 } else if (currentUrl.contains("/listExtensionPoints")) { 128 navPoint = "listExtensionPoints"; 129 } else if (currentUrl.contains("/listContributions")) { 130 navPoint = "listContributions"; 131 } else if (currentUrl.contains("/listBundleGroups")) { 132 navPoint = "listBundleGroups"; 133 } else if (currentUrl.contains("/viewBundleGroup")) { 134 navPoint = "viewBundleGroup"; 135 } else if (currentUrl.contains("/viewComponent")) { 136 navPoint = "viewComponent"; 137 } else if (currentUrl.contains("/viewService")) { 138 navPoint = "viewService"; 139 } else if (currentUrl.contains("/viewExtensionPoint")) { 140 navPoint = "viewExtensionPoint"; 141 } else if (currentUrl.contains("/viewContribution")) { 142 navPoint = "viewContribution"; 143 } else if (currentUrl.contains("/viewBundle")) { 144 navPoint = "viewBundle"; 145 } else if (currentUrl.contains("/listOperations")) { 146 navPoint = "listOperations"; 147 } else if (currentUrl.contains("/viewOperation")) { 148 navPoint = "viewOperation"; 149 } else if (currentUrl.contains("/doc")) { 150 navPoint = "documentation"; 151 } 152 return navPoint; 153 } 154 155 @GET 156 @Produces("text/html") 157 public Object doGet() { 158 return getView("index").arg("hideNav", Boolean.TRUE); 159 } 160 161 @Path("latest") 162 public Resource getLatest() { 163 List<DistributionSnapshot> snaps = listPersistedDistributions(); 164 Optional<DistributionSnapshot> distribution = snaps.stream() 165 .filter(snap -> snap.getName() 166 .toLowerCase() 167 .startsWith("nuxeo platform")) 168 .findFirst(); 169 170 String latest = "current"; 171 if (distribution.isPresent()) { 172 latest = distribution.get().getKey(); 173 } 174 return ctx.newObject("redirectWO", "latest", latest); 175 } 176 177 @Path("{distributionId}") 178 public Resource viewDistribution(@PathParam("distributionId") String distributionId) { 179 if (distributionId == null || "".equals(distributionId)) { 180 return this; 181 } 182 183 List<DistributionSnapshot> snaps = getSnapshotManager().listPersistentSnapshots((ctx.getCoreSession())); 184 if (distributionId.matches(VERSION_REGEX.toString())) { 185 String finalDistributionId = distributionId; 186 String distribution = snaps.stream() 187 .filter(s -> s.getVersion().equals(finalDistributionId)) 188 .findFirst() 189 .map(DistributionSnapshot::getKey) 190 .orElse("current"); 191 192 return ctx.newObject("redirectWO", finalDistributionId, distribution); 193 } 194 195 String orgDistributionId = distributionId; 196 Boolean embeddedMode = Boolean.FALSE; 197 if ("adm".equals(distributionId)) { 198 embeddedMode = Boolean.TRUE; 199 } else { 200 201 snaps.add(getSnapshotManager().getRuntimeSnapshot()); 202 distributionId = SnapshotResolverHelper.findBestMatch(snaps, distributionId); 203 } 204 if (distributionId == null || "".equals(distributionId)) { 205 distributionId = "current"; 206 } 207 208 if (!orgDistributionId.equals(distributionId)) { 209 return ctx.newObject("redirectWO", orgDistributionId, distributionId); 210 } 211 212 ctx.setProperty("embeddedMode", embeddedMode); 213 ctx.setProperty("distribution", getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession())); 214 ctx.setProperty(DIST_ID, distributionId); 215 return ctx.newObject("apibrowser", distributionId, embeddedMode); 216 } 217 218 public List<DistributionSnapshotDesc> getAvailableDistributions() { 219 return getSnapshotManager().getAvailableDistributions(ctx.getCoreSession()); 220 } 221 222 public String getRuntimeDistributionName() { 223 return SnapshotManagerComponent.RUNTIME; 224 } 225 226 public DistributionSnapshot getRuntimeDistribution() { 227 return getSnapshotManager().getRuntimeSnapshot(); 228 } 229 230 public List<DistributionSnapshot> listPersistedDistributions() { 231 SnapshotManager sm = getSnapshotManager(); 232 return sm.listPersistentSnapshots(ctx.getCoreSession()) 233 .stream() 234 .sorted((o1, o2) -> { 235 Matcher m1 = VERSION_REGEX.matcher(o1.getVersion()); 236 Matcher m2 = VERSION_REGEX.matcher(o2.getVersion()); 237 238 if (m1.matches() && m2.matches()) { 239 for (int i = 0; i < 3; i++) { 240 String s1 = m1.group(i + 1); 241 int c1 = s1 != null ? Integer.parseInt(s1) : 0; 242 String s2 = m2.group(i + 1); 243 int c2 = s2 != null ? Integer.parseInt(s2) : 0; 244 245 if (c1 != c2 || i == 2) { 246 return Integer.compare(c2, c1); 247 } 248 } 249 } 250 log.info(String.format("Comparing version using String between %s - %s", o1.getVersion(), 251 o2.getVersion())); 252 return o2.getVersion().compareTo(o1.getVersion()); 253 }) 254 .filter(s -> !s.isHidden()) 255 .collect(Collectors.toList()); 256 } 257 258 public Map<String, DistributionSnapshot> getPersistedDistributions() { 259 return getSnapshotManager().getPersistentSnapshots(ctx.getCoreSession()); 260 } 261 262 public DistributionSnapshot getCurrentDistribution() { 263 String distId = (String) ctx.getProperty(DIST_ID); 264 DistributionSnapshot currentDistribution = (DistributionSnapshot) ctx.getProperty("currentDistribution"); 265 if (currentDistribution == null || !currentDistribution.getKey().equals(distId)) { 266 currentDistribution = getSnapshotManager().getSnapshot(distId, ctx.getCoreSession()); 267 ctx.setProperty("currentDistribution", currentDistribution); 268 } 269 return currentDistribution; 270 } 271 272 @POST 273 @Path("save") 274 @Produces("text/html") 275 public Object doSave() throws NamingException, NotSupportedException, SystemException, RollbackException, 276 HeuristicMixedException, HeuristicRollbackException, ParseException { 277 if (!canAddDocumentation()) { 278 return null; 279 } 280 FormData formData = getContext().getForm(); 281 String distribLabel = formData.getString("name"); 282 283 log.info("Start Snapshot..."); 284 boolean startedTx = false; 285 UserTransaction tx = TransactionHelper.lookupUserTransaction(); 286 if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) { 287 tx.begin(); 288 startedTx = true; 289 } 290 291 Map<String, Serializable> otherProperties = readFormData(formData); 292 try { 293 getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties); 294 295 } catch (NuxeoException e) { 296 log.error("Error during storage", e); 297 if (tx != null) { 298 tx.rollback(); 299 } 300 return getView("savedKO").arg("message", e.getMessage()); 301 } 302 log.info("Snapshot saved."); 303 if (tx != null && startedTx) { 304 tx.commit(); 305 } 306 307 String redirectUrl = getContext().getBaseURL() + getPath(); 308 log.debug("Path => " + redirectUrl); 309 return getView("saved"); 310 } 311 312 protected Map<String, Serializable> readFormData(FormData formData) { 313 Map<String, Serializable> properties = new HashMap<>(); 314 315 // Release date 316 String released = formData.getString("released"); 317 if (StringUtils.isNotBlank(released)) { 318 LocalDate date = LocalDate.parse(released); 319 Instant instant = date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(); 320 properties.put(PROP_RELEASED, Date.from(instant)); 321 } 322 323 return properties; 324 } 325 326 @POST 327 @Path("saveExtended") 328 @Produces("text/html") 329 public Object doSaveExtended() throws NamingException, NotSupportedException, SystemException, SecurityException, 330 RollbackException, HeuristicMixedException, HeuristicRollbackException { 331 if (!canAddDocumentation()) { 332 return null; 333 } 334 335 FormData formData = getContext().getForm(); 336 337 String distribLabel = formData.getString("name"); 338 String bundleList = formData.getString("bundles"); 339 String pkgList = formData.getString("packages"); 340 SnapshotFilter filter = new SnapshotFilter(distribLabel); 341 342 if (bundleList != null) { 343 String[] bundles = bundleList.split("\n"); 344 for (String bundleId : bundles) { 345 filter.addBundlePrefix(bundleId); 346 } 347 } 348 349 if (pkgList != null) { 350 String[] packages = pkgList.split("\\r?\\n"); 351 for (String pkg : packages) { 352 filter.addPackagesPrefix(pkg); 353 } 354 } 355 356 Map<String, Serializable> otherProperties = readFormData(formData); 357 358 log.info("Start Snapshot..."); 359 boolean startedTx = false; 360 UserTransaction tx = TransactionHelper.lookupUserTransaction(); 361 if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) { 362 tx.begin(); 363 startedTx = true; 364 } 365 try { 366 getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties, 367 filter); 368 } catch (NuxeoException e) { 369 log.error("Error during storage", e); 370 if (tx != null) { 371 tx.rollback(); 372 } 373 return getView("savedKO").arg("message", e.getMessage()); 374 } 375 log.info("Snapshot saved."); 376 if (tx != null && startedTx) { 377 tx.commit(); 378 } 379 return getView("saved"); 380 } 381 382 public String getDocumentationInfo() { 383 DocumentationService ds = Framework.getService(DocumentationService.class); 384 return ds.getDocumentationStats(getContext().getCoreSession()); 385 } 386 387 protected File getExportTmpFile() { 388 File tmpFile = new File(Environment.getDefault().getTemp(), "export.zip"); 389 if (tmpFile.exists()) { 390 tmpFile.delete(); 391 } 392 tmpFile.deleteOnExit(); 393 return tmpFile; 394 } 395 396 @GET 397 @Path("downloadDoc") 398 public Response downloadDoc() throws IOException { 399 DocumentationService ds = Framework.getService(DocumentationService.class); 400 File tmp = getExportTmpFile(); 401 tmp.createNewFile(); 402 OutputStream out = new FileOutputStream(tmp); 403 ds.exportDocumentation(getContext().getCoreSession(), out); 404 out.flush(); 405 out.close(); 406 ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath()); 407 return Response.ok(aFile) 408 .header("Content-Disposition", "attachment;filename=" + "nuxeo-documentation.zip") 409 .type("application/zip") 410 .build(); 411 } 412 413 @GET 414 @Path("download/{distributionId}") 415 public Response downloadDistrib(@PathParam("distributionId") String distribId) throws IOException { 416 File tmp = getExportTmpFile(); 417 tmp.createNewFile(); 418 OutputStream out = new FileOutputStream(tmp); 419 getSnapshotManager().exportSnapshot(getContext().getCoreSession(), distribId, out); 420 out.close(); 421 String fName = "nuxeo-distribution-" + distribId + ".zip"; 422 fName = fName.replace(" ", "_"); 423 ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath()); 424 return Response.ok(aFile) 425 .header("Content-Disposition", "attachment;filename=" + fName) 426 .type("application/zip") 427 .build(); 428 } 429 430 /** 431 * Use to allow authorized users to upload distribution even in site mode 432 * 433 * @since 8.3 434 */ 435 @GET 436 @Path("_admin") 437 public Object getForms() { 438 NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal(); 439 if (SecurityHelper.canEditDocumentation(principal)) { 440 return getView("forms").arg("hideNav", Boolean.TRUE); 441 } else { 442 return Response.status(401).build(); 443 } 444 } 445 446 @POST 447 @Path("uploadDistrib") 448 @Produces("text/html") 449 public Object uploadDistrib() throws IOException { 450 if (!canAddDocumentation()) { 451 return null; 452 } 453 Blob blob = getContext().getForm().getFirstBlob(); 454 455 getSnapshotManager().importSnapshot(getContext().getCoreSession(), blob.getStream()); 456 getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession()); 457 458 return getView("index"); 459 } 460 461 @POST 462 @Path("uploadDistribTmp") 463 @Produces("text/html") 464 public Object uploadDistribTmp() throws IOException { 465 if (!canAddDocumentation()) { 466 return null; 467 } 468 Blob blob = getContext().getForm().getFirstBlob(); 469 if (blob == null || blob.getLength() == 0) { 470 return null; 471 } 472 DocumentModel snap = getSnapshotManager().importTmpSnapshot(getContext().getCoreSession(), blob.getStream()); 473 if (snap == null) { 474 log.error("Unable to import archive"); 475 return null; 476 } 477 DistributionSnapshot snapObject = snap.getAdapter(DistributionSnapshot.class); 478 return getView("uploadEdit").arg("tmpSnap", snap).arg("snapObject", snapObject); 479 } 480 481 @POST 482 @Path("uploadDistribTmpValid") 483 @Produces("text/html") 484 public Object uploadDistribTmpValid() { 485 if (!canAddDocumentation()) { 486 return null; 487 } 488 489 FormData formData = getContext().getForm(); 490 String name = formData.getString("name"); 491 String version = formData.getString("version"); 492 String pathSegment = formData.getString("pathSegment"); 493 String title = formData.getString("title"); 494 495 getSnapshotManager().validateImportedSnapshot(getContext().getCoreSession(), name, version, pathSegment, title); 496 getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession()); 497 return getView("importDone"); 498 } 499 500 @POST 501 @Path("uploadDoc") 502 @Produces("text/html") 503 public Object uploadDoc() throws IOException { 504 if (!canAddDocumentation()) { 505 return null; 506 } 507 508 Blob blob = getContext().getForm().getFirstBlob(); 509 if (blob == null || blob.getLength() == 0) { 510 return null; 511 } 512 513 DocumentationService ds = Framework.getService(DocumentationService.class); 514 ds.importDocumentation(getContext().getCoreSession(), blob.getStream()); 515 516 log.info("Documents imported."); 517 518 return getView("docImportDone"); 519 } 520 521 @GET 522 @Path("_reindex") 523 @Produces("text/plain") 524 public Object reindex() { 525 NuxeoPrincipal nxPrincipal = (NuxeoPrincipal) getContext().getPrincipal(); 526 if (!nxPrincipal.isAdministrator()) { 527 return Response.status(404).build(); 528 } 529 530 CoreSession coreSession = getContext().getCoreSession(); 531 String query = String.format( 532 "SELECT ecm:uuid FROM Document WHERE ecm:primaryType in ('%s') AND ecm:isProxy = 0 AND ecm:currentLifeCycleState <> 'deleted'", 533 StringUtils.join(AttributesExtractorStater.DOC_TYPES, "','")); 534 535 try (IterableQueryResult it = coreSession.queryAndFetch(query, NXQL.NXQL, QueryFilter.EMPTY);) { 536 for (Map<String, Serializable> map : it) { 537 String id = (String) map.get(NXQL.ECM_UUID); 538 Work work = new ExtractXmlAttributesWorker(coreSession.getRepositoryName(), nxPrincipal.getName(), id); 539 Framework.getService(WorkManager.class).schedule(work); 540 } 541 } 542 543 return Response.ok().build(); 544 } 545 546 public boolean isEmbeddedMode() { 547 Boolean embed = (Boolean) getContext().getProperty("embeddedMode", Boolean.FALSE); 548 return embed != null && embed; 549 } 550 551 public boolean isEditor() { 552 if (isEmbeddedMode() || isSiteMode()) { 553 return false; 554 } 555 NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal(); 556 return SecurityHelper.canEditDocumentation(principal); 557 } 558 559 public boolean canAddDocumentation() { 560 NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal(); 561 return !isEmbeddedMode() && SecurityHelper.canEditDocumentation(principal); 562 } 563 564 public static boolean showCurrentDistribution() { 565 return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.current.distribution") || isSiteMode()); 566 } 567 568 public static boolean showSeamComponent() { 569 return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.seam.components") || isSiteMode()); 570 } 571 572 public static boolean isSiteMode() { 573 return Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.site.mode"); 574 } 575}