001/*
002 *  Copyright 2016 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.core.ui;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Map.Entry;
023
024import org.apache.avalon.framework.activity.Initializable;
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.component.WrapperComponentManager;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.excalibur.source.SourceResolver;
034
035import org.ametys.core.cache.AbstractCacheManager;
036import org.ametys.core.cache.Cache;
037import org.ametys.core.ui.ribbonconfiguration.RibbonConfiguration;
038import org.ametys.core.ui.ribbonconfiguration.RibbonConfigurationSource;
039import org.ametys.runtime.i18n.I18nizableText;
040import org.ametys.runtime.plugin.component.AbstractLogEnabled;
041import org.ametys.runtime.plugin.component.PluginsComponentManager;
042import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
043
044/**
045 * Helper for RibbonManager, that support Thread safe usage of the managers, while implementing a cache for performances.
046 */
047public class RibbonManagerCache extends AbstractLogEnabled implements Contextualizable, Serviceable, Component, Initializable
048{
049    /** Avalon role */
050    public static final String ROLE = RibbonManagerCache.class.getName();
051    
052    private Map<RibbonManager, Map<String, Long>> _ribbonManagerCacheValidity = new HashMap<>();
053    private Map<RibbonManager, Integer> _ribbonManagerUsageCache = new HashMap<>();
054    private Map<RibbonManager, RibbonConfiguration> _ribbonConfigurationCache = new HashMap<>();
055    private Map<RibbonManager, ThreadSafeComponentManager<Object>> _ribbonServiceManagers = new HashMap<>();
056    
057    private Context _context;
058
059    private ServiceManager _cocoonManager;
060
061    private RibbonControlsManager _ribbonControlsManager;
062
063    private RibbonTabsManager _ribbonTabsManager;
064    
065    private SourceResolver _resolver;
066    
067    private AbstractCacheManager _cacheManager;
068    
069    public void contextualize(Context context) throws ContextException
070    {
071        _context = context;
072    }
073    
074    public void service(ServiceManager manager) throws ServiceException
075    {
076        _cocoonManager = manager;
077        _ribbonControlsManager = (RibbonControlsManager) manager.lookup(RibbonControlsManager.ROLE);
078        _ribbonTabsManager = (RibbonTabsManager) manager.lookup(RibbonTabsManager.ROLE);
079        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
080        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
081    }
082
083    public void initialize() throws Exception
084    {
085        _cacheManager.createMemoryCache(ROLE, 
086                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_RIBBON_MANAGER_LABEL"),
087                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_RIBBON_MANAGER_DESCRIPTION"),
088                false,
089                null);
090    }
091    
092    /**
093     * Invalidate the cache
094     */
095    public void invalidate()
096    {
097        _getRibbonManagerCache().invalidateAll();
098    }
099    
100    /**
101     * Create the RibbonManager associated with the given ribbon file. 
102     * If the RibbonManager already exists for the ribbon file and is still valid, it is simply returned.  
103     * @param uri The ribbon configuration uri
104     * @return The RibbonManager
105     * @throws Exception If an error occurs
106     */
107    public synchronized RibbonManager getManager(String uri) throws Exception
108    {
109        RibbonManager ribbonManager = _getRibbonManagerCache().get(uri);
110        if (ribbonManager != null)
111        {
112            if (_isRibbonManagerStillValid(ribbonManager))
113            {
114                _increaseUsage(ribbonManager);
115                return ribbonManager;
116            }
117            else
118            {
119                _ribbonManagerCacheValidity.remove(ribbonManager);
120                _decreaseUsage(ribbonManager, true);
121            }
122        }
123        
124        ribbonManager = _createRibbonManager();
125        _getRibbonManagerCache().put(uri, ribbonManager);
126        _ribbonManagerCacheValidity.put(ribbonManager, null);
127        _increaseUsage(ribbonManager);
128        
129        return ribbonManager;
130    }
131
132    private boolean _isRibbonManagerStillValid(RibbonManager ribbonManager)
133    {
134        Map<String, Long> validity = _ribbonManagerCacheValidity.get(ribbonManager);
135        if (validity == null)
136        {
137            return false;
138        }
139        
140        for (Entry<String, Long> entry : validity.entrySet())
141        {
142            RibbonConfigurationSource ribbonConfigurationEntry = null;
143            try
144            {
145                ribbonConfigurationEntry = RibbonConfigurationSource.createFromUri(entry.getKey(), _resolver);
146                if (!_isRibbonEntryStillValid(entry.getValue(), ribbonConfigurationEntry))
147                {
148                    return false;
149                }
150            }
151            catch (IOException e)
152            {
153                // invalid import, uri is out of date
154                return false;
155            }
156            finally
157            {
158                if (ribbonConfigurationEntry != null)
159                {
160                    _resolver.release(ribbonConfigurationEntry.getSource());
161                }
162            }
163        }
164        
165        return true;
166    }
167    
168    private boolean _isRibbonEntryStillValid(Long cachedValidity, RibbonConfigurationSource ribbonEntrySource)
169    {
170        boolean entryExists = ribbonEntrySource.getSource().exists();
171        if (cachedValidity == 0 && entryExists)
172        {
173            // Unable to get "Last Modified" of Entry. Assume changed.
174            return false;
175        }
176        
177        if (cachedValidity > 0 && !entryExists)
178        {
179            // Entry was deleted since it was put in cache.
180            return false;
181        }
182        
183        // Compare the last modified to the cached "last modified" value, to check for changes
184        return ribbonEntrySource.getLastModified().equals(cachedValidity);
185    }
186
187    private RibbonManager _createRibbonManager() throws Exception
188    {
189        PluginsComponentManager ribbonServiceManager = new PluginsComponentManager(new WrapperComponentManager(_cocoonManager));
190        ribbonServiceManager.contextualize(_context);
191        ribbonServiceManager.service(_cocoonManager);
192        ribbonServiceManager.setLogger(getLogger());
193        ribbonServiceManager.addExtensionPoint("core-ui", RibbonManager.ROLE, RibbonManager.class, null, new ArrayList<>());
194        ribbonServiceManager.initialize();
195        
196        RibbonManager ribbonManager = (RibbonManager) ribbonServiceManager.lookup(RibbonManager.ROLE);
197        ribbonManager.initialize();
198
199        _ribbonServiceManagers.put(ribbonManager, ribbonServiceManager);
200        
201        _ribbonControlsManager.registerRibbonManager(ribbonManager);
202        _ribbonTabsManager.registerRibbonManager(ribbonManager);
203        return ribbonManager;
204    }
205    
206    /**
207     * Dispose of a RibbonManager that was previously retrieve with this helper
208     * @param ribbonManager The ribbon manager
209     */
210    public void dispose(RibbonManager ribbonManager)
211    {
212        if (ribbonManager != null)
213        {
214            _decreaseUsage(ribbonManager, false);
215        }
216    }
217
218    private synchronized void _increaseUsage(RibbonManager ribbonManager)
219    {
220        Integer usage = _ribbonManagerUsageCache.get(ribbonManager);
221        _ribbonManagerUsageCache.put(ribbonManager, usage != null ? usage + 1 : 1);
222    }
223    
224    @SuppressWarnings("null")
225    private synchronized void _decreaseUsage(RibbonManager ribbonManager, boolean forceRemove)
226    {
227        Integer usage = _ribbonManagerUsageCache.get(ribbonManager);
228        if (forceRemove || usage != null)
229        {
230            if (!forceRemove && usage > 1)
231            {
232                _ribbonManagerUsageCache.put(ribbonManager, usage - 1);
233            }
234            else
235            {
236                _ribbonManagerUsageCache.remove(ribbonManager);
237                if (!_ribbonManagerCacheValidity.containsKey(ribbonManager))
238                {
239                    _ribbonConfigurationCache.remove(ribbonManager);
240                    ThreadSafeComponentManager<Object> serviceManager = _ribbonServiceManagers.get(ribbonManager);
241                    _ribbonServiceManagers.remove(ribbonManager);
242                    _ribbonControlsManager.unregisterRibbonManager(ribbonManager);
243                    _ribbonTabsManager.unregisterRibbonManager(ribbonManager);
244                    serviceManager.dispose();
245                }
246            }
247        }
248    }
249
250    /**
251     * Get the ribbon configuration managed by the RibbonManager
252     * @param ribbonManager The ribbon manager
253     * @return The configuration, or null if no configuration was cached
254     */
255    public RibbonConfiguration getCachedConfiguration(RibbonManager ribbonManager)
256    {
257        return _ribbonConfigurationCache.get(ribbonManager);
258    }
259    
260    /**
261     * Add a ribbon configuration to the cache
262     * @param ribbonManager The ribbon manager
263     * @param configuration The configuration
264     * @param importsValidity The list of imports for this ribbon configuration and their validity
265     */
266    public void addCachedConfiguration(RibbonManager ribbonManager, RibbonConfiguration configuration, Map<String, Long> importsValidity)
267    {
268        _ribbonConfigurationCache.put(ribbonManager, configuration);
269        _ribbonManagerCacheValidity.put(ribbonManager, importsValidity);
270    }
271
272    private Cache<String, RibbonManager> _getRibbonManagerCache() 
273    {
274        return this._cacheManager.get(ROLE);
275    }
276    
277}