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().getValue("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}