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;
017
018import java.io.BufferedOutputStream;
019import java.io.File;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.OutputStream;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Properties;
030import java.util.stream.Collectors;
031
032import javax.xml.transform.OutputKeys;
033import javax.xml.transform.Result;
034import javax.xml.transform.TransformerConfigurationException;
035import javax.xml.transform.TransformerFactory;
036import javax.xml.transform.sax.SAXTransformerFactory;
037import javax.xml.transform.sax.TransformerHandler;
038import javax.xml.transform.stream.StreamResult;
039
040import org.apache.avalon.framework.component.Component;
041import org.apache.avalon.framework.configuration.Configuration;
042import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
043import org.apache.avalon.framework.context.Context;
044import org.apache.avalon.framework.context.ContextException;
045import org.apache.avalon.framework.context.Contextualizable;
046import org.apache.avalon.framework.logger.AbstractLogEnabled;
047import org.apache.avalon.framework.service.ServiceException;
048import org.apache.avalon.framework.service.ServiceManager;
049import org.apache.avalon.framework.service.Serviceable;
050import org.apache.cocoon.ProcessingException;
051import org.apache.cocoon.xml.AttributesImpl;
052import org.apache.cocoon.xml.XMLUtils;
053import org.apache.commons.io.FileUtils;
054import org.apache.xml.serializer.OutputPropertiesFactory;
055import org.slf4j.LoggerFactory;
056import org.xml.sax.SAXException;
057
058import org.ametys.core.ui.Callable;
059import org.ametys.plugins.repository.AmetysObjectIterable;
060import org.ametys.runtime.i18n.I18nizableText;
061import org.ametys.runtime.model.CategorizedElementDefinitionHelper;
062import org.ametys.runtime.model.DefinitionAndValue;
063import org.ametys.runtime.model.ElementDefinition;
064import org.ametys.runtime.model.type.ElementType;
065import org.ametys.runtime.util.AmetysHomeHelper;
066import org.ametys.web.cocoon.I18nTransformer;
067import org.ametys.web.cocoon.I18nUtils;
068import org.ametys.web.impl.model.type.xsl.XSLElementType;
069import org.ametys.web.repository.site.Site;
070import org.ametys.web.repository.site.SiteManager;
071
072/**
073 * DAO for manipulating skins
074 */
075public class SkinDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
076{
077    /** The avalon role*/
078    public static final String ROLE = SkinDAO.class.getName();
079    
080    private static final String __LICENCE = "\n"
081            + "   Copyright 2012 Anyware Services\n"
082            + "\n"
083            + "   Licensed under the Apache License, Version 2.0 (the \"License\");\n"
084            + "   you may not use this file except in compliance with the License.\n"
085            + "   You may obtain a copy of the License at\n"
086            + "\n"
087            + "       http://www.apache.org/licenses/LICENSE-2.0\n"
088            + "\n"
089            + "   Unless required by applicable law or agreed to in writing, software\n"
090            + "   distributed under the License is distributed on an \"AS IS\" BASIS,\n"
091            + "   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
092            + "   See the License for the specific language governing permissions and\n"
093            + "   limitations under the License.\n"
094            + "\n"; 
095
096    /** The service manager instance */
097    protected ServiceManager _manager;
098    private Context _context;
099    private SkinsManager _skinsManager;
100    private SkinModelsManager _modelsManager;
101    private SiteManager _siteManager;
102    private I18nUtils _i18nUtils;
103    private SkinParameterTypeExtensionPoint _skinParameterTypeEP;
104
105    @Override
106    public void service(ServiceManager manager) throws ServiceException
107    {
108        _manager = manager;
109        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
110        _modelsManager = (SkinModelsManager) manager.lookup(SkinModelsManager.ROLE);
111        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
112        _i18nUtils = (I18nUtils) manager.lookup(org.ametys.core.util.I18nUtils.ROLE);
113        _skinParameterTypeEP = (SkinParameterTypeExtensionPoint) manager.lookup(SkinParameterTypeExtensionPoint.ROLE);
114    }
115    
116    public void contextualize(Context context) throws ContextException
117    {
118        _context = context;
119    }
120    /**
121     * Retrieve informations on a skin
122     * @param skinId The skin id
123     * @return the informations of a skin
124     */
125    @Callable
126    public Map<String, Object> getSkin(String skinId)
127    {
128        Map<String, Object> result = new HashMap<>();
129
130        Skin skin = _skinsManager.getSkin(skinId);
131
132        if (skin != null)
133        {
134            result.put("id", skin.getId());
135            result.put("label", skin.getLabel());
136            result.put("inUse", isInUse(skinId));
137    
138            String modelName = _modelsManager.getModelOfSkin(skin);
139            result.put("model", modelName);
140        }
141        
142        return result;
143    }
144    
145    /**
146     * Determines if a skin exists
147     * @param skinId The skin id
148     * @return true if skin exists.
149     * @throws ProcessingException if an error occurs
150     */
151    @Callable
152    public boolean skinExists (String skinId) throws ProcessingException
153    {
154        return _skinsManager.getSkins().contains(skinId);
155    }
156    
157    /**
158     * Determines if a skin is currently used by one or more sites
159     * @param skinId The skin id
160     * @return true if skin is currently in use
161     */
162    @Callable
163    public boolean isInUse (String skinId)
164    {
165        AmetysObjectIterable<Site> sites = _siteManager.getSites();
166        for (Site site : sites)
167        {
168            if (skinId.equals(site.getSkinId()))
169            {
170                return true;
171            }
172            
173        }
174        return false;
175    }
176    
177    /**
178     * Retrieve the list of available skins
179     * @return a map of skins
180     * @throws ProcessingException if an error occurs
181     */
182    @Callable
183    public Map<String, Object> getSkins() throws ProcessingException
184    {
185        Map<String, Object> result = new HashMap<>();
186
187        result.put("skins", _skins2JsonObject());
188
189        return result;
190    }
191
192    private List<Object> _skins2JsonObject()
193    {
194        List<Object> skinsList = new ArrayList<>();
195        Map<String, List<Site>> skins = new HashMap<>();
196        for (String id : _skinsManager.getSkins())
197        {
198            skins.put(id, new ArrayList<Site>());
199        }
200
201        AmetysObjectIterable<Site> sites = _siteManager.getSites();
202        for (Site site : sites)
203        {
204            String skinId = site.getSkinId();
205            if (skinId != null)
206            {
207                List<Site> skin = skins.get(skinId);
208    
209                if (skin != null)
210                {
211                    skin.add(site);
212                }
213            }
214        }
215
216        for (String id : skins.keySet())
217        {
218            skinsList.add(_skin2JsonObject(id, skins.get(id)));
219        }
220
221        return skinsList;
222    }
223
224    private Map<String, Object> _skin2JsonObject(String id, List<Site> skinSites)
225    {
226        Map<String, Object> jsonSkin = new HashMap<>();
227
228        Skin skin = _skinsManager.getSkin(id);
229
230        jsonSkin.put("id", skin.getId());
231        jsonSkin.put("label", skin.getLabel());
232        jsonSkin.put("description", skin.getDescription());
233        jsonSkin.put("iconLarge", skin.getLargeImage());
234        jsonSkin.put("iconSmall", skin.getSmallImage());
235        jsonSkin.put("nbSites", skinSites.size());
236        jsonSkin.put("sites", skinSites.stream()
237                .map(site -> site.getTitle() + " (" + site.getName() + ")")
238                .collect(Collectors.joining(", ")));
239
240        String modelName = _modelsManager.getModelOfSkin(skin);
241        if (modelName != null)
242        {
243            SkinModel model = _modelsManager.getModel(modelName);
244            if (model != null)
245            {
246                Map<String, Object> skinModel = new HashMap<>();
247
248                skinModel.put("id", modelName);
249                skinModel.put("name", model.getLabel());
250
251                jsonSkin.put("model", skinModel);
252            }
253        }
254
255        return jsonSkin;
256    }
257
258
259
260    /**
261     * This action receive a form with the "importfile" zip file as an exported skin.
262     * Replace existing skin 
263     * @param skinName The skin name
264     * @param tmpDirPath The directory where the zip was uploaded 
265     * @param values the configuration's values. Can be empty.
266     * @return The skin name
267     * @throws SAXException if an error occurs during configuration file creation
268     * @throws IOException if an error occurs while manipulating files
269     * @throws TransformerConfigurationException if an error occurs during configuration file creation
270     * @throws ProcessingException error while parsing model
271     */
272    @Callable
273    public Map<String, Object> importSkin(String skinName, String tmpDirPath, Map<String, Object> values) throws TransformerConfigurationException, IOException, SAXException, ProcessingException
274    {
275        Map<String, Object> result = new HashMap<>();
276        
277        File ametysTmpDir = AmetysHomeHelper.getAmetysHomeTmp();
278        File tmpDir = new File(ametysTmpDir, tmpDirPath.replace('/', File.separatorChar));
279        if (tmpDir.isDirectory())
280        {
281            File rootLocation = new File(_skinsManager.getSkinsLocation());
282
283            if (!values.isEmpty())
284            {
285                SkinParametersModel model = new SkinParametersModel(skinName, tmpDir, _skinParameterTypeEP, _context, _manager);
286                Map<String, ElementDefinition> flatDefinitions = model.getFlatDefinitions();
287                Map<String, DefinitionAndValue> definitionAndValues = _getDefinitionAndValues(values, flatDefinitions);
288                
289                Map<String, List<I18nizableText>> errorFields = CategorizedElementDefinitionHelper.validateValuesForWriting(definitionAndValues, LoggerFactory.getLogger(getClass()));
290                if (!errorFields.isEmpty())
291                {
292                    result.put("errors", errorFields);
293                    return result;
294                }
295                
296                _createConfigFile(model, tmpDir, values);
297            }
298            
299            // If exists: remove.
300            File skinDir = new File(rootLocation, skinName);
301            if (skinDir.exists())
302            {
303                FileUtils.deleteDirectory(skinDir);
304            }
305
306            // Move to skins
307            FileUtils.moveDirectory(tmpDir, skinDir);
308
309            _i18nUtils.reloadCatalogues();
310            I18nTransformer.needsReload();
311        }
312        
313        result.put("skinId", skinName);
314        return result;
315    }
316    
317    /**
318     * Configure a skin
319     * @param skinName the skin name
320     * @param values the configuration's values
321     * @return A map with "errors" key that is a map &lt;errorName&gt; &lt;errorMessage&gt;
322     * @throws SAXException if an error occurs during configuration file creation
323     * @throws IOException if an error occurs during configuration file creation
324     * @throws TransformerConfigurationException if an error occurs during configuration file creation
325     * @throws ProcessingException error while parsing model
326     */
327    @Callable
328    public Map<String, Object> configureSkin (String skinName,  Map<String, Object> values) throws TransformerConfigurationException, IOException, SAXException, ProcessingException
329    {
330        Map<String, Object> result = new HashMap<>();
331        
332        Skin skin = _skinsManager.getSkin(skinName);
333        
334        File skinDir = new File (skin.getLocation());
335        SkinParametersModel model = new SkinParametersModel(skinName, skinDir, _skinParameterTypeEP, _context, _manager);
336        
337        Map<String, ElementDefinition> flatDefinitions = model.getFlatDefinitions();
338        Map<String, DefinitionAndValue> definitionAndValues = _getDefinitionAndValues(values, flatDefinitions);
339        
340        Map<String, List<I18nizableText>> errorFields = CategorizedElementDefinitionHelper.validateValuesForWriting(definitionAndValues, LoggerFactory.getLogger(getClass()));
341        if (!errorFields.isEmpty())
342        {
343            result.put("errors", errorFields);
344            return result;
345        }
346        
347        //Delete the old file only if no error found
348        File configFile = new File(skinDir, "stylesheets" + File.separator + "config" + File.separator + "config.xsl");
349        if (configFile.exists())
350        {
351            FileUtils.deleteQuietly(configFile);
352        }
353        _createConfigFile(model, skinDir, values);
354        
355        _i18nUtils.reloadCatalogues();
356        I18nTransformer.needsReload();
357        
358        result.put("skinId", skinName);
359        return result;
360    }
361    
362    private Map<String, DefinitionAndValue> _getDefinitionAndValues(Map<String, Object> values, Map<String, ElementDefinition> flatDefinitions)
363    {
364        Map<String, DefinitionAndValue> definitionAndValues = new HashMap<>();
365        for (Map.Entry<String, ElementDefinition> definitionEntry : flatDefinitions.entrySet())
366        {
367            Object value = values.get(definitionEntry.getKey());
368            ElementDefinition definition = definitionEntry.getValue();
369            if (definition != null)
370            {
371                DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, definition, value);
372                definitionAndValues.put(definitionEntry.getKey(), definitionAndValue);
373            }
374        }
375        return definitionAndValues;
376    }
377    
378    private void _createConfigFile(SkinParametersModel model, File skinDir, Map<String, Object> values) throws IOException, SAXException, TransformerConfigurationException, ProcessingException
379    {
380        File configFile = new File(skinDir, "stylesheets" + File.separator + "config" + File.separator + "config.xsl");
381        
382        Map<String, ElementDefinition> flatDefinitions = model.getFlatDefinitions();
383        
384        SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
385        TransformerHandler handler = factory.newTransformerHandler();
386        
387        try (OutputStream os = new BufferedOutputStream(new FileOutputStream(configFile)))
388        {
389            Result result = new StreamResult(os);
390            
391            Properties format = new Properties();
392            format.put(OutputKeys.METHOD, "xml");
393            format.put(OutputKeys.ENCODING, "UTF-8");
394            format.put(OutputKeys.INDENT, "yes");
395            format.put(OutputKeys.OMIT_XML_DECLARATION, "no");
396            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
397            
398            handler.setResult(result);
399            handler.getTransformer().setOutputProperties(format);
400            
401            handler.startDocument();
402            
403            AttributesImpl attrs = new AttributesImpl();
404            attrs.addCDATAAttribute("version", "1.0");
405            attrs.addCDATAAttribute("xmlns:xsl", "http://www.w3.org/1999/XSL/Transform");
406            
407            handler.comment(__LICENCE.toCharArray(), 0, __LICENCE.length());
408            XMLUtils.startElement(handler, "xsl:stylesheet", attrs);
409            
410            for (Entry<String, Object> variable : values.entrySet())
411            {
412                ElementDefinition elementDefinition = flatDefinitions.get(variable.getKey());
413                if (elementDefinition != null)
414                {
415                    ElementType type = elementDefinition.getType();
416                    if (type instanceof XSLElementType)
417                    {
418                        ((XSLElementType) type).write(handler, elementDefinition.getName(), variable.getValue());
419                    }
420                }
421            }
422            
423            XMLUtils.endElement(handler, "xsl:stylesheet");
424            
425            handler.endDocument();
426        }
427    }
428    
429    /**
430     * Duplicate an existing skin
431     * @param skinId The new skin id
432     * @param originalSkinId The original skin id
433     * @return An error message, or null if successful
434     * @throws IOException if an I/O exception occurs during copy
435     */
436    @Callable
437    public String copySkin(String skinId, String originalSkinId) throws IOException
438    {
439        // Check if exists
440        if (_skinsManager.getSkins().contains(skinId))
441        {
442            return "already-exists";
443        }
444        
445        File srcDir = new File(_skinsManager.getSkinsLocation() + File.separator + originalSkinId);
446        File destDir = new File(_skinsManager.getSkinsLocation() + File.separator + skinId);
447        
448        FileUtils.copyDirectory(srcDir, destDir, new ModelFileFilter(srcDir), false);
449        
450        _i18nUtils.reloadCatalogues();
451        I18nTransformer.needsReload();
452        
453        return null;
454    }
455    
456    /**
457     * Delete a skin
458     * @param skinId The skin id
459     * @return the skin id
460     * @throws IOException if an I/O exception occurs during deletion
461     */
462    @Callable
463    public String deleteSkin(String skinId) throws IOException
464    {
465        Skin skin = _skinsManager.getSkin(skinId);
466        File file = skin.getFile();
467        
468        if (file.exists())
469        {
470            FileUtils.deleteDirectory(file);
471        }
472        
473        return skinId;
474    }
475    
476    /**
477     * Parse the values file and make a liaison with the definitions, to return a collection of {@link DefinitionAndValue}
478     * @param skinDir folder of the skin
479     * @param flatDefinitions map of {@link ElementDefinition}
480     * @return a {@link Collection} of {@link DefinitionAndValue} using flatDefinitions and the values found in skinDir
481     * @throws Exception Impossible to read the file
482     */
483    public Collection<DefinitionAndValue> readValues(File skinDir, Map<String, ElementDefinition> flatDefinitions) throws Exception
484    {
485        Collection<DefinitionAndValue> definitionAndValues = new ArrayList<>();
486        String fileName = "stylesheets" + File.separator + "config" + File.separator + "config.xsl";
487        File configFile = new File(skinDir, fileName);
488        boolean fileExists = configFile.exists();
489
490        if (fileExists)
491        {
492            Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(configFile);
493            for (Entry<String, ElementDefinition> entry : flatDefinitions.entrySet())
494            {
495                String parameterName = entry.getKey();
496                ElementDefinition definition = entry.getValue();
497                if (definition != null)
498                {
499                    ElementType type = definition.getType();
500                    if (type instanceof XSLElementType)
501                    {
502                        Object value = ((XSLElementType) type).read(configuration, parameterName);
503                        DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, definition, value);
504                        definitionAndValues.add(definitionAndValue);
505                    }
506                }
507            }
508            
509            return definitionAndValues;
510        }
511        
512        return null;
513    }
514}