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