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