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