001/*
002 *  Copyright 2013 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.plugins.skincommons;
017
018import java.io.IOException;
019import java.nio.file.Files;
020import java.nio.file.Path;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.Map;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang.StringUtils;
032
033import org.ametys.core.ui.Callable;
034import org.ametys.core.user.CurrentUserProvider;
035import org.ametys.core.user.User;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.user.UserManager;
038import org.ametys.core.util.DateUtils;
039import org.ametys.core.util.path.PathUtils;
040import org.ametys.runtime.config.Config;
041import org.ametys.web.repository.page.Page;
042import org.ametys.web.repository.site.Site;
043import org.ametys.web.repository.site.SiteManager;
044import org.ametys.web.skin.Skin;
045import org.ametys.web.skin.SkinsManager;
046
047/**
048 * DAO for skin edition.
049 */
050public class CommonSkinDAO implements Serviceable, Component
051{
052    private static final String __TEMP_MODE = "temp";
053    private static final String __WORK_MODE = "work";
054    
055    private SiteManager _siteManager;
056    private SkinEditionHelper _skinHelper;
057    private SkinLockManager _lockManager;
058    private CurrentUserProvider _userProvider;
059    private UserManager _userManager;
060    private SkinsManager _skinManager;
061    
062    @Override
063    public void service(ServiceManager smanager) throws ServiceException
064    {
065        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
066        _skinManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE);
067        _skinHelper = (SkinEditionHelper) smanager.lookup(SkinEditionHelper.ROLE);
068        _lockManager = (SkinLockManager) smanager.lookup(SkinLockManager.ROLE);
069        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
070        _userManager = (UserManager) smanager.lookup(UserManager.ROLE);
071    }
072    
073    /**
074     * Get the URI to preview a site
075     * @param siteName the site name
076     * @param lang the site langage
077     * @return The uri
078     */
079    @Callable
080    public String getPreviewURI(String siteName, String lang)
081    {
082        Site site = _siteManager.getSite(siteName);
083        
084        String siteLangage = !StringUtils.isEmpty(lang) ? lang : site.getSitemaps().iterator().next().getName();
085        
086        if (site.getSitemap(siteLangage).hasChild("index"))
087        {
088            return siteLangage + "/index.html";
089        }
090        else
091        {
092            Iterator<? extends Page> it = site.getSitemap(siteLangage).getChildrenPages().iterator();
093            
094            if (it.hasNext())
095            {
096                String path = it.next().getPathInSitemap();
097                return siteLangage + "/" + path + ".html";
098            }
099        }
100        
101        return null;
102    }
103    
104    /**
105     * Check if there is unsaved or uncomitted changes
106     * @param skinName The skin name
107     * @return The result
108     * @throws IOException if an error occurs while manipulating files
109     */
110    @Callable
111    public Map<String, Object> checkUnsaveModifications (String skinName) throws IOException
112    {
113        Map<String, Object> result = new HashMap<>();
114                
115        Path tempDir = _skinHelper.getTempDirectory(skinName);
116        Path workDir = _skinHelper.getWorkDirectory(skinName);
117        Path skinDir = _skinHelper.getSkinDirectory(skinName);
118        
119        if (!_lockManager.canWrite(tempDir))
120        {
121            // Unecessary to check unsave modifications, the user has no right anymore
122            return result;
123        }
124        
125        long lastModifiedLock = _lockManager.lastModified(tempDir).getTime();
126        if (lastModifiedLock <= Files.getLastModifiedTime(workDir).toMillis())
127        {
128            if (Files.getLastModifiedTime(workDir).toMillis() > Files.getLastModifiedTime(skinDir).toMillis())
129            {
130                result.put("hasUncommitChanges", true);
131            }
132            else
133            {
134                PathUtils.deleteDirectory(workDir);
135            }
136        }
137        else if (lastModifiedLock >= Files.getLastModifiedTime(skinDir).toMillis())
138        {
139            // The modifications were not saved
140            result.put("hasUnsaveChanges", true);
141        }
142        
143        return result;
144    }
145    
146    /**
147     * Save the current skin into the skin work folder
148     * @param skinName The name of the skin
149     * @param quit True if the temp directory can be removed
150     * @return The name of the skin
151     * @throws IOException if an error occurs while manipulating files
152     */
153    @Callable
154    public Map<String, Object> saveChanges(String skinName, boolean quit) throws IOException
155    {
156        Map<String, Object> lockInfos = _checkLock(skinName);
157        if (!lockInfos.isEmpty())
158        {
159            return lockInfos;
160        }
161        
162        Path tempDir = _skinHelper.getTempDirectory(skinName);
163        Path workDir = _skinHelper.getWorkDirectory(skinName);
164        
165        if (Files.exists(workDir))
166        {
167            // Delete work directory
168            _skinHelper.deleteQuicklyDirectory(workDir);
169        }
170        
171        if (quit)
172        {
173            // Move to work directory
174            PathUtils.moveDirectory(tempDir, workDir);
175            
176            // Remove lock
177            PathUtils.deleteQuietly(workDir.resolve(".lock"));
178        }
179        else
180        {
181            // Do a copy in work directory
182            PathUtils.copyDirectory(tempDir, workDir, file -> !file.getFileName().toString().equals(".lock"), false);
183        }
184        
185        Map<String, Object> result = new HashMap<>();
186        result.put("skinName", skinName);
187        return result;
188    }
189    
190    /**
191     * Commit the changes made to the skin
192     * @param skinName the name of the skin
193     * @param quit True to remove the temporary directory
194     * @return A map with information
195     * @throws Exception if an error occurs when committing the skin changes
196     */
197    @Callable
198    public Map<String, Object> commitChanges(String skinName, boolean quit) throws Exception
199    {
200        Skin skin = _skinManager.getSkin(skinName);
201        if (!skin.isModifiable())
202        {
203            throw new IllegalStateException("The skin '" + skinName + "' is not modifiable and thus cannot be modified.");
204        }
205        
206        Map<String, Object> lockInfos = _checkLock(skinName);
207        if (!lockInfos.isEmpty())
208        {
209            return lockInfos;
210        }
211        
212        Path skinDir = _skinHelper.getSkinDirectory(skinName);
213        
214        // Do a backup (move skin directory to backup directory)
215        Path backupDir = _skinHelper.createBackupFile(skinName);
216        
217        // Move temporary version to current skin
218        Path tempDir = _skinHelper.getTempDirectory(skinName);
219        PathUtils.moveDirectory(tempDir, skinDir);
220        
221        Path workDir = _skinHelper.getWorkDirectory(skinName);
222        if (quit)
223        {
224            // Delete work version
225            _skinHelper.deleteQuicklyDirectory(workDir);
226        }
227        else
228        {
229            // Do a copy in work directory
230            PathUtils.copyDirectory(skinDir, workDir, file -> !file.getFileName().toString().equals(".lock"), true);
231            
232            // Do a copy in temp directory
233            PathUtils.copyDirectory(skinDir, tempDir, true);
234        }
235        
236        // Delete lock file
237        PathUtils.deleteQuietly(skinDir.resolve(".lock"));
238        
239        // Invalidate caches
240        _skinHelper.invalidateCaches(skinName);
241        _skinHelper.invalidateSkinCatalogues(skinName);
242        
243        // Remove old backup (keep only the 5 last backup)
244        _skinHelper.deleteOldBackup(skinName, 5);
245        
246        Map<String, Object> result = new HashMap<>();
247        
248        result.put("backupFilename", backupDir.getFileName().toString());
249        
250        String mailSysAdmin = Config.getInstance().getValue("smtp.mail.sysadminto");
251        if (!mailSysAdmin.isEmpty())
252        {
253            result.put("adminEmail", mailSysAdmin);
254        }
255        
256        return result;
257    }
258    
259    /**
260     * Cancel the current modification to the skin
261     * @param skinName The name of the skin
262     * @param workVersion True it is the work version, false for the temp version
263     * @param toolId the id of the tool
264     * @return True if some changes were canceled
265     * @throws IOException if an error occurs while manipulating files
266     */
267    @Callable
268    public Map<String, Object> cancelChanges(String skinName, boolean workVersion, String toolId) throws IOException
269    {        
270        Map<String, Object> lockInfos = _checkLock(skinName);
271        if (!lockInfos.isEmpty())
272        {
273            return lockInfos;
274        }
275        
276        String modelBeforeCancel = _skinHelper.getTempModel(skinName);
277        
278        Path tempDir = _skinHelper.getTempDirectory(skinName);
279        if (Files.exists(tempDir))
280        {
281            // Delete current temp version
282            _skinHelper.deleteQuicklyDirectory(tempDir);
283        }
284        
285        Path workDir = _skinHelper.getWorkDirectory(skinName);
286        if (workVersion && Files.exists(workDir))
287        {
288            // Back from the last saved work version
289            PathUtils.copyDirectory(workDir, tempDir);
290        }
291        else
292        {
293            if (Files.exists(workDir))
294            {
295                // Delete work version
296                _skinHelper.deleteQuicklyDirectory(workDir);
297            }
298            
299            // Back from current skin
300            Path skinDir = _skinHelper.getSkinDirectory(skinName);
301            PathUtils.copyDirectory(skinDir, tempDir);
302        }
303        
304        String modelAfterCancel = _skinHelper.getTempModel(skinName);
305        
306        _lockManager.updateLockFile(tempDir, !toolId.isEmpty() ? toolId : "uitool-skineditor");
307        
308        // Invalidate i18n.
309        _skinHelper.invalidateTempSkinCatalogues(skinName);
310        
311        Map<String, Object> result = new HashMap<>();
312        result.put("hasChanges", modelAfterCancel == null || !modelAfterCancel.equals(modelBeforeCancel));
313        return result;
314    }
315    
316    private Map<String, Object> _checkLock (String skinName) throws IOException
317    {
318        Path tempDir = _skinHelper.getTempDirectory(skinName);
319        
320        if (!_lockManager.canWrite(tempDir))
321        {
322            Map<String, Object> result = new HashMap<>();
323            
324            UserIdentity lockOwner = _lockManager.getLockOwner(tempDir);
325            User user = _userManager.getUser(lockOwner.getPopulationId(), lockOwner.getLogin());
326
327            result.put("isLocked", true);
328            result.put("lockOwner", user != null ? user.getFullName() + " (" + lockOwner + ")" : lockOwner);
329            result.put("success", false);
330            
331            return result;
332        }
333        
334        // Not lock
335        return Collections.EMPTY_MAP;
336    }
337    
338    /**
339     * Get the model for the skin
340     * @param siteName the site name. Can be null if skinName is not null.
341     * @param skinName the skin name. Can be null if siteName is not null.
342     * @param mode the edition mode. Can be null.
343     * @return the model's name or null if there is no model for this skin
344     */
345    @Callable
346    public String getSkinModel(String siteName, String skinName, String mode)
347    {
348        String skinId = _getSkinName (siteName, skinName);
349        
350        if (__TEMP_MODE.equals(mode))
351        {
352            return _skinHelper.getTempModel(skinId);
353        }
354        else if (__WORK_MODE.equals(mode))
355        {
356            return _skinHelper.getWorkModel(skinId);
357        }
358        else
359        {
360            return _skinHelper.getSkinModel(skinId);
361        }
362    }
363    
364    private String _getSkinName (String siteName, String skinName)
365    {
366        if (StringUtils.isEmpty(skinName) && StringUtils.isNotEmpty(siteName))
367        {
368            Site site = _siteManager.getSite(siteName);
369            return site.getSkinId();
370        }
371        
372        return skinName;
373    }
374
375    /**
376     * Get lock informations on a skin
377     * @param siteName the site name. Can be null if skinName is not null.
378     * @param skinName the skin name. Can be null if siteName is not null.
379     * @return Informations about the lock
380     * @throws IOException if an error occurs
381     */
382    @Callable
383    public Map<String, Object> getLock(String siteName, String skinName) throws IOException
384    {
385        Map<String, Object> result = new HashMap<>();
386        
387        String skinId = _getSkinName(siteName, skinName);
388        
389        Path tempDir = _skinHelper.getTempDirectory(skinId);
390        if (Files.exists(tempDir) && _lockManager.isLocked(tempDir))
391        {
392            UserIdentity lockOwner = _lockManager.getLockOwner(tempDir);
393            User user = _userManager.getUser(lockOwner.getPopulationId(), lockOwner.getLogin());
394
395            result.put("isLocked", !_userProvider.getUser().equals(lockOwner));
396            result.put("lockOwner", user != null ? user.getFullName() + " (" + lockOwner + ")" : lockOwner);
397            result.put("lastModified", DateUtils.dateToString(_lockManager.lastModified(tempDir)));
398            result.put("toolId", _lockManager.getLockTool(tempDir));
399        }
400        else
401        {
402            result.put("isLocked", false);
403        }
404        
405        Path workDir = _skinHelper.getWorkDirectory(skinId);
406        if (Files.exists(workDir))
407        {
408            result.put("lastSave", DateUtils.dateToString(new Date(Files.getLastModifiedTime(workDir).toMillis())));
409        }
410        
411        return result;
412    }
413    
414    /**
415     * Revert changes and back to last work version or to current skin
416     * @param skinName The skin name
417     * @param workVersion true to get back the work version
418     * @return The skin name in case of success or lock infos if the skin is locked.
419     * @throws IOException if an error occurs
420     */
421    @Callable
422    public Map<String, Object> clearModifications (String skinName, boolean workVersion) throws IOException
423    {
424        Map<String, Object> lockInfos = _checkLock(skinName);
425        if (!lockInfos.isEmpty())
426        {
427            return lockInfos;
428        }
429        
430        Path tempDir = _skinHelper.getTempDirectory(skinName);
431        if (Files.exists(tempDir))
432        {
433            // Delete current temp version
434            _skinHelper.deleteQuicklyDirectory(tempDir);
435        }
436        
437        if (workVersion)
438        {
439            Path workDir = _skinHelper.getWorkDirectory(skinName);
440            if (Files.exists(workDir))
441            {
442                // Delete current work version
443                _skinHelper.deleteQuicklyDirectory(workDir);
444            }
445        }
446        
447        Map<String, Object> result = new HashMap<>();
448        result.put("skinName", skinName);
449        return result;
450    }
451}