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}