001/* 002 * Copyright 2010 Anyware Services 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 */ 016package org.ametys.web.generation; 017 018import java.io.File; 019import java.io.FileOutputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.nio.charset.StandardCharsets; 024import java.util.Map; 025import java.util.concurrent.locks.Lock; 026 027import javax.jcr.NoSuchWorkspaceException; 028import javax.jcr.Node; 029import javax.jcr.NodeIterator; 030import javax.jcr.Repository; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033 034import org.apache.avalon.framework.component.WrapperComponentManager; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.cocoon.Processor; 039import org.apache.cocoon.components.CocoonComponentManager; 040import org.apache.cocoon.environment.Context; 041import org.apache.cocoon.environment.ObjectModelHelper; 042import org.apache.cocoon.environment.Request; 043import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 044import org.apache.commons.collections.Predicate; 045import org.apache.commons.collections.PredicateUtils; 046import org.apache.commons.io.FileUtils; 047import org.apache.commons.io.IOUtils; 048import org.apache.excalibur.source.Source; 049import org.apache.excalibur.source.SourceResolver; 050import org.apache.jackrabbit.api.JackrabbitWorkspace; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053 054import org.ametys.cms.repository.Content; 055import org.ametys.plugins.repository.AmetysObjectResolver; 056import org.ametys.plugins.repository.AmetysRepositoryException; 057import org.ametys.plugins.repository.metadata.BinaryMetadata; 058import org.ametys.plugins.repository.metadata.CompositeMetadata; 059import org.ametys.plugins.repository.metadata.Folder; 060import org.ametys.plugins.repository.metadata.Resource; 061import org.ametys.plugins.repository.metadata.RichText; 062import org.ametys.plugins.repository.metadata.jcr.JCRResource; 063import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 064import org.ametys.runtime.config.Config; 065import org.ametys.web.WebConstants; 066import org.ametys.web.live.LiveAccessManager; 067import org.ametys.web.renderingcontext.RenderingContext; 068import org.ametys.web.renderingcontext.RenderingContextHandler; 069import org.ametys.web.repository.page.Page; 070import org.ametys.web.repository.page.Zone; 071import org.ametys.web.repository.page.ZoneItem; 072import org.ametys.web.repository.site.Site; 073import org.ametys.web.repository.sitemap.Sitemap; 074import org.ametys.web.synchronization.SynchronizeComponent; 075 076/** 077 * Component for generating a site. 078 */ 079public class SiteGenerator implements Serviceable 080{ 081 /** Logger available to subclasses. */ 082 protected final Logger _logger = LoggerFactory.getLogger(getClass()); 083 084 private RenderingContextHandler _renderingContextHandler; 085 086 @Override 087 public void service(ServiceManager manager) throws ServiceException 088 { 089 _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE); 090 } 091 092 /** 093 * Generates site's content. 094 * @param manager the service manager. 095 * @param objectModel the objectModel. 096 * @param site the site to populate. 097 * @param tempOutputDir the output directory. 098 * @throws Exception if an error occurs. 099 */ 100 public void generate(ServiceManager manager, Map objectModel, Site site, File tempOutputDir) throws Exception 101 { 102 try 103 { 104 Repository repository = (Repository) manager.lookup(Repository.class.getName()); 105 AmetysObjectResolver resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 106 LiveAccessManager liveAccessManager = (LiveAccessManager) manager.lookup(LiveAccessManager.ROLE); 107 SynchronizeComponent synchronizeComponent = (SynchronizeComponent) manager.lookup(SynchronizeComponent.ROLE); 108 109 Lock lock = liveAccessManager.getLiveWriteLock(); 110 111 // Ensure write access to the live workspace is exclusive 112 lock.lock(); 113 114 try 115 { 116 if (_logger.isDebugEnabled()) 117 { 118 _logger.debug("Live write lock has been acquired: " + lock); 119 } 120 121 Session liveSession = null; 122 Session generationSession = null; 123 124 // FIXME refactor with JCR 2.0 methods 125 126 try 127 { 128 // Open a session to the workspace live 129 liveSession = repository.login(WebConstants.LIVE_WORKSPACE); 130 131 try 132 { 133 generationSession = repository.login("generation"); 134 } 135 catch (NoSuchWorkspaceException e) 136 { 137 // Not available with JCR 1.0 :( 138 ((JackrabbitWorkspace) liveSession.getWorkspace()).createWorkspace("generation"); 139 generationSession = repository.login("generation"); 140 } 141 142 Node generationRootNode = generationSession.getRootNode(); 143 NodeIterator itNode = generationRootNode.getNodes(); 144 145 // Remove workspace content 146 while (itNode.hasNext()) 147 { 148 Node node = itNode.nextNode(); 149 150 if (!node.getName().equals("jcr:system")) 151 { 152 node.remove(); 153 } 154 } 155 156 // Commit changes 157 generationSession.save(); 158 159 Node liveRootNode = liveSession.getRootNode(); 160 161 // Clone nodes 162 synchronizeComponent.cloneNodeAndPreserveUUID(liveRootNode, generationRootNode, PredicateUtils.truePredicate(), new Predicate() 163 { 164 @Override 165 public boolean evaluate(Object object) 166 { 167 if (object instanceof Node) 168 { 169 try 170 { 171 return !((Node) object).getPath().equals("/jcr:system"); 172 } 173 catch (RepositoryException e) 174 { 175 throw new RuntimeException(e); 176 } 177 } 178 179 return false; 180 } 181 }); 182 183 // Commit changes 184 generationSession.save(); 185 186 // Retrieve current workspace 187 Request request = ObjectModelHelper.getRequest(objectModel); 188 189 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 190 RenderingContext currentContext = _renderingContextHandler.getRenderingContext(); 191 192 try 193 { 194 // Use live workspace 195 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "generation"); 196 197 // Set right rendering context 198 _renderingContextHandler.setRenderingContext(RenderingContext.FRONT); 199 200 // Retrieve the site in the live workspace 201 Site liveSite = resolver.resolveById(site.getId()); 202 203 _generateSite(manager, objectModel, liveSite, tempOutputDir); 204 } 205 finally 206 { 207 // Restore context 208 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 209 _renderingContextHandler.setRenderingContext(currentContext); 210 } 211 } 212 catch (Exception e) 213 { 214 throw new Exception("Generation of site: " + site + " failed", e); 215 } 216 finally 217 { 218 if (liveSession != null) 219 { 220 liveSession.logout(); 221 } 222 if (generationSession != null) 223 { 224 generationSession.logout(); 225 } 226 } 227 } 228 finally 229 { 230 lock.unlock(); 231 } 232 } 233 catch (ServiceException e) 234 { 235 _logger.error("Unable to lookup for a component needed for the generation", e); 236 } 237 } 238 239 private void _generateSite(ServiceManager manager, Map objectModel, Site site, File tempOutputDir) throws Exception 240 { 241 // Create environment 242 // TODO change context depending on the site 243 Context context = ObjectModelHelper.getContext(objectModel); 244 GenerationEnvironment environment = new GenerationEnvironment(new SLF4JLoggerAdapter(_logger), context, ""); 245 Processor processor = (Processor) manager.lookup(Processor.ROLE); 246 Object processingKey = CocoonComponentManager.startProcessing(environment); 247 int environmentDepth = CocoonComponentManager.markEnvironment(); 248 CocoonComponentManager.enterEnvironment(environment, new WrapperComponentManager(manager), processor); 249 250 try 251 { 252 // Clean directory first 253 if (tempOutputDir.exists() && tempOutputDir.isDirectory()) 254 { 255 FileUtils.cleanDirectory(tempOutputDir); 256 } 257 else 258 { 259 tempOutputDir.mkdirs(); 260 } 261 262 // Generate sitemap.xml & use this sitemap.xml 263 SourceResolver resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 264 265 try 266 { 267 String siteName = site.getName(); 268 269 // For each sitemap 270 for (Sitemap sitemap : site.getSitemaps()) 271 { 272 if (_logger.isDebugEnabled()) 273 { 274 _logger.debug("Processing sitemap: " + sitemap); 275 } 276 277 String sitemapName = sitemap.getName(); 278 279 // Generate each page 280 for (Page page : sitemap.getChildrenPages()) 281 { 282 _generatePage(resolver, tempOutputDir, siteName, sitemapName, page); 283 } 284 } 285 } 286 finally 287 { 288 manager.release(resolver); 289 } 290 291 // Copy skin resources 292 _copySkin(context, tempOutputDir); 293 294 // Generate additional resources (at least error page in each language) 295 _generationAdditionalResources(manager, site, tempOutputDir); 296 297 // Execute script for pushing changes 298 _pushChanges(site); 299 } 300 finally 301 { 302 CocoonComponentManager.leaveEnvironment(); 303 CocoonComponentManager.endProcessing(environment, processingKey); 304 CocoonComponentManager.checkEnvironment(environmentDepth, new SLF4JLoggerAdapter(_logger)); 305 manager.release(processor); 306 } 307 308 } 309 310 private void _generatePage(SourceResolver resolver, File tempOutputDir, String siteName, String sitemapName, Page page) throws AmetysRepositoryException, IOException, RepositoryException 311 { 312 if (_logger.isDebugEnabled()) 313 { 314 _logger.debug("Processing page: " + page); 315 } 316 317 String pageContext = sitemapName + "/" + page.getPathInSitemap(); 318 319 switch (page.getType()) 320 { 321 case LINK: 322 case NODE: 323 // No HTML rendering because of redirection 324 break; 325 case CONTAINER: 326 // Add content binary metadata 327 for (Zone zone : page.getZones()) 328 { 329 for (ZoneItem zoneItem : zone.getZoneItems()) 330 { 331 switch (zoneItem.getType()) 332 { 333 case SERVICE: 334 // No resources attached 335 break; 336 case CONTENT: 337 Content content = zoneItem.getContent(); 338 339 if (_logger.isDebugEnabled()) 340 { 341 _logger.debug("Processing resources of content: " + content); 342 } 343 344 _generateResources(resolver, tempOutputDir, pageContext + "/_data", content.getMetadataHolder()); 345 346 break; 347 default: 348 break; 349 } 350 } 351 } 352 353 // Generate HTML rendering 354 // TODO How to add pdf export ? 355 _generateURI(resolver, tempOutputDir, "cocoon://" + siteName + "/" + pageContext + ".html", pageContext); 356 break; 357 default: 358 break; 359 } 360 361 // Generate child pages 362 for (Page childPage : page.getChildrenPages()) 363 { 364 _generatePage(resolver, tempOutputDir, siteName, sitemapName, childPage); 365 } 366 } 367 368 private void _copySkin(Context context, File tempOutputDir) throws IOException 369 { 370 // TODO Copy only used skin ? 371 FileUtils.copyDirectoryToDirectory(new File(context.getRealPath("/skins")), tempOutputDir); 372 } 373 374 private void _generationAdditionalResources(ServiceManager manager, Site site, File tempOutputDir) throws Exception 375 { 376 AdditionalSiteResourcesProviderExtensionPoint addResourcesExtPt = (AdditionalSiteResourcesProviderExtensionPoint) manager.lookup(AdditionalSiteResourcesProviderExtensionPoint.ROLE); 377 378 for (String extensionId : addResourcesExtPt.getExtensionsIds()) 379 { 380 AdditionalSiteResourcesProvider resourcesProvider = addResourcesExtPt.getExtension(extensionId); 381 382 if (_logger.isDebugEnabled()) 383 { 384 _logger.debug("Processing resources provider: " + resourcesProvider); 385 } 386 387 resourcesProvider.addResources(site, tempOutputDir); 388 } 389 } 390 391 private void _pushChanges(Site site) throws Exception 392 { 393 String shell = Config.getInstance().getValueAsString("generation.shell"); 394 //shell = "/bin/echo"; 395 396 if (shell != null && shell.trim().length() != 0) 397 { 398 if (_logger.isInfoEnabled()) 399 { 400 _logger.info("Creating process: " + shell); 401 } 402 403 Process process = Runtime.getRuntime().exec(shell + " " + site.getName()); 404 405 // Waiting for the process to finish 406 int statusCode = process.waitFor(); 407 408 if (statusCode == 0) 409 { 410 if (_logger.isInfoEnabled()) 411 { 412 _logger.info("Process terminated correctly, generation successful"); 413 } 414 415 if (_logger.isDebugEnabled()) 416 { 417 _logger.debug("Process output stream: " + IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8)); 418 _logger.debug("Process error stream: " + IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8)); 419 } 420 } 421 else 422 { 423 if (_logger.isWarnEnabled()) 424 { 425 _logger.warn("Shell terminated with an error, exit code: " + statusCode); 426 _logger.warn("Output stream: " + IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8)); 427 _logger.warn("Error stream: " + IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8)); 428 } 429 430 throw new Exception("Process terminated with an error, exit code: " + statusCode); 431 } 432 } 433 else 434 { 435 if (_logger.isInfoEnabled()) 436 { 437 _logger.info("No process for pushing changes"); 438 } 439 } 440 } 441 442 private void _generateResources(SourceResolver resolver, File tempOutputDir, String pageContext, CompositeMetadata compositeMetadata) throws IOException, RepositoryException 443 { 444 String resourcesContext = pageContext + "/_data"; 445 446 for (String metadataName : compositeMetadata.getMetadataNames()) 447 { 448 switch (compositeMetadata.getType(metadataName)) 449 { 450 case BINARY: 451 BinaryMetadata binaryMetadata = compositeMetadata.getBinaryMetadata(metadataName); 452 _copyResource(tempOutputDir, resourcesContext, binaryMetadata.getFilename(), binaryMetadata); 453 break; 454 case RICHTEXT: 455 RichText richText = compositeMetadata.getRichText(metadataName); 456 Folder folder = richText.getAdditionalDataFolder(); 457 _copyFolder(tempOutputDir, resourcesContext, folder); 458 break; 459 case COMPOSITE: 460 _generateResources(resolver, tempOutputDir, resourcesContext, compositeMetadata); 461 break; 462 default: 463 break; 464 } 465 } 466 467 } 468 469 private void _copyFolder(File tempOutputDir, String resourcesContext, Folder folder) throws AmetysRepositoryException, IOException, RepositoryException 470 { 471 for (org.ametys.plugins.repository.metadata.File file : folder.getFiles()) 472 { 473 _copyResource(tempOutputDir, resourcesContext, file.getName(), file.getResource()); 474 } 475 476 for (Folder subFolder : folder.getFolders()) 477 { 478 _copyFolder(tempOutputDir, resourcesContext, subFolder); 479 } 480 } 481 482 private void _copyResource(File tempOutputDir, String pageContext, String fileName, Resource resource) throws IOException, RepositoryException 483 { 484 String newFileName = fileName; 485 486 if (newFileName == null) 487 { 488 newFileName = "data"; 489 // TODO found a library for computing extension from a mime-type 490 String mimeType = resource.getMimeType(); 491 492 if (mimeType.equals("image/jpeg")) 493 { 494 newFileName += ".jpg"; 495 } 496 else if (mimeType.equals("image/gif")) 497 { 498 newFileName += ".gif"; 499 } 500 else if (mimeType.equals("image/png")) 501 { 502 newFileName += ".png"; 503 } 504 else if (mimeType.equals("application/x-shockwave-flash")) 505 { 506 newFileName += ".swf"; 507 } 508 else if (mimeType.equals("application/pdf")) 509 { 510 newFileName += ".pdf"; 511 } 512 else if (mimeType.equals("application/msword")) 513 { 514 newFileName += ".doc"; 515 } 516 else if (mimeType.equals("application/vnd.ms-excel")) 517 { 518 newFileName += ".xls"; 519 } 520 else if (mimeType.equals("application/vnd.ms-powerpoint")) 521 { 522 newFileName += ".ppt"; 523 } 524 else if (mimeType.equals("application/vnd.oasis.opendocument.text")) 525 { 526 newFileName += ".odt"; 527 } 528 else if (mimeType.equals("application/zip")) 529 { 530 newFileName += ".zip"; 531 } 532 else if (_logger.isWarnEnabled()) 533 { 534 _logger.warn("Unknown mime-type: " + mimeType + ", using: " + newFileName); 535 } 536 } 537 538 // TODO encode filename to avoid encoding/space problem ? 539 540 String resourceDir = pageContext; 541 542 if (resource instanceof JCRResource) 543 { 544 String uuid = ((JCRResource) resource).getNode().getIdentifier(); 545 resourceDir += "/" + uuid; 546 } 547 548 File output = new File(new File(tempOutputDir, resourceDir), fileName); 549 output.getParentFile().mkdirs(); 550 try (InputStream is = resource.getInputStream(); OutputStream os = new FileOutputStream(output)) 551 { 552 IOUtils.copy(is, os); 553 } 554 } 555 556 private void _generateURI(SourceResolver resolver, File tempOutputDir, String uri, String destPath) throws IOException 557 { 558 Source source = resolver.resolveURI(uri); 559 560 File output = new File(tempOutputDir, destPath); 561 output.getParentFile().mkdirs(); 562 try (InputStream is = source.getInputStream(); OutputStream os = new FileOutputStream(output)) 563 { 564 IOUtils.copy(is, os); 565 } 566 } 567}