001/*
002 *  Copyright 2014 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.plugins.flipbook;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.concurrent.ConcurrentHashMap;
023import java.util.concurrent.ConcurrentMap;
024import java.util.concurrent.locks.ReentrantLock;
025
026import org.apache.avalon.framework.activity.Initializable;
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.logger.AbstractLogEnabled;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.avalon.framework.thread.ThreadSafe;
033import org.apache.commons.io.FileUtils;
034import org.apache.commons.io.IOUtils;
035
036import org.ametys.plugins.repository.AmetysObjectResolver;
037import org.ametys.runtime.servlet.RuntimeConfig;
038
039/**
040 * Document to images action: converts the document into one image per page if it is not already done.
041 */
042public abstract class AbstractConvertDocument2ImagesComponent extends AbstractLogEnabled implements ThreadSafe, Initializable, Serviceable, Component
043{
044    /** The ametys object resolver. */
045    protected AmetysObjectResolver _resolver;
046    
047    /** The document to images convertor. */
048    protected Document2ImagesConvertor _documentToImages;
049    
050    /** The cocoon context */
051    protected org.apache.cocoon.environment.Context _cocoonContext;
052    
053    /** Map of locks, indexed by resource ID. */
054    private ConcurrentMap<String, ReentrantLock> _locks;
055    
056    @Override
057    public void initialize() throws Exception
058    {
059        _locks = new ConcurrentHashMap<>();
060    }   
061    
062    @Override
063    public void service(ServiceManager serviceManager) throws ServiceException
064    {
065        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
066        _documentToImages = (Document2ImagesConvertor) serviceManager.lookup(Document2ImagesConvertor.ROLE);
067    }
068    
069    /**
070     * Test if the mimetype is supported for conversion
071     * @param mimeType the mime type
072     * @return true if the mimetype can be transformed
073     */
074    protected boolean isMimeTypeSupported(String mimeType)
075    {
076        return _documentToImages.getSupportedMimeTypes().contains(mimeType);
077    }
078    
079    /**
080     * Create the images and put it into the cache
081     * @param relativeCachePath the path of the relative cache
082     * @param md5sum the md5 sum
083     * @param documentInputStream the input stream of the document
084     * @param documentName the document's name
085     * @param documentId the id of the document
086     * @param documentMimeType the mime type of the document
087     * @return The absolute cache path
088     * @throws IOException if an error occurs while manipulating files
089     * @throws FlipbookException if an error occurs while manipulating the flipbook
090     * @throws UnsupportedOperationException If the mime type is not supported
091     */
092    protected String cache(String relativeCachePath, String md5sum, InputStream documentInputStream, String documentName, String documentId, String documentMimeType) throws IOException, FlipbookException 
093    {
094        if (!isMimeTypeSupported(documentMimeType))
095        {
096            throw new UnsupportedOperationException("Cannot convert files of type '" + documentMimeType + "'");
097        }
098        
099        File baseFolder = FileUtils.getFile(RuntimeConfig.getInstance().getAmetysHome(), "flipbook", relativeCachePath);
100        if (!baseFolder.exists())
101        {
102            baseFolder.mkdirs();
103        }
104            
105        String cachePath = baseFolder.getPath().replace('\\', '/');
106        
107        File md5File = new File(baseFolder, "document.md5");
108        if (!md5File.isFile())
109        {
110            md5File.createNewFile();
111        }
112        // Compare the two 
113        String oldMd5 = FileUtils.readFileToString(md5File, "UTF-8");
114        
115        File documentFile = new File(baseFolder, "document/" + documentName);
116        
117        if (!md5sum.equals(oldMd5) || !documentFile.isFile())
118        {
119            ReentrantLock lock = new ReentrantLock();
120            
121            ReentrantLock oldLock = _locks.putIfAbsent(documentId, lock);
122            if (oldLock != null)
123            {
124                lock = oldLock;
125            }
126            
127            lock.lock();
128            
129            try
130            {
131                // Test the md5 again, perhaps it was generated by another thread while we were locked.
132                oldMd5 = FileUtils.readFileToString(md5File, "UTF-8");
133                
134                if (!md5sum.equals(oldMd5) || !documentFile.isFile())
135                {
136                    try
137                    {
138                        createImages(documentInputStream, documentName, baseFolder);
139                    }
140                    catch (Throwable t)
141                    {
142                        getLogger().error("An error occured while converting the document to images \"" + documentFile.getAbsolutePath() + "\"", t);
143                        throw new FlipbookException("An error occured while converting the document to images \"" + documentFile.getAbsolutePath() + "\"", t);
144                    }
145                }
146                
147                // MD5
148                FileUtils.writeStringToFile(md5File, md5sum, "UTF-8");
149            }
150            finally
151            {
152                lock.unlock();
153                
154                if (!lock.hasQueuedThreads())
155                {
156                    _locks.remove(documentId);
157                }
158            }
159        }
160        
161        return cachePath;
162    }
163
164    /**
165     * Create images for a resource, in a specified folder.
166     * @param documentInputStream the input stream of the document 
167     * @param documentName the name of the document
168     * @param baseFolder the base folder.
169     * @throws IOException if an error occurs while manipulating files
170     * @throws FlipbookException if an error occurs while manipulating the flipbook
171     */
172    protected void createImages(InputStream documentInputStream, String documentName, File baseFolder) throws IOException, FlipbookException
173    {
174        // Create the document folder.
175        File documentFolder = new File(baseFolder, "document");
176        documentFolder.mkdirs();
177        
178        // Create the document file.
179        File documentFile = new File(documentFolder, documentName);
180        
181        try (FileOutputStream fos = new FileOutputStream(documentFile))
182        {
183            IOUtils.copy(documentInputStream, fos);
184            fos.flush();
185        }
186        
187        // Trigger the images creation.
188        File imageFolder = new File(baseFolder, "pages");
189        imageFolder.mkdirs();
190        FileUtils.cleanDirectory(imageFolder);
191        
192        _documentToImages.convert(documentFile, imageFolder);
193    }
194}