001/*
002 *  Copyright 2015 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.web.skin.actions;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Comparator;
024import java.util.HashMap;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.TreeMap;
030
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
034import org.apache.avalon.framework.context.Context;
035import org.apache.avalon.framework.context.ContextException;
036import org.apache.avalon.framework.context.Contextualizable;
037import org.apache.avalon.framework.parameters.Parameters;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.cocoon.ProcessingException;
041import org.apache.cocoon.acting.ServiceableAction;
042import org.apache.cocoon.environment.ObjectModelHelper;
043import org.apache.cocoon.environment.Redirector;
044import org.apache.cocoon.environment.Request;
045import org.apache.cocoon.environment.SourceResolver;
046import org.apache.commons.io.FileUtils;
047import org.apache.commons.lang3.StringUtils;
048import org.apache.excalibur.xml.sax.SAXParser;
049import org.slf4j.LoggerFactory;
050import org.xml.sax.Attributes;
051import org.xml.sax.InputSource;
052import org.xml.sax.SAXException;
053import org.xml.sax.helpers.DefaultHandler;
054
055import org.ametys.core.cocoon.JSonReader;
056import org.ametys.runtime.i18n.I18nizableText;
057import org.ametys.runtime.parameter.AbstractParameterParser;
058import org.ametys.runtime.parameter.Enumerator;
059import org.ametys.runtime.parameter.ParameterHelper;
060import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
061import org.ametys.runtime.parameter.Validator;
062import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
063import org.ametys.runtime.util.AmetysHomeHelper;
064import org.ametys.web.skin.Skin;
065import org.ametys.web.skin.SkinParameter;
066import org.ametys.web.skin.SkinsManager;
067
068/**
069 * Get the skin configuration parameters and their values
070 */
071public class SkinConfigurationAction extends ServiceableAction implements Contextualizable
072{
073    private SkinsManager _skinsManager;
074    private SAXParser _saxParser;
075    private Context _context;
076    
077    @Override
078    public void contextualize(Context context) throws ContextException
079    {
080        _context = context;
081    }
082    
083    @Override
084    public void service(ServiceManager serviceManager) throws ServiceException
085    {
086        super.service(serviceManager);
087        _skinsManager = (SkinsManager) serviceManager.lookup(SkinsManager.ROLE);
088        _saxParser = (SAXParser) serviceManager.lookup(SAXParser.ROLE);
089    }
090    
091    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
092    {
093        Request request = ObjectModelHelper.getRequest(objectModel);
094        
095        String skinName = request.getParameter("skinName");
096        String tempDir = request.getParameter("tempDir");
097        String skinDirName = request.getParameter("skinDir");
098        
099        File skinDir = null;
100        if (StringUtils.isEmpty(tempDir))
101        {
102            Skin skin = _skinsManager.getSkin(skinName);
103            skinDir = new File(skin.getLocation());
104        }
105        else
106        {
107            File ametysTmpDir = AmetysHomeHelper.getAmetysHomeTmp();
108            skinDir = FileUtils.getFile(ametysTmpDir, tempDir.replace('/', File.separatorChar), skinDirName);
109        }
110        
111        Map<String, Object> result = new HashMap<>();
112        result.put("parameters", _parameters2json(skinDir));
113        result.put("values", _values2json(skinDir));
114        
115        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
116        return EMPTY_MAP;
117    }
118
119    private Map<String, Object> _values2json (File skinDir)
120    {
121        Map<String, Object> form = new HashMap<>();
122        Map<String, Object> values = new HashMap<>();
123        
124        File configFile = new File(skinDir, "stylesheets" + File.separator + "config" + File.separator + "config.xsl");
125        if (configFile.exists())
126        {
127            try (FileInputStream is = new FileInputStream(new File(skinDir, "stylesheets" + File.separator + "config" + File.separator + "config.xsl")))
128            {
129                SkinConfigHandler handler = new SkinConfigHandler();
130                _saxParser.parse(new InputSource(is), handler);
131                
132                values.putAll(handler.getVariables());
133            }
134            catch (SAXException | IOException e)
135            {
136                getLogger().error("Failed to read skin's parameters values.", e);
137            }
138        }
139        
140        form.put("values", values);
141        return form;
142    }
143    
144    private Map<String, Object> _parameters2json (File skinDir) throws ProcessingException
145    {
146        Map<String, Object> parameters = new HashMap<>();
147        
148        List<Map<String, Object>> tabs = new ArrayList<>();
149        
150        Map<I18nizableText, Map<I18nizableText, List<SkinParameter>>> skinParameters = _getCategorizedParameters(skinDir);
151        
152        for (Entry<I18nizableText, Map<I18nizableText, List<SkinParameter>>> categoryEntry : skinParameters.entrySet())
153        {
154            Map<String, Object> tab = new HashMap<>();
155            tab.put("role", "tab");
156            tab.put("label", categoryEntry.getKey());
157            
158            Map<String, Object> tabElmts = new HashMap<>();
159            
160            List<Map<String, Object>> fieldsets = new ArrayList<>();
161            Map<I18nizableText, List<SkinParameter>> groups = categoryEntry.getValue();
162            for (Entry<I18nizableText, List<SkinParameter>> groupEntry : groups.entrySet())
163            {
164                Map<String, Object> fieldset = new HashMap<>();
165                fieldset.put("role", "fieldset");
166                fieldset.put("label", groupEntry.getKey());
167                fieldset.put("elements", _parameters2JsonObject(groupEntry.getValue()));
168                
169                fieldsets.add(fieldset);
170            }
171            
172            tabElmts.put("fieldsets", fieldsets);
173            tab.put("elements", tabElmts);
174            
175            tabs.add(tab);
176        }
177        
178        parameters.put("fieldsets", tabs);
179        
180        return parameters;
181    }
182    
183    private List<SkinParameter> _getParameters(File skinDir) throws ProcessingException
184    {
185        List<SkinParameter> skinParams = new ArrayList<>();
186        
187        ThreadSafeComponentManager<Validator> validatorManager = new ThreadSafeComponentManager<>();
188        ThreadSafeComponentManager<Enumerator> enumeratorManager = new ThreadSafeComponentManager<>();
189        
190        try
191        {
192            validatorManager.setLogger(LoggerFactory.getLogger(getClass()));
193            validatorManager.contextualize(_context);
194            validatorManager.service(manager);
195            
196            enumeratorManager.setLogger(LoggerFactory.getLogger(getClass()));
197            enumeratorManager.contextualize(_context);
198            enumeratorManager.service(manager);
199            
200            SkinParameterParser parser = new SkinParameterParser(enumeratorManager, validatorManager, new File (skinDir, "i18n").toURI().toString());
201                
202            Configuration configuration = _getConfigurationModel(skinDir);
203            
204            if (configuration != null)
205            {
206                Configuration[] parameterConfigs = configuration.getChild("parameters").getChildren("parameter");
207                for (Configuration parameterConfig : parameterConfigs)
208                {
209                    SkinParameter parameter = parser.parseParameter(manager, "tempskin", parameterConfig);
210                    skinParams.add(parameter);
211                }
212            }
213            
214            return skinParams;
215        }
216        catch (ConfigurationException | SAXException | IOException e)
217        {
218            throw new ProcessingException(e);
219        }
220    }
221    
222    private Map<I18nizableText, Map<I18nizableText, List<SkinParameter>>> _getCategorizedParameters(File skinDir) throws ProcessingException
223    {
224        return _categorize(_getParameters(skinDir));
225    }
226           
227    
228    private Map<String, Object> _parameters2JsonObject (List<SkinParameter> skinParameters) throws ProcessingException
229    {
230        Map<String, Object> jsonObject = new LinkedHashMap<>();
231        
232        for (SkinParameter skinParameter : skinParameters)
233        {
234            jsonObject.put(skinParameter.getId(), _parameter2JsonObject(skinParameter));
235        }
236        
237        return jsonObject;
238    }
239    
240    private Map<String, Object> _parameter2JsonObject (SkinParameter skinParameter) throws ProcessingException
241    {
242        Map<String, Object> param = new HashMap<>();
243        
244        param.put("label", skinParameter.getLabel());
245        param.put("description", skinParameter.getDescription());
246        param.put("type", skinParameter.getType());
247        
248        Validator validator = skinParameter.getValidator();
249        if (validator != null)
250        {
251            param.put("validation", validator.toJson());
252        }
253        
254        String widget = skinParameter.getWidget();
255        if (widget != null)
256        {
257            param.put("widget", widget);
258        }
259        
260        Map<String, I18nizableText> widgetParameters = skinParameter.getWidgetParameters();
261        if (widgetParameters != null && widgetParameters.size() > 0)
262        {
263            param.put("widget-params", skinParameter.getWidgetParameters());
264        }
265        
266        Object defaultValue = skinParameter.getDefaultValue();
267        if (defaultValue != null)
268        {
269            param.put("default-value", skinParameter.getDefaultValue());
270        }
271
272        Enumerator enumerator = skinParameter.getEnumerator();
273        
274        if (enumerator != null)
275        {
276            try
277            {
278                List<Map<String, Object>> options = new ArrayList<>();
279                
280                for (Map.Entry<Object, I18nizableText> entry : enumerator.getEntries().entrySet())
281                {
282                    String valueAsString = ParameterHelper.valueToString(entry.getKey());
283                    I18nizableText entryLabel = entry.getValue();
284                    
285                    Map<String, Object> option = new HashMap<>();
286                    option.put("label", entryLabel != null ? entryLabel : valueAsString);
287                    option.put("value", valueAsString);
288                    options.add(option);
289                }
290                
291                param.put("enumeration", options);
292            }
293            catch (Exception e)
294            {
295                throw new ProcessingException("Unable to enumerate entries with enumerator: " + enumerator, e);
296            }
297        }
298        
299        return param;
300    }
301    
302    private Configuration _getConfigurationModel(File skinDir) throws ConfigurationException, SAXException, IOException
303    {
304        File configFile = new File(skinDir, "stylesheets" + File.separator + "config" + File.separator + "config-model.xml");
305        
306        if (configFile.exists())
307        {
308            try (FileInputStream is = new FileInputStream(configFile))
309            {
310                DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
311                return builder.build(is);
312            }
313        }
314
315        // There is no parameter
316        return null;
317    }
318    
319    /**
320     * Organize a collection of skin parameters by categories and groups.
321     * @param parameters a collection of skin parameters.
322     * @return a Map of parameters sorted first by category then group.
323     */
324    protected Map<I18nizableText, Map<I18nizableText, List<SkinParameter>>> _categorize(Collection<SkinParameter> parameters)
325    {
326        Map<I18nizableText, Map<I18nizableText, List<SkinParameter>>> categories = new HashMap<>();
327
328        // Classify parameters by groups and categories
329        for (SkinParameter parameter : parameters)
330        {
331            I18nizableText categoryName = parameter.getDisplayCategory();
332            I18nizableText groupName = parameter.getDisplayGroup();
333
334            // Get the map of groups of the category
335            Map<I18nizableText, List<SkinParameter>> category = categories.get(categoryName);
336            if (category == null)
337            {
338                category = new TreeMap<>(new I18nizableTextComparator());
339                categories.put(categoryName, category);
340            }
341
342            // Get the map of parameters of the group
343            List<SkinParameter> group = category.get(groupName);
344            if (group == null)
345            {
346                group = new ArrayList<>();
347                category.put(groupName, group);
348            }
349
350            group.add(parameter);
351        }
352        
353        return categories;
354    }
355    
356    class I18nizableTextComparator implements Comparator<I18nizableText>
357    {
358        @Override
359        public int compare(I18nizableText t1, I18nizableText t2)
360        {
361            return t1.toString().compareTo(t2.toString());
362        }
363    }
364    
365    /**
366     * Parser for {@link SkinParameter} parameter.
367     */
368    class SkinParameterParser extends AbstractParameterParser<SkinParameter, ParameterType>
369    {
370        private String _i18nCatalogueLocation;
371        
372        public SkinParameterParser(ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager, String i18nCatalogueLocation)
373        {
374            super(enumeratorManager, validatorManager);
375            _i18nCatalogueLocation = i18nCatalogueLocation;
376        }
377        
378        @Override
379        protected SkinParameter _createParameter(Configuration parameterConfig) throws ConfigurationException
380        {
381            return new SkinParameter();
382        }
383        
384        @Override
385        protected String _parseId(Configuration parameterConfig) throws ConfigurationException
386        {
387            return parameterConfig.getAttribute("id");
388        }
389        
390        @Override
391        protected ParameterType _parseType(Configuration parameterConfig) throws ConfigurationException
392        {
393            try
394            {
395                return ParameterType.valueOf(parameterConfig.getAttribute("type").toUpperCase());
396            }
397            catch (IllegalArgumentException e)
398            {
399                throw new ConfigurationException("Invalid parameter type", parameterConfig, e);
400            }
401        }
402        
403        @Override
404        protected I18nizableText _parseI18nizableText(Configuration config, String pluginName, String name) throws ConfigurationException
405        {
406            return I18nizableText.parseI18nizableText(config.getChild(name), _i18nCatalogueLocation, "messages", null);
407        }
408        
409        @Override
410        protected Object _parseDefaultValue(Configuration parameterConfig, SkinParameter parameter)
411        {
412            String value;
413            
414            Configuration childNode = parameterConfig.getChild("default-value", false);
415            if (childNode == null)
416            {
417                value = null;
418            }
419            else
420            {
421                value = childNode.getValue("");
422            }
423            
424            return ParameterHelper.castValue(value, parameter.getType());
425        }
426        
427        @Override
428        protected void _additionalParsing(ServiceManager smanager, String pluginName, Configuration parameterConfig, String parameterId, SkinParameter parameter) throws ConfigurationException
429        {
430            super._additionalParsing(smanager, pluginName, parameterConfig, parameterId, parameter);
431            
432            parameter.setId(parameterId);
433            parameter.setDisplayCategory(_parseI18nizableText(parameterConfig, pluginName, "category"));
434            parameter.setDisplayGroup(_parseI18nizableText(parameterConfig, pluginName, "group"));
435        }
436    }
437    
438    class SkinConfigHandler extends DefaultHandler
439    {
440        
441        // The object being constructed
442        private Map<String, String> _variables;
443        
444        // The object being constructed
445        private String _variable;
446        
447        // current characters from SAX events
448        private StringBuffer _currentString;
449        
450        public Map<String, String> getVariables()
451        {
452            return _variables;
453        }
454        
455        @Override
456        public void startDocument() throws SAXException
457        {
458            _variables = new HashMap<>();
459            _variable = null;
460        }
461        
462        @Override
463        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
464        {
465            if ("xsl:variable".equals(qName))
466            {
467                _variable = attributes.getValue("name");
468                
469                _currentString = new StringBuffer();
470            }
471        }
472
473        @Override
474        public void characters(char[] ch, int start, int length) throws SAXException
475        {
476            if (_variable != null)
477            {
478                _currentString.append(ch, start, length);
479            }
480        }
481
482        @Override
483        public void endElement(String uri, String localName, String qName) throws SAXException
484        {
485            if ("xsl:variable".equals(qName) && _variable != null)
486            {
487                _variables.put(_variable, _currentString.toString());
488                
489                _variable = null;
490            }
491        }
492    }
493}