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.cocoon.environment.Request;
045import org.apache.commons.lang.StringUtils;
046import org.apache.excalibur.source.Source;
047import org.apache.excalibur.source.SourceResolver;
048import org.apache.excalibur.source.TraversableSource;
049
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.runtime.i18n.I18nizableText;
052import org.ametys.runtime.model.Enumerator;
053import org.ametys.runtime.plugin.component.PluginAware;
054import org.ametys.web.WebConstants;
055import org.ametys.web.repository.page.ServicesAssignmentHandler;
056import org.ametys.web.repository.page.SitemapElement;
057import org.ametys.web.repository.page.ZoneItem;
058import org.ametys.web.source.ServiceSourceFactory;
059
060/**
061 * This enuerator return the list of available files for services.
062 * The files are searched in the skin and in the plugin
063 * Configuration is
064 * <path> to specify the path where file are stored
065 * <file> an optionnal regexp to list files in the path (default is ^.*\.xsl$ to list all xsl files)
066 */
067public class ServiceXSLTEnumerator extends AbstractLogEnabled implements Enumerator<String>, org.ametys.runtime.parameter.Enumerator, Component, Configurable, PluginAware, Serviceable, Contextualizable
068{
069    /** The relative path to search */
070    protected String _path;
071    
072    /** The file pattern */
073    protected Pattern _fileFilter;
074    /** The plugin declaring the enumerator */
075    protected String _pluginName;
076    /** The feature declaring the enumerator */
077    protected String _featureName;
078    /** The excalibur source resolver */
079    protected SourceResolver _resolver;
080    /** The default value of the xsl to use. Can be null. */
081    protected String _defaultValue;
082    /** The plugin xsl to use. Can be empty. */
083    protected Set<String> _values;
084    /** The avalon context */
085    protected Context _context;
086    /** The service assignement handler instance */
087    protected ServicesAssignmentHandler _servicesAssignmentHandler;
088    /** The ametys object resolver instance */
089    protected AmetysObjectResolver _ametysObjectResolver;
090    
091    @Override
092    public void configure(Configuration configuration) throws ConfigurationException
093    {
094        _defaultValue = configuration.getChild("default-value").getValue(null);
095        
096        Configuration enumConf = configuration.getChild("enumeration").getChild("custom-enumerator");
097        
098        _values = _getPluginOtherXSL (enumConf);
099        
100        _path = enumConf.getChild("path").getValue("");
101        if (StringUtils.isBlank(_path))
102        {
103            throw new ConfigurationException("The configuration of the path is mandatory", configuration);
104        }
105        
106        String regexp = enumConf.getChild("file").getValue("^.*\\.xsl$");
107        _fileFilter = Pattern.compile(regexp);
108    }
109    
110    @Override
111    public void contextualize(Context context) throws ContextException
112    {
113        _context = context;
114    }
115    
116    @Override
117    public void service(ServiceManager manager) throws ServiceException
118    {
119        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
120        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
121        _servicesAssignmentHandler = (ServicesAssignmentHandler) manager.lookup(ServicesAssignmentHandler.ROLE);
122    }
123    
124    @Override
125    public void setPluginInfo(String pluginName, String featureName, String id)
126    {
127        _pluginName = pluginName;
128        _featureName = featureName;
129    }
130    
131    @Override
132    public Map<String, Object> getConfiguration()
133    {
134        Map<String, Object> config = new HashMap<>();
135        
136        config.put("defaultValue", _defaultValue);
137        config.put("values", _values);
138        config.put("path", _path);
139        config.put("fileFilter", _fileFilter.pattern());
140        
141        return config;
142    }
143    
144    /**
145     * Filter a list of sources to return thoses matching the _filter
146     * @param files A non null list of files to filter
147     * @return The non null filtered list of names
148     */
149    protected List<String> _filterNames(Collection<TraversableSource> files)
150    {
151        List<String> matched = new ArrayList<>();
152        
153        for (TraversableSource f : files)
154        {
155            if (!f.isCollection() && _fileFilter.matcher(f.getName()).matches())
156            {
157                matched.add(f.getName());
158            }
159        }
160        
161        return matched;
162    }
163    
164    public Map<String, I18nizableText> getTypedEntries() throws Exception
165    {
166        // 1 - Compute the list of files
167        Set<String> files = new HashSet<>();
168        
169        TraversableSource source = null;
170        try
171        {
172            source = (TraversableSource) _resolver.resolveURI("skin://services/" + _pluginName + "/" + _path);
173            if (source.exists())
174            {
175                if (source.isCollection())
176                {
177                    @SuppressWarnings("unchecked")
178                    List<String> sourceFiles = _filterNames(source.getChildren());
179                    files.addAll(sourceFiles);
180                }
181                else
182                {
183                    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");
184                }
185            }
186        }
187        finally
188        {
189            _resolver.release(source);
190        }
191
192        if (_defaultValue != null)
193        {
194            Source pluginSource = null;
195            try
196            {
197                pluginSource = _resolver.resolveURI("plugin:" + _pluginName + "://" + _defaultValue);
198                if (pluginSource.exists())
199                {
200                    int i = _defaultValue.lastIndexOf('/');
201                    String name = _defaultValue;
202                    if (i > 0)
203                    {
204                        name = _defaultValue.substring(i + 1);
205                    }
206                    files.add(name);
207                }
208            }
209            finally
210            {
211                _resolver.release(pluginSource);
212            }
213        }
214        
215        if (_values != null)
216        {
217            for (String value : _values)
218            {
219                Source pluginSource = null;
220                try
221                {
222                    pluginSource = _resolver.resolveURI("plugin:" + _pluginName + "://" + value);
223                    if (pluginSource.exists())
224                    {
225                        int i = value.lastIndexOf('/');
226                        String name = value;
227                        if (i > 0)
228                        {
229                            name = value.substring(i + 1);
230                        }
231                        files.add(name);
232                    }
233                }
234                finally
235                {
236                    _resolver.release(pluginSource);
237                }
238            }
239        }
240        
241        // 2 - Limit to authorized values
242        Request request = ContextHelper.getRequest(_context);
243        
244        String pageId = (String) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE_ID);
245        if (StringUtils.isNotBlank(pageId)) // Specific places like forms preview, may need the enumerator with no restriction possible
246        {
247            SitemapElement page = _ametysObjectResolver.resolveById(pageId);
248            
249            String zoneName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_ZONE_NAME);
250            if (zoneName == null)
251            {
252                String zoneItemId = (String) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM_ID);
253                ZoneItem zoneItem = _ametysObjectResolver.resolveById(zoneItemId);
254                zoneName = zoneItem.getZone().getName();
255            }
256            
257            String serviceId = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SERVICE_ID);
258            files = _servicesAssignmentHandler.limitAvailableServiceViews(files, page, zoneName, serviceId);
259        }
260        
261        // 3 - Convert the files into entries
262        Map<String, I18nizableText> entries = new HashMap<>();
263        for (String filename : files)
264        {
265            String s = _path + "/" + filename;
266            
267            I18nizableText i18ntext = getEntry(s);
268            if (i18ntext != null)
269            {
270                entries.put(s, i18ntext);
271            }
272        }
273        
274        return entries;
275    }
276    
277    @Override
278    // TODO NEWATTRIBUTEAPI: remove this method when org.ametys.runtime.parameter.Enumerator will be removed
279    public Map<Object, I18nizableText> getEntries() throws Exception
280    {
281        Map<Object, I18nizableText> result = new HashMap<>();
282        for (Map.Entry<String, I18nizableText> entry : getTypedEntries().entrySet())
283        {
284            result.put(entry.getKey(), entry.getValue());
285        }
286        return Collections.unmodifiableMap(result);
287    }
288    
289    @Override
290    public I18nizableText getEntry(String value) throws Exception
291    {
292        String valueWithNoExtension = value.substring(0, value.length() - 4);
293        String url = "service:" + _pluginName + "://" + valueWithNoExtension + ".xml";
294        
295        Source source = null;
296        try
297        {
298            source = _resolver.resolveURI(url);
299            if (source.exists())
300            {
301                String defaultCatalogue = "plugin." + _pluginName;
302                
303                if (source instanceof ServiceSourceFactory.ServiceSource)
304                {
305                    if (((ServiceSourceFactory.ServiceSource) source).getSourceType() != ServiceSourceFactory.SourceType.PLUGIN)
306                    {
307                        String skinName = (String) ContextHelper.getRequest(_context).getAttribute("skin");
308                        defaultCatalogue = "skin." + skinName;
309                    }
310                }
311                
312                try (InputStream inputStream = source.getInputStream())
313                {
314                    Configuration conf = new DefaultConfigurationBuilder().build(inputStream);
315                    
316                    if (!conf.getAttributeAsBoolean("exclude", false))
317                    {
318                        getLogger().warn("The 'exclude' attribute in service view definition file (" + url + ") is obsolete. Consider using the services-views-xxx.xml file : search for \"services-views-default.xml\" in the Ametys documentation to know more about it.");
319                        
320                        Configuration node = conf.getChild("label", false);
321                        if (node != null)
322                        {
323                            return I18nizableText.parseI18nizableText(node, defaultCatalogue);
324                        }
325                        else
326                        {
327                            // No label defined
328                            int i = valueWithNoExtension.lastIndexOf('/');
329                            String shortFilename = valueWithNoExtension.substring(i + 1);
330                            return new I18nizableText(shortFilename);
331                        }
332                    }
333                    else
334                    {
335                        return null;
336                    }
337                }
338            }
339        }
340        catch (IOException e)
341        {
342            // Nothing
343        }
344        finally
345        {
346            _resolver.release(source);
347        }
348        
349        int i = valueWithNoExtension.lastIndexOf('/');
350        String shortFilename = valueWithNoExtension.substring(i + 1);
351        return new I18nizableText(shortFilename);
352    }
353    
354    private Set<String> _getPluginOtherXSL (Configuration configuration)
355    {
356        Set<String> otherFiles = new HashSet<>();
357        
358        Configuration[] filesConf = configuration.getChild("values", true).getChildren("value");
359        for (Configuration fileConf : filesConf)
360        {
361            otherFiles.add(fileConf.getValue(null));
362        }
363        
364        return otherFiles;
365    }
366
367}