001/*
002 *  Copyright 2013 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.site;
017
018import java.io.File;
019import java.io.IOException;
020
021import org.apache.cocoon.ProcessingException;
022import org.apache.cocoon.components.source.SourceUtil;
023import org.apache.cocoon.environment.SourceResolver;
024import org.apache.commons.codec.digest.DigestUtils;
025import org.apache.commons.io.FilenameUtils;
026import org.apache.commons.lang.StringUtils;
027import org.apache.excalibur.source.Source;
028import org.apache.excalibur.source.SourceException;
029import org.apache.excalibur.source.impl.FileSource;
030
031import org.ametys.core.util.URLEncoder;
032import org.ametys.runtime.config.Config;
033import org.ametys.runtime.servlet.RuntimeConfig;
034
035/**
036 * Class providing helper methods for cache files.
037 */
038public final class SiteCacheHelper
039{
040    
041    private static final String __MD5_PREFIX = "md5$";
042    
043    private SiteCacheHelper()
044    {
045        // Hidden.
046    }
047    
048    /**
049     * Return the root cache file
050     * @return The root cache File
051     */
052    public static File getRootCache()
053    {
054        return new File(RuntimeConfig.getInstance().getAmetysHome(), "cache");
055    }
056    
057    /**
058     * Test if a file is valid.
059     * @param file the file to test.
060     * @return true if the file is valid, false otherwise.
061     */
062    public static boolean isValid(File file)
063    {
064        boolean valid = true;
065        
066        long maxLength = Config.getInstance().getValueAsLong("org.ametys.site.cache.max.filename.length");
067        
068        try
069        {
070            file.getCanonicalPath();
071            file.toURI();
072            
073            valid = file.getName().length() <= maxLength;
074            
075            File parent = file.getParentFile();
076            while (valid && parent != null)
077            {
078                valid = parent.getName().length() <= maxLength;
079                parent = parent.getParentFile();
080            }
081        }
082        catch (IOException e)
083        {
084            valid = false;
085        }
086        
087        return valid;
088    }
089    
090    /**
091     * Test if a file source is valid.
092     * @param fileSource the file source to test.
093     * @return true if the file is valid, false otherwise.
094     */
095    public static boolean isValid(FileSource fileSource)
096    {
097        return isValid(fileSource.getFile());
098    }
099    
100    /**
101     * Get a hashed file path for the given file.
102     * @param file the original resource file.
103     * @return the hashed file path.
104     */
105    public static String getHashedFilePath(File file)
106    {
107        return getHashedFilePath(file, false);
108    }
109    
110    /**
111     * Get a hashed file path for the given file.
112     * @param file the original resource file.
113     * @param encode true to encode the folder parts, false otherwise.
114     * @return the hashed file path.
115     */
116    public static String getHashedFilePath(File file, boolean encode)
117    {
118        return getHashedFilePath(file.getPath(), encode);
119    }
120    
121    /**
122     * Get a hashed version of the given file path.
123     * @param filePath the original file path.
124     * @return the hashed file path.
125     */
126    public static String getHashedFilePath(String filePath)
127    {
128        return getHashedFilePath(filePath, false);
129    }
130    
131    /**
132     * Get a hashed version of the given file path.
133     * @param filePath the original file path.
134     * @param encode true to encode the folder parts, false otherwise.
135     * @return the hashed file path.
136     */
137    public static String getHashedFilePath(String filePath, boolean encode)
138    {
139        long maxLength = Config.getInstance().getValueAsLong("org.ametys.site.cache.max.filename.length");
140        
141        String prefix = FilenameUtils.getPrefix(filePath);
142        String basePath = FilenameUtils.getPath(filePath);
143        String baseName = FilenameUtils.getBaseName(filePath);
144        String name = FilenameUtils.getName(filePath);
145        
146        String encodedBasePath = hashPath(basePath, encode);
147        
148        if (name.length() > maxLength)
149        {
150            String extension = FilenameUtils.getExtension(filePath);
151            name = __MD5_PREFIX + DigestUtils.md5Hex(baseName) + "." + extension;
152        }
153        
154        String validPath = prefix + encodedBasePath + name;
155        
156        return validPath;
157    }
158    
159    /**
160     * Release the given file source and resolve a hashed version of the file to replace it.
161     * @param resolver the source resolver.
162     * @param source the invalid file source to hash.
163     * @return the hashed file source.
164     * @throws IOException if an IO error occurs.
165     * @throws ProcessingException if the new source couldn't be resolved.
166     */
167    public static Source getHashedFileSource(SourceResolver resolver, FileSource source) throws IOException, ProcessingException
168    {
169        File file = source.getFile();
170        
171        String validPath = SiteCacheHelper.getHashedFilePath(file, true);
172        String prefix = source.getScheme() + ":";
173        
174        try
175        {
176            // Release the old source
177            resolver.release(source);
178            
179            // Resolve the new source.
180            return resolver.resolveURI(prefix + validPath);
181        }
182        catch (SourceException e)
183        {
184            throw SourceUtil.handle("Error during resolving of '" + validPath + "'.", e);
185        }
186    }
187    
188    /**
189     * Hash the resource path
190     * @param path the resource path
191     * @param encode true to encode the un-hashed paths, false otherwise.
192     * @return the hashed and encoded URI
193     */
194    private static String hashPath(String path, boolean encode)
195    {
196        long maxLength = Config.getInstance().getValueAsLong("org.ametys.site.cache.max.filename.length");
197        
198        StringBuilder sb = new StringBuilder();
199        
200        String[] parts = StringUtils.split(path, "/\\");
201        for (int i = 0; i < parts.length; i++)
202        {
203            if (i > 0 || path.charAt(0) == File.separatorChar)
204            {
205                sb.append(File.separatorChar);
206            }
207            
208            if (parts[i].length() > maxLength)
209            {
210                // Hash the path part.
211                sb.append(__MD5_PREFIX).append(DigestUtils.md5Hex(parts[i]));
212            }
213            else if (encode)
214            {
215                // Encode the path part.
216                sb.append(URLEncoder.encodePath(parts[i]));
217            }
218            else
219            {
220                // Leave the path part as is.
221                sb.append(parts[i]);
222            }
223        }
224        
225        if (sb.charAt(sb.length() - 1) != File.separatorChar)
226        {
227            sb.append(File.separatorChar);
228        }
229        
230        return sb.toString();
231    }
232    
233}