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}