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 */
016
017package org.ametys.core.ui;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.function.Function;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.configuration.DefaultConfiguration;
031import org.apache.avalon.framework.configuration.MutableConfiguration;
032import org.apache.avalon.framework.service.ServiceException;
033
034import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
035
036/**
037 * Implementation of an ExtensionPoint for client side elements.
038 * @param <T> The client side element implementation
039 */
040public abstract class AbstractClientSideExtensionPoint<T extends ClientSideElement> extends AbstractThreadSafeComponentExtensionPoint<T>
041{
042    private Map<String, Configuration> _configurations = new HashMap<>();
043    private Map<String, String> _configurationPlugins = new HashMap<>();
044    
045    private List<AbstractClientSideExtensionPoint<T>> _registeredManagers = new ArrayList<>();
046    
047    private List<ReferencingExtension> _referencingExtensions = new ArrayList<>();
048    
049    private boolean _initialized;
050
051    @Override
052    public void addExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException
053    {
054        if (configuration.getAttribute("ref-id", null) != null)
055        {
056            if (_initialized)
057            {
058                _addReferencingExtension(id, pluginName, featureName, configuration);
059            }
060            else
061            {
062                _referencingExtensions.add(new ReferencingExtension(id, pluginName, featureName, configuration));
063            }
064        }
065        else
066        {
067            _configurations.put(id, configuration);
068            _configurationPlugins.put(id, pluginName);
069            super.addExtension(id, pluginName, featureName, configuration);
070        }
071    }
072    
073    private void _addReferencingExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException
074    {
075        try
076        {
077            String refId = configuration.getAttribute("ref-id");
078            String extensionPoint = configuration.getAttribute("point");
079            AbstractClientSideExtensionPoint clientSideElementManager = (AbstractClientSideExtensionPoint) _cocoonManager.lookup(extensionPoint);
080            Configuration baseConfiguration = clientSideElementManager._getConfiguration(refId);
081            if (baseConfiguration == null)
082            {
083                throw new ConfigurationException("Unknow ref-id '" + refId + "' for the extension point '" + extensionPoint + "'", configuration);
084            }
085            
086            DefaultConfiguration mergedConfiguration = new DefaultConfiguration(baseConfiguration, true);
087            _getMerdedChildsConfiguration(configuration, mergedConfiguration);
088            mergedConfiguration.setAttribute("ref-id", null);
089            
090            _configurations.put(id, mergedConfiguration);
091            _configurationPlugins.put(id, pluginName);
092            super.addExtension(id, pluginName, featureName, mergedConfiguration);
093        }
094        catch (ServiceException e)
095        {
096            throw new ConfigurationException("Invalid id referenced by the attribute 'ref-id'", configuration);
097        }
098    }
099    
100    private Configuration _getConfiguration(String id) throws ConfigurationException
101    {
102        Configuration configuration = _configurations.get(id);
103        if (configuration != null)
104        {
105            return _getContexutalizedConfiguration(configuration, _configurationPlugins.get(id));
106        }
107        
108        for (AbstractClientSideExtensionPoint manager : _registeredManagers)
109        {
110            configuration = manager._getConfiguration(id);
111            if (configuration != null)
112            {
113                return configuration;
114            }
115        }
116        
117        return null;
118    }
119    
120    private Configuration _getContexutalizedConfiguration(Configuration configuration, String pluginName) throws ConfigurationException
121    {
122        DefaultConfiguration contextualizedConfiguration = new DefaultConfiguration(configuration);
123        
124        _contexutalizeConfiguration(contextualizedConfiguration, pluginName);
125        
126        return contextualizedConfiguration;
127    }
128    
129    private void _contexutalizeConfiguration(MutableConfiguration configuration, String pluginName) throws ConfigurationException
130    {
131        for (MutableConfiguration child : configuration.getMutableChildren())
132        {
133            if ((child.getName().equals("file") || child.getAttributeAsBoolean("file", false) || "file".equals(child.getAttribute("type", null))) && child.getAttribute("plugin", null) == null)
134            {
135                child.setAttribute("plugin", pluginName);
136            }
137            else if (("true".equals(child.getAttribute("i18n", "false")) || "i18n".equals(child.getAttribute("type", null))) && child.getValue().indexOf(":") < 0 && child.getValue().length() > 0)
138            {
139                child.setValue("plugin." + pluginName + ":" + child.getValue());
140            }
141            _contexutalizeConfiguration(child, pluginName);
142        }
143    }
144
145    private void _getMerdedChildsConfiguration(Configuration configuration, MutableConfiguration base) throws ConfigurationException
146    {
147        base.addAllAttributes(configuration);
148        for (Configuration child : configuration.getChildren())
149        {
150            String tagName = child.getName();
151            MutableConfiguration baseChild = base.getMutableChild(tagName);
152            if ("scripts".equals(tagName) || "css".equals(tagName))
153            {
154                for (Configuration fileChild : child.getChildren())
155                {
156                    baseChild.addChild(fileChild);
157                }
158            }
159            else
160            {
161                _mergeChildsConfiguration(child, baseChild);
162            }
163        }
164    }
165    
166    private void _mergeChildsConfiguration(Configuration configuration, MutableConfiguration base) throws ConfigurationException
167    {
168        String value = configuration.getValue(null);
169        if (value != null)
170        {
171            base.setValue(value);
172        }
173        
174        base.addAllAttributes(configuration);
175        
176        Set<String> childrenToProcess = Arrays.stream(configuration.getChildren())
177                                              .map(Configuration::getName)
178                                              .collect(Collectors.toSet());
179        for (String childName : childrenToProcess)
180        {
181            MutableConfiguration[] baseChildren = base.getMutableChildren(childName);
182            Configuration[] newChildren = configuration.getChildren(childName);
183            
184            if (baseChildren.length == newChildren.length)
185            {
186                for (int i = 0; i < baseChildren.length; i++)
187                {
188                    _mergeChildsConfiguration(newChildren[i], baseChildren[i]);
189                }
190            }
191            else
192            {
193                for (Configuration baseChild : baseChildren)
194                {
195                    base.removeChild(baseChild);
196                }
197                
198                for (Configuration newChild : newChildren)
199                {
200                    base.addChild(newChild);
201                }
202            }
203        }
204    }
205    
206    @Override
207    public void initializeExtensions() throws Exception
208    {
209        Map<String, ReferencingExtension> refExtIds = _referencingExtensions.stream().collect(Collectors.toMap(ReferencingExtension::getId, Function.identity()));
210        List<String> processing = new ArrayList<>();
211        for (ReferencingExtension refExt : _referencingExtensions)
212        {
213            _lazyInitializeReferencingExtension(refExt, refExtIds, processing);
214        }
215        
216        super.initializeExtensions();
217        _initialized = true;
218    }
219    
220    private void _lazyInitializeReferencingExtension(ReferencingExtension ext, Map<String, ReferencingExtension> refExtIds, List<String> processing) throws ConfigurationException
221    {
222        if (!processing.contains(ext.getId()))
223        {
224            processing.add(ext.getId());
225            if (refExtIds.containsKey(ext.getRefId()))
226            {
227                // if we are referencing another referencing extension, make sure it is initialized before
228                _lazyInitializeReferencingExtension(refExtIds.get(ext.getRefId()), refExtIds, processing);
229            }
230
231            _addReferencingExtension(ext.getId(), ext.getPluginName(), ext.getFeatureName(), ext.getConfiguration());
232        }
233    }
234    
235    /**
236     * Register a new ribbon manager whose extensions will also be managed by this RibbonControlsManager
237     * @param manager The manager to register
238     */
239    public void registerRibbonManager(AbstractClientSideExtensionPoint<T> manager)
240    {
241        _registeredManagers.add(manager);
242    }
243    
244    /**
245     * Remove a previously registered ribbon manager
246     * @param manager The manager to remove
247     */
248    public void unregisterRibbonManager(AbstractClientSideExtensionPoint<T> manager)
249    {
250        _registeredManagers.remove(manager);
251    }
252    
253    @Override
254    public T getExtension(String id)
255    {
256        T extension = super.getExtension(id);
257        if (extension == null)
258        {
259            for (AbstractClientSideExtensionPoint<T> manager : _registeredManagers)
260            {
261                extension = manager.getExtension(id);
262                if (extension != null)
263                {
264                    return extension;
265                }
266            }
267        }
268        
269        return extension;
270    }
271    
272    @Override
273    public Set<String> getExtensionsIds()
274    {
275        Set<String> extensionsIds = super.getExtensionsIds();
276        for (AbstractClientSideExtensionPoint<T> manager : _registeredManagers)
277        {
278            extensionsIds.addAll(manager.getExtensionsIds());
279        }
280        return extensionsIds;
281    }
282
283    /**
284     * Find a dependency of this manager from the Client side elements it knows. 
285     * @param pattern The matching pattern to find the dependency.
286     * @return The dependency, or null if no Client side element matched.
287     */
288    public List<ClientSideElement> findDependency(String pattern)
289    {
290        ClientSideElement extension = getExtension(pattern);
291        
292        if (extension == null)
293        {
294            throw new IllegalArgumentException("Unable to find dependency with id : " + pattern + ".");
295        }
296        
297        List<ClientSideElement> result = new ArrayList<>();
298        result.add(extension);
299        return result;
300    }
301    
302    private class ReferencingExtension
303    {
304        private String _id;
305        private String _pluginName;
306        private String _featureName;
307        private Configuration _configuration;
308        private String _refId;
309
310        public ReferencingExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException
311        {
312            _id = id;
313            _pluginName = pluginName;
314            _featureName = featureName;
315            _configuration = configuration;
316            
317            _refId = configuration.getAttribute("ref-id");
318        }
319
320        /**
321         * Return the id
322         * @return the id
323         */
324        public String getId()
325        {
326            return _id;
327        }
328
329        /**
330         * Return the plugin name
331         * @return the plugin name
332         */
333        public String getPluginName()
334        {
335            return _pluginName;
336        }
337
338        /**
339         * Return the feature name
340         * @return the feature name
341         */
342        public String getFeatureName()
343        {
344            return _featureName;
345        }
346
347        /**
348         * Return the configuration
349         * @return the configuration
350         */
351        public Configuration getConfiguration()
352        {
353            return _configuration;
354        }
355        
356        /**
357         * Get the referenced extension id
358         * @return The ref-id
359         */
360        public String getRefId()
361        {
362            return _refId;
363        }
364    }
365}