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;
034import org.ametys.core.cache.AbstractCacheManager;
035import org.ametys.core.cache.AbstractCacheManager.CacheType;
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.createCache(ROLE, 
086                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_RIBBON_MANAGER_LABEL"),
087                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_RIBBON_MANAGER_DESCRIPTION"),
088                CacheType.MEMORY,
089                false);
090    }
091    
092    /**
093     * Create the RibbonManager associated with the given ribbon file. 
094     * If the RibbonManager already exists for the ribbon file and is still valid, it is simply returned.  
095     * @param uri The ribbon configuration uri
096     * @return The RibbonManager
097     * @throws Exception If an error occurs
098     */
099    public synchronized RibbonManager getManager(String uri) throws Exception
100    {
101        RibbonManager ribbonManager = _getRibbonManagerCache().get(uri);
102        if (ribbonManager != null)
103        {
104            if (_isRibbonManagerStillValid(ribbonManager))
105            {
106                _increaseUsage(ribbonManager);
107                return ribbonManager;
108            }
109            else
110            {
111                _ribbonManagerCacheValidity.remove(ribbonManager);
112                _decreaseUsage(ribbonManager, true);
113            }
114        }
115        
116        ribbonManager = _createRibbonManager();
117        _getRibbonManagerCache().put(uri, ribbonManager);
118        _ribbonManagerCacheValidity.put(ribbonManager, null);
119        _increaseUsage(ribbonManager);
120        
121        return ribbonManager;
122    }
123
124    private boolean _isRibbonManagerStillValid(RibbonManager ribbonManager)
125    {
126        Map<String, Long> validity = _ribbonManagerCacheValidity.get(ribbonManager);
127        if (validity == null)
128        {
129            return false;
130        }
131        
132        for (Entry<String, Long> entry : validity.entrySet())
133        {
134            RibbonConfigurationSource ribbonConfigurationEntry = null;
135            try
136            {
137                ribbonConfigurationEntry = RibbonConfigurationSource.createFromUri(entry.getKey(), _resolver);
138                if (!_isRibbonEntryStillValid(entry.getValue(), ribbonConfigurationEntry))
139                {
140                    return false;
141                }
142            }
143            catch (IOException e)
144            {
145                // invalid import, uri is out of date
146                return false;
147            }
148            finally
149            {
150                if (ribbonConfigurationEntry != null)
151                {
152                    _resolver.release(ribbonConfigurationEntry.getSource());
153                }
154            }
155        }
156        
157        return true;
158    }
159    
160    private boolean _isRibbonEntryStillValid(Long cachedValidity, RibbonConfigurationSource ribbonEntrySource)
161    {
162        boolean entryExists = ribbonEntrySource.getSource().exists();
163        if (cachedValidity == 0 && entryExists)
164        {
165            // Unable to get "Last Modified" of Entry. Assume changed.
166            return false;
167        }
168        
169        if (cachedValidity > 0 && !entryExists)
170        {
171            // Entry was deleted since it was put in cache.
172            return false;
173        }
174        
175        // Compare the last modified to the cached "last modified" value, to check for changes
176        return ribbonEntrySource.getLastModified().equals(cachedValidity);
177    }
178
179    private RibbonManager _createRibbonManager() throws Exception
180    {
181        PluginsComponentManager ribbonServiceManager = new PluginsComponentManager(new WrapperComponentManager(_cocoonManager));
182        ribbonServiceManager.contextualize(_context);
183        ribbonServiceManager.service(_cocoonManager);
184        ribbonServiceManager.setLogger(getLogger());
185        ribbonServiceManager.addExtensionPoint("core-ui", RibbonManager.ROLE, RibbonManager.class, null, new ArrayList<>());
186        ribbonServiceManager.initialize();
187        
188        RibbonManager ribbonManager = (RibbonManager) ribbonServiceManager.lookup(RibbonManager.ROLE);
189        ribbonManager.initialize();
190
191        _ribbonServiceManagers.put(ribbonManager, ribbonServiceManager);
192        
193        _ribbonControlsManager.registerRibbonManager(ribbonManager);
194        _ribbonTabsManager.registerRibbonManager(ribbonManager);
195        return ribbonManager;
196    }
197    
198    /**
199     * Dispose of a RibbonManager that was previously retrieve with this helper
200     * @param ribbonManager The ribbon manager
201     */
202    public void dispose(RibbonManager ribbonManager)
203    {
204        if (ribbonManager != null)
205        {
206            _decreaseUsage(ribbonManager, false);
207        }
208    }
209
210    private synchronized void _increaseUsage(RibbonManager ribbonManager)
211    {
212        Integer usage = _ribbonManagerUsageCache.get(ribbonManager);
213        _ribbonManagerUsageCache.put(ribbonManager, usage != null ? usage + 1 : 1);
214    }
215    
216    @SuppressWarnings("null")
217    private synchronized void _decreaseUsage(RibbonManager ribbonManager, boolean forceRemove)
218    {
219        Integer usage = _ribbonManagerUsageCache.get(ribbonManager);
220        if (forceRemove || usage != null)
221        {
222            if (!forceRemove && usage > 1)
223            {
224                _ribbonManagerUsageCache.put(ribbonManager, usage - 1);
225            }
226            else
227            {
228                _ribbonManagerUsageCache.remove(ribbonManager);
229                if (!_ribbonManagerCacheValidity.containsKey(ribbonManager))
230                {
231                    _ribbonConfigurationCache.remove(ribbonManager);
232                    ThreadSafeComponentManager<Object> serviceManager = _ribbonServiceManagers.get(ribbonManager);
233                    _ribbonServiceManagers.remove(ribbonManager);
234                    _ribbonControlsManager.unregisterRibbonManager(ribbonManager);
235                    _ribbonTabsManager.unregisterRibbonManager(ribbonManager);
236                    serviceManager.dispose();
237                }
238            }
239        }
240    }
241
242    /**
243     * Get the ribbon configuration managed by the RibbonManager
244     * @param ribbonManager The ribbon manager
245     * @return The configuration, or null if no configuration was cached
246     */
247    public RibbonConfiguration getCachedConfiguration(RibbonManager ribbonManager)
248    {
249        return _ribbonConfigurationCache.get(ribbonManager);
250    }
251    
252    /**
253     * Add a ribbon configuration to the cache
254     * @param ribbonManager The ribbon manager
255     * @param configuration The configuration
256     * @param importsValidity The list of imports for this ribbon configuration and their validity
257     */
258    public void addCachedConfiguration(RibbonManager ribbonManager, RibbonConfiguration configuration, Map<String, Long> importsValidity)
259    {
260        _ribbonConfigurationCache.put(ribbonManager, configuration);
261        _ribbonManagerCacheValidity.put(ribbonManager, importsValidity);
262    }
263
264    private Cache<String, RibbonManager> _getRibbonManagerCache() 
265    {
266        return this._cacheManager.get(ROLE);
267    }
268    
269}