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 */
016
017package org.ametys.skinfactory.model;
018
019import java.io.IOException;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.text.DateFormat;
023import java.text.SimpleDateFormat;
024import java.util.ArrayList;
025import java.util.Date;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.cocoon.ProcessingException;
036import org.apache.commons.io.FileUtils;
037
038import org.ametys.core.ui.Callable;
039import org.ametys.core.util.path.PathUtils;
040import org.ametys.plugins.skincommons.SkinEditionHelper;
041import org.ametys.runtime.plugin.component.AbstractLogEnabled;
042import org.ametys.runtime.util.AmetysHomeHelper;
043import org.ametys.skinfactory.SkinFactoryComponent;
044import org.ametys.skinfactory.filefilter.FileFilter;
045import org.ametys.web.cocoon.I18nTransformer;
046import org.ametys.web.cocoon.I18nUtils;
047import org.ametys.web.skin.Skin;
048import org.ametys.web.skin.SkinDAO;
049import org.ametys.web.skin.SkinModel;
050import org.ametys.web.skin.SkinModelsManager;
051import org.ametys.web.skin.SkinsManager;
052
053/**
054 * Component for interact with a skin model
055 */
056public class SkinModelDAO extends AbstractLogEnabled implements Serviceable, Component
057{
058    private static final DateFormat _DATE_FORMAT = new SimpleDateFormat("yyyyMMdd-HHmm");
059    
060    
061    private SkinsManager _skinsManager;
062    private SkinModelsManager _modelsManager;
063    private SkinFactoryComponent _skinFactoryManager;
064    private SkinEditionHelper _skinHelper;
065    private SkinDAO _skinDAO;
066    private I18nUtils _i18nUtils;
067
068    @Override
069    public void service(ServiceManager manager) throws ServiceException
070    {
071        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
072        _modelsManager = (SkinModelsManager) manager.lookup(SkinModelsManager.ROLE);
073        _skinFactoryManager = (SkinFactoryComponent) manager.lookup(SkinFactoryComponent.ROLE);
074        _skinHelper = (SkinEditionHelper) manager.lookup(SkinEditionHelper.ROLE);
075        _skinDAO = (SkinDAO) manager.lookup(SkinDAO.ROLE);
076        _i18nUtils = (I18nUtils) manager.lookup(org.ametys.core.util.I18nUtils.ROLE);
077    }
078
079    /**
080     * Retrieve informations on a skin
081     * @param modelId The skin id
082     * @return the informations of a skin
083     */
084    @Callable
085    public Map<String, Object> getModel(String modelId)
086    {
087        Map<String, Object> result = new HashMap<>();
088
089        SkinModel model = _modelsManager.getModel(modelId);
090
091        if (model != null)
092        {
093            result.put("name", model.getId());
094            result.put("title", model.getLabel());
095            result.put("isModifiable", model.isModifiable());
096        }
097        
098        return result;
099    }
100    
101    /**
102     * Retrieve the list of models and skins available
103     * @param includeAbstract Should include abstract skins
104     * @return a map of skins and models
105     */
106    @Callable
107    public Map<String, Object> getSkinsAndModels(boolean includeAbstract)
108    {
109        Map<String, Object> result = _skinDAO.getSkins(includeAbstract);
110
111        result.put("models", _models2JsonObject());
112
113        return result;
114    }
115
116    private List<Object> _models2JsonObject()
117    {
118        List<Object> modelsList = new ArrayList<>();
119        Set<String> models = _modelsManager.getModels();
120        for (String modelName : models)
121        {
122            Map<String, Object> jsonModel = new HashMap<>();
123            SkinModel model = _modelsManager.getModel(modelName);
124
125            jsonModel.put("id", modelName);
126            jsonModel.put("label", model.getLabel());
127            jsonModel.put("description", model.getDescription());
128            jsonModel.put("iconLarge", model.getLargeImage());
129            jsonModel.put("iconSmall", model.getSmallImage());
130
131            modelsList.add(jsonModel);
132        }
133
134        return modelsList;
135    }
136    
137    /**
138     * Determines if a model exists
139     * @param modelId The model id
140     * @return true if model exists.
141     * @throws ProcessingException if something goes wrong when retrieving the list of models
142     */
143    @Callable
144    public boolean modelExists (String modelId) throws ProcessingException
145    {
146        return _modelsManager.getModels().contains(modelId);
147    }
148    
149    /**
150     * Import a model from a zip file
151     * @param modelName The name of the new model
152     * @param tmpDirPath the tmp dir path where the zip has been uploaded
153     * @return The model name
154     * @throws IOException if something goes wrong when manipulating files
155     */
156    @Callable
157    public String importModel(String modelName, String tmpDirPath) throws IOException
158    {
159        Path tmpDir = AmetysHomeHelper.getAmetysHomeTmp().toPath().resolve(tmpDirPath);
160        
161        if (Files.isDirectory(tmpDir))
162        {
163            // If exists: remove.
164            SkinModel model = _modelsManager.getModel(modelName);
165            if (model != null)
166            {
167                if (model.isModifiable())
168                {
169                    PathUtils.deleteDirectory(model.getPath());
170                }
171                else
172                {
173                    throw new IllegalStateException("The skin model '" + modelName + "' already exists and is not modifiable and thus cannot be replaced.");       
174                }
175            }
176            
177            // Move to models
178            Path rootLocation = _modelsManager.getLocalModelsLocation();
179            PathUtils.moveDirectory(tmpDir, rootLocation.resolve(modelName));
180            
181            _i18nUtils.reloadCatalogues();
182            I18nTransformer.needsReload();
183        }
184        
185        return modelName;
186    }
187    
188    /**
189     * Generate a new skin from a model
190     * @param skinId The new skin id
191     * @param modelId The model
192     * @return An error message, or null on success
193     * @throws IOException if an error occurs when manipulating files
194     * @throws ProcessingException if an exception occurred during the generation processs 
195     */
196    @Callable
197    public String generateSkin(String skinId, String modelId) throws IOException, ProcessingException
198    {
199        // Check if exists
200        if (_skinsManager.getSkins().contains(skinId))
201        {
202            return "already-exists";
203        }
204        
205        Path modelDir = _modelsManager.getModel(modelId).getPath();
206        Path skinDir = _skinsManager.getLocalSkinsLocation().resolve(skinId);
207        
208        // Copy the model
209        PathUtils.copyDirectory(modelDir, skinDir, FileFilter.getModelFilter(modelDir), false);
210        
211        try
212        {
213            Skin skin = _skinsManager.getSkin(skinId);
214
215            // Create model.xml file
216            _modelsManager.generateModelFile(skinDir, modelId);
217            
218            SkinModel model = _modelsManager.getModel(modelId);
219            String defaultColorTheme = model.getDefaultColorTheme();
220            if (defaultColorTheme != null)
221            {
222                _skinFactoryManager.saveColorTheme(skin.getRawPath(), defaultColorTheme);
223            }
224
225            // Apply all parameters
226            _skinFactoryManager.applyModelParameters(modelId, skin.getRawPath());
227            
228            I18nTransformer.needsReload();
229            _i18nUtils.reloadCatalogues();
230        }
231        catch (Exception e)
232        {
233            // Delete skin directory if the generation failed
234            FileUtils.deleteDirectory(skinDir.toFile());
235            
236            throw new ProcessingException("The generation of skin failed", e);
237            
238        }
239        
240        return null;
241    }
242    
243    /**
244     * Apply the model to all its skins
245     * @param modelId The model id
246     * @return The set of modified skins id
247     * @throws IOException if an error occurs when manipulating files 
248     */
249    @Callable
250    public Map<String, Object> applyModelToAll(String modelId) throws IOException
251    {
252        Map<String, Object> result = new HashMap<>();
253        
254        result.put("modifiedSkins", new ArrayList<>());
255        result.put("unmodifiedSkins", new ArrayList<>());
256        result.put("unmodifiableSkins", new ArrayList<>());
257        
258        Set<String> skins = _skinsManager.getSkins();
259        
260        for (String skinId : skins)
261        {
262            Skin skin = _skinsManager.getSkin(skinId);
263            if (modelId.equals(_modelsManager.getModelOfSkin(skin)))
264            {
265                if (!skin.isModifiable())
266                {
267                    @SuppressWarnings("unchecked")
268                    List<Map<String, Object>> unmodifiableSkins = (List<Map<String, Object>>) result.get("unmodifiableSkins");
269                    unmodifiableSkins.add(_getSkinProperty(skin));
270                }
271                else if (applyModel(skin, modelId))
272                {
273                    @SuppressWarnings("unchecked")
274                    List<Map<String, Object>> modifiedSkins = (List<Map<String, Object>>) result.get("modifiedSkins");
275                    modifiedSkins.add(_getSkinProperty(skin));
276                }
277                else
278                {
279                    @SuppressWarnings("unchecked")
280                    List<Map<String, Object>> unmodifiedSkins = (List<Map<String, Object>>) result.get("unmodifiedSkins");
281                    unmodifiedSkins.add(_getSkinProperty(skin));
282                }
283            }
284        }
285        
286        return result;
287    }
288    
289    private Map<String, Object> _getSkinProperty(Skin skin)
290    {
291        Map<String, Object> info = new HashMap<>();
292        info.put("name", skin.getId());
293        info.put("label", skin.getLabel());
294        return info;
295    }
296    
297    /**
298     * Apply model to the skin
299     * @param skinId The skin id
300     * @param modelId The id of model
301     * @return true if the model was applyed successfully
302     * @throws IOException if an error occurs when manipulating files
303     */
304    @Callable
305    public boolean applyModel(String skinId, String modelId) throws IOException
306    {
307        Skin skin = _skinsManager.getSkin(skinId);
308        
309        return applyModel(skin, modelId);
310    }
311     
312    /**
313     * Apply model to the skin
314     * @param skin The skin
315     * @param modelId The id of model
316     * @return true if the model was applyed successfully
317     * @throws IOException if an error occurs when manipulating files
318     */
319    protected boolean applyModel(Skin skin, String modelId) throws IOException
320    {
321        if (!skin.isModifiable())
322        {
323            throw new IllegalStateException("The skin '" + skin.getId() + "' is not modifiable and thus the model can not be applied.");
324        }
325        
326        Path skinDir = skin.getRawPath();
327        
328        // Prepare skin in temporary file
329        Path tmpDir = skinDir.getParent().resolve(skin.getId() + "." + _DATE_FORMAT.format(new Date()));
330        
331        // Copy the model
332        Path modelDir = _modelsManager.getModel(modelId).getPath();
333        PathUtils.copyDirectory(modelDir, tmpDir, FileFilter.getModelFilter(modelDir), false);
334        
335        // Copy upload images if exists
336        Path uploadDir = skinDir.resolve("model/_uploads");
337        if (Files.exists(uploadDir))
338        {
339            Path tmpUploadDir = tmpDir.resolve("model/_uploads");
340            Files.createDirectories(tmpUploadDir);
341            FileUtils.copyDirectory(uploadDir.toFile(), tmpUploadDir.toFile());
342        }
343        
344        // Copy model.xml file
345        Path xmlFile = skinDir.resolve("model.xml");
346        FileUtils.copyFileToDirectory(xmlFile.toFile(), tmpDir.toFile());
347        Path tmpXmlFile = tmpDir.resolve("model.xml");
348        
349        // Apply parameters
350        _skinFactoryManager.applyModelParameters(modelId, tmpDir);
351        _skinFactoryManager.updateHash(tmpXmlFile, _modelsManager.getModelHash(modelId));
352        
353        if (!_skinHelper.deleteQuicklyDirectory(skinDir))
354        {
355            getLogger().error("Cannot delete skin directory {}", skinDir.toAbsolutePath().toString()); 
356            return false;
357        }
358        
359        FileUtils.moveDirectory(tmpDir.toFile(), skinDir.toFile());
360        return true;
361    }
362    
363    /**
364     * Delete a model
365     * @param modelId The model id
366     * @throws IOException if an error occurs when manipulating files
367     */
368    @Callable
369    public void delete(String modelId) throws IOException
370    {
371        SkinModel model = _modelsManager.getModel(modelId);
372        
373        if (!model.isModifiable())
374        {
375            throw new IllegalStateException("The skin model '" + modelId + "' is not modified and thus cannot be removed.");       
376        }
377        
378        // Unlink skins
379        Set<String> skins = _skinsManager.getSkins();
380        for (String skinId : skins)
381        {
382            Skin skin = _skinsManager.getSkin(skinId);
383            if (skin.isModifiable() && modelId.equals(_modelsManager.getModelOfSkin(skin)))
384            {
385                _unlinkModel(skin);
386            }
387        }
388        
389        Path file = model.getPath();
390        if (Files.exists(file))
391        {
392            PathUtils.deleteDirectory(file);
393        }
394    }
395
396    /**
397     * Unlink the skin from its model
398     * @param skinId The id of the skin
399     * @param modelId The id of the model
400     * @return An error code, or null on success
401     * @throws IOException If an error occurred while removing the link
402     */
403    @Callable
404    public String unlinkModel(String skinId, String modelId) throws IOException
405    {
406        Skin skin = _skinsManager.getSkin(skinId);
407        
408        if (!modelId.equals(_modelsManager.getModelOfSkin(skin)))
409        {
410            return "incorrect-model";
411        }
412        
413        _unlinkModel(skin);
414        
415        return null;
416    }
417
418    private void _unlinkModel(Skin skin) throws IOException
419    {
420        if (!skin.isModifiable())
421        {
422            throw new IllegalStateException("The skin '" + skin.getId() + "' is not modifiable and thus it can not be unlink to its model.");
423        }
424        
425        Path skinDir = skin.getRawPath();
426        
427        Path modelFile = skinDir.resolve("model.xml");
428        Path bakFile = skinDir.resolve("model.xml.bak");
429        
430        // Delete old bak file if exists
431        Files.deleteIfExists(bakFile);
432        
433        if (Files.exists(modelFile))
434        {
435            Files.move(modelFile, bakFile);
436        }
437    }
438}