001/*
002 *  Copyright 2017 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.core.ui.minimize;
017
018import java.io.UnsupportedEncodingException;
019import java.util.ArrayList;
020import java.util.Base64;
021import java.util.List;
022import java.util.Map;
023import java.util.Objects;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.activity.Initializable;
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.context.Context;
030import org.apache.avalon.framework.context.ContextException;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.cocoon.components.ContextHelper;
036import org.apache.cocoon.environment.Request;
037import org.apache.commons.lang3.StringUtils;
038
039import org.ametys.core.DevMode;
040import org.ametys.core.DevMode.DEVMODE;
041import org.ametys.core.cache.AbstractCacheManager;
042import org.ametys.core.cache.Cache;
043import org.ametys.plugins.core.ui.resources.ResourceDependenciesListExtensionPoint;
044import org.ametys.plugins.core.ui.util.RequestAttributesHelper;
045import org.ametys.runtime.i18n.I18nizableText;
046import org.ametys.runtime.plugin.component.AbstractLogEnabled;
047
048/**
049 * The cache for hashed list of files to minimize
050 */
051public class HashCache extends AbstractLogEnabled implements Component, Contextualizable, Serviceable, Initializable
052{
053    /** The avalon ROLE */
054    public static final String ROLE = HashCache.class.getName();
055    
056    /** RequestAttributesHelper */
057    protected RequestAttributesHelper _requestAttributesHelper;
058
059    /** ResourceDependenciesListExtensionPoint */
060    protected ResourceDependenciesListExtensionPoint _resourceDependenciesListEP;
061
062    /** CacheManager used to create and get cache */
063    protected AbstractCacheManager _cacheManager;
064
065    private Context _context;
066
067    @Override
068    public void contextualize(Context context) throws ContextException
069    {
070        _context = context;
071    }
072
073    public void service(ServiceManager manager) throws ServiceException
074    {
075        _requestAttributesHelper = (RequestAttributesHelper) manager.lookup(RequestAttributesHelper.ROLE);
076        
077        _resourceDependenciesListEP = (ResourceDependenciesListExtensionPoint) manager.lookup(ResourceDependenciesListExtensionPoint.ROLE);
078        
079        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
080    }
081
082    public void initialize() throws Exception
083    {
084        _cacheManager.createMemoryCache(ROLE, 
085                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_HASH_LABEL"),
086                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_HASH_DESCRIPTION"),
087                true,
088                null);
089    }
090    
091    /**
092     * Get the file list corresponding to the given hashcode
093     * @param hash The hashcode created by createHash in this session
094     * @param firstLevel File imported by other files will not be included
095     * @return The list of files or null if the hashcode does not exists
096     */
097    public List<UriData> getFilesForHash(String hash, boolean firstLevel)
098    {
099        List<UriData> uriDataList = _getCache().get(hash);
100        if (firstLevel)
101        {
102            return uriDataList.stream()
103                    .filter(UriData::isFirstLevel)
104                    .collect(Collectors.toList());
105        }
106        return uriDataList;
107    }
108    
109    /**
110     * Creates and memorize a hash code corresponding to a list of files 
111     * @param files The files. Key is the file uri and the value is a map with two keys 'media' and 'tag'.
112     * @param salt Additional parameter used as salt for the hash key.
113     * @return The hash code
114     * @throws IllegalArgumentException If an error occurred
115     */
116    public String createHash(Map<String, Map<String, String>> files, String salt) throws IllegalArgumentException
117    {
118        List<UriData> hashCache = new ArrayList<>();
119        
120        Map<String, Object> attributes = _requestAttributesHelper.saveRequestAttributes();
121        
122        try
123        {
124            Request request = ContextHelper.getRequest(_context);
125            DEVMODE developerMode = DevMode.getDeveloperMode(request);
126            
127            // in production mode, we assume that the dependencies are not modified during the application lifetime
128            // so the hashcode only depends on the first level dependencies
129            hashCache = _getFiles(files, developerMode == DEVMODE.PRODUCTION);
130            
131            String hash = Base64.getEncoder().withoutPadding().encodeToString(String.valueOf(31 * hashCache.hashCode() + salt.hashCode()).getBytes("UTF-8"));
132            
133            if (developerMode == DEVMODE.PRODUCTION)
134            {
135                if (!_getCache().hasKey(hash))
136                {
137                    // if the files have never been cached, even in production mode we should run the while process at least one time
138                    hashCache = _getFiles(files, false);
139                    _getCache().put(hash, hashCache);
140                }
141            }
142            else
143            {
144                _getCache().put(hash, hashCache);
145            }
146            
147            return hash;
148        }
149        catch (UnsupportedEncodingException e)
150        {
151            throw new IllegalArgumentException(e);
152        }
153        finally
154        {
155            _requestAttributesHelper.restoreRequestAttributes(attributes);
156        }
157    }
158    
159    private List<UriData> _getFiles(Map<String, Map<String, String>> files, boolean onlyFirstLevel)
160    {
161        return files.entrySet().stream()
162                    .map(file -> _resourceDependenciesListEP.getDependencies(file.getKey(), file.getValue(), onlyFirstLevel))
163                    .filter(Objects::nonNull)
164                    .flatMap(Set::stream)
165                    .collect(Collectors.toList());
166    }
167
168    /**
169     * The description of an URI
170     */
171    public static class UriData
172    {
173        private String _uri;
174        private Long _lastModified;
175        private String _media;
176        private boolean _firstLevel;
177        
178        /**
179         * Default constructor for a file data
180         * @param uri The uri locating the file
181         * @param firstLevel False if the file data is an import from another file data
182         */
183        public UriData(String uri, boolean firstLevel)
184        {
185            _uri = uri;
186            _firstLevel = firstLevel;
187        }
188        
189        /**
190         * Set the last modified value
191         * @param lastModified the lastModified to set
192         */
193        public void setLastModified(Long lastModified)
194        {
195            this._lastModified = lastModified;
196        }
197
198        /**
199         * set the medias value
200         * @param media the medias to set
201         */
202        public void setMedia(String media)
203        {
204            this._media = media;
205        }
206
207        /**
208         * Get the file uri
209         * @return the uri
210         */
211        public String getUri()
212        {
213            return _uri;
214        }
215        
216        /**
217         * Get the file last modified date
218         * @return the lastModified
219         */
220        public Long getLastModified()
221        {
222            return _lastModified;
223        }
224        
225        /**
226         * Get the file medias
227         * @return the medias
228         */
229        public String getMedia()
230        {
231            return _media;
232        }
233        
234        /**
235         * Check if the file is a first level file
236         * @return True if the file is first level
237         */
238        public boolean isFirstLevel()
239        {
240            return _firstLevel;
241        }
242        
243        @Override
244        public boolean equals(Object obj)
245        {
246            if (obj instanceof UriData)
247            {
248                UriData fObj = (UriData) obj;
249                return StringUtils.equals(_uri, fObj._uri)
250                        && (_lastModified == null ? fObj._lastModified == null : _lastModified.equals(fObj._lastModified)) 
251                        && StringUtils.equals(_media, fObj._media);
252            }
253            return false;
254        }
255        
256        @Override
257        public int hashCode()
258        {
259            return Objects.hash(_uri, _lastModified, _media);
260        }
261        
262        @Override
263        public String toString()
264        {
265            if (_media != null)
266            {
267                return _uri + "#" + _media + " (" + _lastModified + ")";
268            }
269            else
270            {
271                return _uri + " (" + _lastModified + ")";
272            }
273        }
274    }
275
276    private Cache<String, List<UriData>> _getCache() 
277    {
278        return this._cacheManager.get(ROLE);
279    }
280}