001/*
002 *  Copyright 2010 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.web.service;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.regex.Pattern;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.configuration.Configurable;
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
036import org.apache.avalon.framework.context.Context;
037import org.apache.avalon.framework.context.ContextException;
038import org.apache.avalon.framework.context.Contextualizable;
039import org.apache.avalon.framework.logger.AbstractLogEnabled;
040import org.apache.avalon.framework.service.ServiceException;
041import org.apache.avalon.framework.service.ServiceManager;
042import org.apache.avalon.framework.service.Serviceable;
043import org.apache.cocoon.components.ContextHelper;
044import org.apache.commons.lang.StringUtils;
045import org.apache.excalibur.source.Source;
046import org.apache.excalibur.source.SourceResolver;
047import org.apache.excalibur.source.TraversableSource;
048
049import org.ametys.runtime.i18n.I18nizableText;
050import org.ametys.runtime.model.Enumerator;
051import org.ametys.runtime.plugin.component.PluginAware;
052import org.ametys.web.source.ServiceSourceFactory;
053
054/**
055 * This enuerator return the list of available files for services.
056 * The files are searched in the skin and in the plugin
057 * Configuration is
058 * <path> to specify the path where file are stored
059 * <file> an optionnal regexp to list files in the path (default is ^.*\.xsl$ to list all xsl files)
060 */
061public class ServiceXSLTEnumerator extends AbstractLogEnabled implements Enumerator<String>, org.ametys.runtime.parameter.Enumerator, Component, Configurable, PluginAware, Serviceable, Contextualizable
062{
063    /** The relative path to search */
064    protected String _path;
065    
066    /** The file pattern */
067    protected Pattern _fileFilter;
068    /** The plugin declaring the enumerator */
069    protected String _pluginName;
070    /** The feature declaring the enumerator */
071    protected String _featureName;
072    /** The excalibur source resolver */
073    protected SourceResolver _resolver;
074    /** The default value of the xsl to use. Can be null. */
075    protected String _defaultValue;
076    /** The plugin xsl to use. Can be empty. */
077    protected Set<String> _values;
078    /** The avalon context */
079    protected Context _context;
080    
081    @Override
082    public void configure(Configuration configuration) throws ConfigurationException
083    {
084        _defaultValue = configuration.getChild("default-value").getValue(null);
085        
086        Configuration enumConf = configuration.getChild("enumeration").getChild("custom-enumerator");
087        
088        _values = _getPluginOtherXSL (enumConf);
089        
090        _path = enumConf.getChild("path").getValue("");
091        if (StringUtils.isBlank(_path))
092        {
093            throw new ConfigurationException("The configuration of the path is mandatory", configuration);
094        }
095        
096        String regexp = enumConf.getChild("file").getValue("^.*\\.xsl$");
097        _fileFilter = Pattern.compile(regexp);
098    }
099    
100    @Override
101    public void contextualize(Context context) throws ContextException
102    {
103        _context = context;
104    }
105    
106    @Override
107    public void service(ServiceManager manager) throws ServiceException
108    {
109        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
110    }
111    
112    @Override
113    public void setPluginInfo(String pluginName, String featureName, String id)
114    {
115        _pluginName = pluginName;
116        _featureName = featureName;
117    }
118    
119    @Override
120    public Map<String, Object> getConfiguration()
121    {
122        Map<String, Object> config = new HashMap<>();
123        
124        config.put("defaultValue", _defaultValue);
125        config.put("values", _values);
126        config.put("path", _path);
127        config.put("fileFilter", _fileFilter.pattern());
128        
129        return config;
130    }
131    
132    /**
133     * Filter a list of sources to return thoses matching the _filter
134     * @param files A non null list of files to filter
135     * @return The non null filtered list of names
136     */
137    protected List<String> _filterNames(Collection<TraversableSource> files)
138    {
139        List<String> matched = new ArrayList<>();
140        
141        for (TraversableSource f : files)
142        {
143            if (!f.isCollection() && _fileFilter.matcher(f.getName()).matches())
144            {
145                matched.add(f.getName());
146            }
147        }
148        
149        return matched;
150    }
151    
152    public Map<String, I18nizableText> getTypedEntries() throws Exception
153    {
154        // 1 - Compute the list of files
155        Set<String> files = new HashSet<>();
156        
157        TraversableSource source = null;
158        try
159        {
160            source = (TraversableSource) _resolver.resolveURI("skin://services/" + _pluginName + "/" + _path);
161            if (source.exists())
162            {
163                if (source.isCollection())
164                {
165                    @SuppressWarnings("unchecked")
166                    List<String> sourceFiles = _filterNames(source.getChildren());
167                    files.addAll(sourceFiles);
168                }
169                else
170                {
171                    getLogger().warn("Can not enumerate files for service defined in '" + _pluginName + "/" + _featureName + "' on the path '" + _path + "', because it exists on the skin but is not a directory in the skin");
172                }
173            }
174        }
175        finally
176        {
177            _resolver.release(source);
178        }
179
180        if (_defaultValue != null)
181        {
182            Source pluginSource = null; 
183            try
184            {
185                pluginSource = _resolver.resolveURI("plugin:" + _pluginName + "://" + _defaultValue);
186                if (pluginSource.exists())
187                {
188                    int i = _defaultValue.lastIndexOf('/');
189                    String name = _defaultValue;
190                    if (i > 0)
191                    {
192                        name = _defaultValue.substring(i + 1);
193                    }
194                    files.add(name);
195                }
196            }
197            finally
198            {
199                _resolver.release(pluginSource);
200            }
201        }
202        
203        if (_values != null)
204        {
205            for (String value : _values)
206            {
207                Source pluginSource = null; 
208                try
209                {
210                    pluginSource = _resolver.resolveURI("plugin:" + _pluginName + "://" + value);
211                    if (pluginSource.exists())
212                    {
213                        int i = value.lastIndexOf('/');
214                        String name = value;
215                        if (i > 0)
216                        {
217                            name = value.substring(i + 1);
218                        }
219                        files.add(name);
220                    }
221                }
222                finally
223                {
224                    _resolver.release(pluginSource);
225                }
226            }
227        }
228
229        // 2 - Convert the files into entries
230        Map<String, I18nizableText> entries = new HashMap<>();
231        for (String filename : files)
232        {
233            String s = _path + "/" + filename;
234            
235            I18nizableText i18ntext = getEntry(s); 
236            if (i18ntext != null)
237            {
238                entries.put(s, i18ntext);
239            }
240        }
241        
242        return entries;
243    }
244    
245    @Override
246    // TODO NEWATTRIBUTEAPI: remove this method when org.ametys.runtime.parameter.Enumerator will be removed
247    public Map<Object, I18nizableText> getEntries() throws Exception
248    {
249        Map<Object, I18nizableText> result = new HashMap<>();
250        for (Map.Entry<String, I18nizableText> entry : getTypedEntries().entrySet())
251        {
252            result.put(entry.getKey(), entry.getValue());
253        }
254        return Collections.unmodifiableMap(result);
255    }
256    
257    @Override
258    public I18nizableText getEntry(String value) throws Exception
259    {
260        String valueWithNoExtension = value.substring(0, value.length() - 4);
261        String url = "service:" + _pluginName + "://" + valueWithNoExtension + ".xml";
262        
263        Source source = null;
264        try
265        {
266            source = _resolver.resolveURI(url);
267            if (source.exists())
268            {
269                String defaultCatalogue = "plugin." + _pluginName;
270                
271                if (source instanceof ServiceSourceFactory.ServiceSource)
272                {
273                    if (((ServiceSourceFactory.ServiceSource) source).getSourceType() != ServiceSourceFactory.SourceType.PLUGIN)
274                    {
275                        String skinName = (String) ContextHelper.getRequest(_context).getAttribute("skin");
276                        defaultCatalogue = "skin." + skinName;
277                    }
278                }
279                
280                try (InputStream inputStream = source.getInputStream())
281                {
282                    Configuration conf = new DefaultConfigurationBuilder().build(inputStream);
283                    
284                    if (!conf.getAttributeAsBoolean("exclude", false))
285                    {
286                        Configuration node = conf.getChild("label");
287                        return I18nizableText.parseI18nizableText(node, defaultCatalogue);
288                    }
289                    else
290                    {
291                        return null;
292                    }
293                }
294            }
295        }
296        catch (IOException e) 
297        {
298            // Nothing
299        }
300        finally
301        {
302            _resolver.release(source);
303        }
304        
305        int i = valueWithNoExtension.lastIndexOf('/');
306        String shortFilename = valueWithNoExtension.substring(i + 1);
307        return new I18nizableText(shortFilename);
308    }
309    
310    private Set<String> _getPluginOtherXSL (Configuration configuration)
311    {
312        Set<String> otherFiles = new HashSet<>();
313        
314        Configuration[] filesConf = configuration.getChild("values", true).getChildren("value");
315        for (Configuration fileConf : filesConf)
316        {
317            otherFiles.add(fileConf.getValue(null));
318        }
319        
320        return otherFiles;
321    }
322
323}