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