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