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}