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