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 = ParameterHelper.toJSON(skinParameter);
243        return param;
244    }
245    
246    private Configuration _getConfigurationModel(File skinDir) throws ConfigurationException, SAXException, IOException
247    {
248        File configFile = new File(skinDir, "stylesheets" + File.separator + "config" + File.separator + "config-model.xml");
249        
250        if (configFile.exists())
251        {
252            try (FileInputStream is = new FileInputStream(configFile))
253            {
254                DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
255                return builder.build(is);
256            }
257        }
258
259        // There is no parameter
260        return null;
261    }
262    
263    /**
264     * Organize a collection of skin parameters by categories and groups.
265     * @param parameters a collection of skin parameters.
266     * @return a Map of parameters sorted first by category then group.
267     */
268    protected Map<I18nizableText, Map<I18nizableText, List<SkinParameter>>> _categorize(Collection<SkinParameter> parameters)
269    {
270        Map<I18nizableText, Map<I18nizableText, List<SkinParameter>>> categories = new HashMap<>();
271
272        // Classify parameters by groups and categories
273        for (SkinParameter parameter : parameters)
274        {
275            I18nizableText categoryName = parameter.getDisplayCategory();
276            I18nizableText groupName = parameter.getDisplayGroup();
277
278            // Get the map of groups of the category
279            Map<I18nizableText, List<SkinParameter>> category = categories.get(categoryName);
280            if (category == null)
281            {
282                category = new TreeMap<>(new I18nizableTextComparator());
283                categories.put(categoryName, category);
284            }
285
286            // Get the map of parameters of the group
287            List<SkinParameter> group = category.get(groupName);
288            if (group == null)
289            {
290                group = new ArrayList<>();
291                category.put(groupName, group);
292            }
293
294            group.add(parameter);
295        }
296        
297        return categories;
298    }
299    
300    class I18nizableTextComparator implements Comparator<I18nizableText>
301    {
302        @Override
303        public int compare(I18nizableText t1, I18nizableText t2)
304        {
305            return t1.toString().compareTo(t2.toString());
306        }
307    }
308    
309    /**
310     * Parser for {@link SkinParameter} parameter.
311     */
312    class SkinParameterParser extends AbstractParameterParser<SkinParameter, ParameterType>
313    {
314        private String _i18nCatalogueLocation;
315        
316        public SkinParameterParser(ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager, String i18nCatalogueLocation)
317        {
318            super(enumeratorManager, validatorManager);
319            _i18nCatalogueLocation = i18nCatalogueLocation;
320        }
321        
322        @Override
323        protected SkinParameter _createParameter(Configuration parameterConfig) throws ConfigurationException
324        {
325            return new SkinParameter();
326        }
327        
328        @Override
329        protected String _parseId(Configuration parameterConfig) throws ConfigurationException
330        {
331            return parameterConfig.getAttribute("id");
332        }
333        
334        @Override
335        protected ParameterType _parseType(Configuration parameterConfig) throws ConfigurationException
336        {
337            try
338            {
339                return ParameterType.valueOf(parameterConfig.getAttribute("type").toUpperCase());
340            }
341            catch (IllegalArgumentException e)
342            {
343                throw new ConfigurationException("Invalid parameter type", parameterConfig, e);
344            }
345        }
346        
347        @Override
348        protected I18nizableText _parseI18nizableText(Configuration config, String pluginName, String name) throws ConfigurationException
349        {
350            return I18nizableText.parseI18nizableText(config.getChild(name), _i18nCatalogueLocation, "messages", null);
351        }
352        
353        @Override
354        protected Object _parseDefaultValue(Configuration parameterConfig, SkinParameter parameter)
355        {
356            String value;
357            
358            Configuration childNode = parameterConfig.getChild("default-value", false);
359            if (childNode == null)
360            {
361                value = null;
362            }
363            else
364            {
365                value = childNode.getValue("");
366            }
367            
368            return ParameterHelper.castValue(value, parameter.getType());
369        }
370        
371        @Override
372        protected void _additionalParsing(ServiceManager smanager, String pluginName, Configuration parameterConfig, String parameterId, SkinParameter parameter) throws ConfigurationException
373        {
374            super._additionalParsing(smanager, pluginName, parameterConfig, parameterId, parameter);
375            
376            parameter.setId(parameterId);
377            parameter.setDisplayCategory(_parseI18nizableText(parameterConfig, pluginName, "category"));
378            parameter.setDisplayGroup(_parseI18nizableText(parameterConfig, pluginName, "group"));
379        }
380    }
381    
382    class SkinConfigHandler extends DefaultHandler
383    {
384        
385        // The object being constructed
386        private Map<String, String> _variables;
387        
388        // The object being constructed
389        private String _variable;
390        
391        // current characters from SAX events
392        private StringBuffer _currentString;
393        
394        public Map<String, String> getVariables()
395        {
396            return _variables;
397        }
398        
399        @Override
400        public void startDocument() throws SAXException
401        {
402            _variables = new HashMap<>();
403            _variable = null;
404        }
405        
406        @Override
407        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
408        {
409            if ("xsl:variable".equals(qName))
410            {
411                _variable = attributes.getValue("name");
412                
413                _currentString = new StringBuffer();
414            }
415        }
416
417        @Override
418        public void characters(char[] ch, int start, int length) throws SAXException
419        {
420            if (_variable != null)
421            {
422                _currentString.append(ch, start, length);
423            }
424        }
425
426        @Override
427        public void endElement(String uri, String localName, String qName) throws SAXException
428        {
429            if ("xsl:variable".equals(qName) && _variable != null)
430            {
431                _variables.put(_variable, _currentString.toString());
432                
433                _variable = null;
434            }
435        }
436    }
437}