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