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 String orgDistributionId = distributionId; 184 Boolean embeddedMode = Boolean.FALSE; 185 if ("adm".equals(distributionId)) { 186 embeddedMode = Boolean.TRUE; 187 } else { 188 List<DistributionSnapshot> snaps = getSnapshotManager().listPersistentSnapshots((ctx.getCoreSession())); 189 snaps.add(getSnapshotManager().getRuntimeSnapshot()); 190 distributionId = SnapshotResolverHelper.findBestMatch(snaps, distributionId); 191 } 192 if (distributionId == null || "".equals(distributionId)) { 193 distributionId = "current"; 194 } 195 196 if (!orgDistributionId.equals(distributionId)) { 197 return ctx.newObject("redirectWO", orgDistributionId, distributionId); 198 } 199 200 ctx.setProperty("embeddedMode", embeddedMode); 201 ctx.setProperty("distribution", getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession())); 202 ctx.setProperty(DIST_ID, distributionId); 203 return ctx.newObject("apibrowser", distributionId, embeddedMode); 204 } 205 206 public List<DistributionSnapshotDesc> getAvailableDistributions() { 207 return getSnapshotManager().getAvailableDistributions(ctx.getCoreSession()); 208 } 209 210 public String getRuntimeDistributionName() { 211 return SnapshotManagerComponent.RUNTIME; 212 } 213 214 public DistributionSnapshot getRuntimeDistribution() { 215 return getSnapshotManager().getRuntimeSnapshot(); 216 } 217 218 public List<DistributionSnapshot> listPersistedDistributions() { 219 SnapshotManager sm = getSnapshotManager(); 220 return sm.listPersistentSnapshots(ctx.getCoreSession()) 221 .stream() 222 .sorted((o1, o2) -> { 223 Matcher m1 = VERSION_REGEX.matcher(o1.getVersion()); 224 Matcher m2 = VERSION_REGEX.matcher(o2.getVersion()); 225 226 if (m1.matches() && m2.matches()) { 227 for (int i = 0; i < 3; i++) { 228 String s1 = m1.group(i + 1); 229 int c1 = s1 != null ? Integer.parseInt(s1) : 0; 230 String s2 = m2.group(i + 1); 231 int c2 = s2 != null ? Integer.parseInt(s2) : 0; 232 233 if (c1 != c2 || i == 2) { 234 return Integer.compare(c2, c1); 235 } 236 } 237 } 238 log.info(String.format("Comparing version using String between %s - %s", o1.getVersion(), 239 o2.getVersion())); 240 return o2.getVersion().compareTo(o1.getVersion()); 241 }) 242 .filter(s -> !s.isHidden()) 243 .collect(Collectors.toList()); 244 } 245 246 public Map<String, DistributionSnapshot> getPersistedDistributions() { 247 return getSnapshotManager().getPersistentSnapshots(ctx.getCoreSession()); 248 } 249 250 public DistributionSnapshot getCurrentDistribution() { 251 String distId = (String) ctx.getProperty(DIST_ID); 252 DistributionSnapshot currentDistribution = (DistributionSnapshot) ctx.getProperty("currentDistribution"); 253 if (currentDistribution == null || !currentDistribution.getKey().equals(distId)) { 254 currentDistribution = getSnapshotManager().getSnapshot(distId, ctx.getCoreSession()); 255 ctx.setProperty("currentDistribution", currentDistribution); 256 } 257 return currentDistribution; 258 } 259 260 @POST 261 @Path("save") 262 @Produces("text/html") 263 public Object doSave() throws NamingException, NotSupportedException, SystemException, RollbackException, 264 HeuristicMixedException, HeuristicRollbackException, ParseException { 265 if (!canAddDocumentation()) { 266 return null; 267 } 268 FormData formData = getContext().getForm(); 269 String distribLabel = formData.getString("name"); 270 271 log.info("Start Snapshot..."); 272 boolean startedTx = false; 273 UserTransaction tx = TransactionHelper.lookupUserTransaction(); 274 if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) { 275 tx.begin(); 276 startedTx = true; 277 } 278 279 Map<String, Serializable> otherProperties = readFormData(formData); 280 try { 281 getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties); 282 283 } catch (NuxeoException e) { 284 log.error("Error during storage", e); 285 if (tx != null) { 286 tx.rollback(); 287 } 288 return getView("savedKO").arg("message", e.getMessage()); 289 } 290 log.info("Snapshot saved."); 291 if (tx != null && startedTx) { 292 tx.commit(); 293 } 294 295 String redirectUrl = getContext().getBaseURL() + getPath(); 296 log.debug("Path => " + redirectUrl); 297 return getView("saved"); 298 } 299 300 protected Map<String, Serializable> readFormData(FormData formData) { 301 Map<String, Serializable> properties = new HashMap<>(); 302 303 // Release date 304 String released = formData.getString("released"); 305 if (StringUtils.isNotBlank(released)) { 306 LocalDate date = LocalDate.parse(released); 307 Instant instant = date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(); 308 properties.put(PROP_RELEASED, Date.from(instant)); 309 } 310 311 return properties; 312 } 313 314 @POST 315 @Path("saveExtended") 316 @Produces("text/html") 317 public Object doSaveExtended() throws NamingException, NotSupportedException, SystemException, SecurityException, 318 RollbackException, HeuristicMixedException, HeuristicRollbackException { 319 if (!canAddDocumentation()) { 320 return null; 321 } 322 323 FormData formData = getContext().getForm(); 324 325 String distribLabel = formData.getString("name"); 326 String bundleList = formData.getString("bundles"); 327 String pkgList = formData.getString("packages"); 328 SnapshotFilter filter = new SnapshotFilter(distribLabel); 329 330 if (bundleList != null) { 331 String[] bundles = bundleList.split("\n"); 332 for (String bundleId : bundles) { 333 filter.addBundlePrefix(bundleId); 334 } 335 } 336 337 if (pkgList != null) { 338 String[] packages = pkgList.split("\\r?\\n"); 339 for (String pkg : packages) { 340 filter.addPackagesPrefix(pkg); 341 } 342 } 343 344 Map<String, Serializable> otherProperties = readFormData(formData); 345 346 log.info("Start Snapshot..."); 347 boolean startedTx = false; 348 UserTransaction tx = TransactionHelper.lookupUserTransaction(); 349 if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) { 350 tx.begin(); 351 startedTx = true; 352 } 353 try { 354 getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties, 355 filter); 356 } catch (NuxeoException e) { 357 log.error("Error during storage", e); 358 if (tx != null) { 359 tx.rollback(); 360 } 361 return getView("savedKO").arg("message", e.getMessage()); 362 } 363 log.info("Snapshot saved."); 364 if (tx != null && startedTx) { 365 tx.commit(); 366 } 367 return getView("saved"); 368 } 369 370 public String getDocumentationInfo() { 371 DocumentationService ds = Framework.getService(DocumentationService.class); 372 return ds.getDocumentationStats(getContext().getCoreSession()); 373 } 374 375 protected File getExportTmpFile() { 376 File tmpFile = new File(Environment.getDefault().getTemp(), "export.zip"); 377 if (tmpFile.exists()) { 378 tmpFile.delete(); 379 } 380 tmpFile.deleteOnExit(); 381 return tmpFile; 382 } 383 384 @GET 385 @Path("downloadDoc") 386 public Response downloadDoc() throws IOException { 387 DocumentationService ds = Framework.getService(DocumentationService.class); 388 File tmp = getExportTmpFile(); 389 tmp.createNewFile(); 390 OutputStream out = new FileOutputStream(tmp); 391 ds.exportDocumentation(getContext().getCoreSession(), out); 392 out.flush(); 393 out.close(); 394 ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath()); 395 return Response.ok(aFile) 396 .header("Content-Disposition", "attachment;filename=" + "nuxeo-documentation.zip") 397 .type("application/zip") 398 .build(); 399 } 400 401 @GET 402 @Path("download/{distributionId}") 403 public Response downloadDistrib(@PathParam("distributionId") String distribId) throws IOException { 404 File tmp = getExportTmpFile(); 405 tmp.createNewFile(); 406 OutputStream out = new FileOutputStream(tmp); 407 getSnapshotManager().exportSnapshot(getContext().getCoreSession(), distribId, out); 408 out.close(); 409 String fName = "nuxeo-distribution-" + distribId + ".zip"; 410 fName = fName.replace(" ", "_"); 411 ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath()); 412 return Response.ok(aFile) 413 .header("Content-Disposition", "attachment;filename=" + fName) 414 .type("application/zip") 415 .build(); 416 } 417 418 /** 419 * Use to allow authorized users to upload distribution even in site mode 420 * 421 * @since 8.3 422 */ 423 @GET 424 @Path("_admin") 425 public Object getForms() { 426 NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal(); 427 if (SecurityHelper.canEditDocumentation(principal)) { 428 return getView("forms").arg("hideNav", Boolean.TRUE); 429 } else { 430 return Response.status(401).build(); 431 } 432 } 433 434 @POST 435 @Path("uploadDistrib") 436 @Produces("text/html") 437 public Object uploadDistrib() throws IOException { 438 if (!canAddDocumentation()) { 439 return null; 440 } 441 Blob blob = getContext().getForm().getFirstBlob(); 442 443 getSnapshotManager().importSnapshot(getContext().getCoreSession(), blob.getStream()); 444 getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession()); 445 446 return getView("index"); 447 } 448 449 @POST 450 @Path("uploadDistribTmp") 451 @Produces("text/html") 452 public Object uploadDistribTmp() throws IOException { 453 if (!canAddDocumentation()) { 454 return null; 455 } 456 Blob blob = getContext().getForm().getFirstBlob(); 457 if (blob == null || blob.getLength() == 0) { 458 return null; 459 } 460 DocumentModel snap = getSnapshotManager().importTmpSnapshot(getContext().getCoreSession(), blob.getStream()); 461 if (snap == null) { 462 log.error("Unable to import archive"); 463 return null; 464 } 465 DistributionSnapshot snapObject = snap.getAdapter(DistributionSnapshot.class); 466 return getView("uploadEdit").arg("tmpSnap", snap).arg("snapObject", snapObject); 467 } 468 469 @POST 470 @Path("uploadDistribTmpValid") 471 @Produces("text/html") 472 public Object uploadDistribTmpValid() { 473 if (!canAddDocumentation()) { 474 return null; 475 } 476 477 FormData formData = getContext().getForm(); 478 String name = formData.getString("name"); 479 String version = formData.getString("version"); 480 String pathSegment = formData.getString("pathSegment"); 481 String title = formData.getString("title"); 482 483 getSnapshotManager().validateImportedSnapshot(getContext().getCoreSession(), name, version, pathSegment, title); 484 getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession()); 485 return getView("importDone"); 486 } 487 488 @POST 489 @Path("uploadDoc") 490 @Produces("text/html") 491 public Object uploadDoc() throws IOException { 492 if (!canAddDocumentation()) { 493 return null; 494 } 495 496 Blob blob = getContext().getForm().getFirstBlob(); 497 if (blob == null || blob.getLength() == 0) { 498 return null; 499 } 500 501 DocumentationService ds = Framework.getService(DocumentationService.class); 502 ds.importDocumentation(getContext().getCoreSession(), blob.getStream()); 503 504 log.info("Documents imported."); 505 506 return getView("docImportDone"); 507 } 508 509 @GET 510 @Path("_reindex") 511 @Produces("text/plain") 512 public Object reindex() { 513 NuxeoPrincipal nxPrincipal = (NuxeoPrincipal) getContext().getPrincipal(); 514 if (!nxPrincipal.isAdministrator()) { 515 return Response.status(404).build(); 516 } 517 518 CoreSession coreSession = getContext().getCoreSession(); 519 String query = String.format( 520 "SELECT ecm:uuid FROM Document WHERE ecm:primaryType in ('%s') AND ecm:isProxy = 0 AND ecm:currentLifeCycleState <> 'deleted'", 521 StringUtils.join(AttributesExtractorStater.DOC_TYPES, "','")); 522 523 try (IterableQueryResult it = coreSession.queryAndFetch(query, NXQL.NXQL, QueryFilter.EMPTY);) { 524 for (Map<String, Serializable> map : it) { 525 String id = (String) map.get(NXQL.ECM_UUID); 526 Work work = new ExtractXmlAttributesWorker(coreSession.getRepositoryName(), nxPrincipal.getName(), id); 527 Framework.getLocalService(WorkManager.class).schedule(work); 528 } 529 } 530 531 return Response.ok().build(); 532 } 533 534 public boolean isEmbeddedMode() { 535 Boolean embed = (Boolean) getContext().getProperty("embeddedMode", Boolean.FALSE); 536 return embed != null && embed; 537 } 538 539 public boolean isEditor() { 540 if (isEmbeddedMode() || isSiteMode()) { 541 return false; 542 } 543 NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal(); 544 return SecurityHelper.canEditDocumentation(principal); 545 } 546 547 public boolean canAddDocumentation() { 548 NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal(); 549 return !isEmbeddedMode() && SecurityHelper.canEditDocumentation(principal); 550 } 551 552 public static boolean showCurrentDistribution() { 553 return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.current.distribution") || isSiteMode()); 554 } 555 556 public static boolean showSeamComponent() { 557 return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.seam.components") || isSiteMode()); 558 } 559 560 public static boolean isSiteMode() { 561 return Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.site.mode"); 562 } 563}