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.alias;
017
018import java.time.LocalDate;
019import java.time.ZoneId;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import javax.jcr.RepositoryException;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.logger.AbstractLogEnabled;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.commons.lang.StringUtils;
036
037import org.ametys.core.ui.Callable;
038import org.ametys.core.util.DateUtils;
039import org.ametys.plugins.repository.AmetysObject;
040import org.ametys.plugins.repository.AmetysObjectIterable;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.ModifiableAmetysObject;
043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
044import org.ametys.plugins.repository.TraversableAmetysObject;
045import org.ametys.plugins.repository.UnknownAmetysObjectException;
046import org.ametys.web.alias.Alias.TargetType;
047import org.ametys.web.repository.page.Page;
048import org.ametys.web.repository.site.SiteManager;
049
050/**
051 * Class managing {@link Alias} creation, modification, deletion and moving
052 */
053public class AliasDAO extends AbstractLogEnabled implements Component, Serviceable
054{
055    /** The component's role */
056    public static final String ROLE = AliasDAO.class.getName();
057    
058    /** The PAGE pattern */
059    public static final Pattern PAGE_PATTERN = Pattern.compile("/([^?]+)\\.html");
060    /** The URL pattern */
061    public static final Pattern URL_PATTERN = Pattern.compile("/.+");
062    /** The URL pattern ... Copy of the regexp in Ametys.plugins.web.alias.AliasActions.js#_delayedInitialize */
063    public static final Pattern TARGET_URL_PATTERN = Pattern.compile("(https?://|/).+");
064    /** The alias default name */
065    public static final String DEFAULT_ALIAS_NAME = "alias";
066    
067    /** The Ametys object resolver */
068    private AmetysObjectResolver _ametysObjectResolver;
069    /** The site manager */
070    private SiteManager _siteManager;
071    
072    public void service(ServiceManager smanager) throws ServiceException
073    {
074        _ametysObjectResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
075        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
076    }
077    
078    /**
079     * Get an alias
080     * @param id the id of the alias to get
081     * @return the JSON representation of the alias
082     */
083    @Callable (rights = "Web_Rights_Alias_Handle")
084    public Map<String, Object> getAlias(String id)
085    {
086        DefaultAlias alias = _ametysObjectResolver.resolveById(id);
087        return alias2Json(alias);
088    }
089    
090    /**
091     * Create an alias
092     * @param type the type (page or url)
093     * @param url the origin url
094     * @param target the target url
095     * @param siteName the site's name
096     * @param dateStr the optional expiration date of the alias
097     * @return a map
098     */
099    @Callable (rights = "Web_Rights_Alias_Handle")
100    public Map<String, String> createAlias(String type, String url, String target, String siteName, String dateStr)
101    {
102        Map<String, String> result = new HashMap<>();
103        TargetType targetType = TargetType.valueOf(type);
104        
105        // Check if the alias does not already exist
106        if (_checkExistence(siteName, url))
107        {
108            result.put("msg", "already-exists");
109            return result;
110        }
111        
112        if (!isValidUrl(url))
113        {
114            result.put("msg", "invalid-url");
115            return result;
116        }
117        
118        if (TargetType.URL.equals(TargetType.valueOf(type)) && !isValidTargetUrl(target))
119        {
120            result.put("msg", "invalid-target-url");
121            return result;
122        }
123        
124        ModifiableTraversableAmetysObject rootNode = AliasHelper.getRootNode(_siteManager.getSite(siteName));
125        
126        String aliasName = AliasHelper.getAliasNextUniqueName(rootNode);
127        
128        DefaultAlias alias = rootNode.createChild(aliasName, "ametys:alias");
129        alias.setUrl(url);
130        alias.setTarget(target);
131        alias.setType(targetType);
132        alias.setCreationDate(new Date());
133        if (StringUtils.isNotEmpty(dateStr))
134        {
135            Date date = DateUtils.parse(dateStr);
136            alias.setExpirationDate(date);
137        }
138        
139        rootNode.saveChanges();
140        
141        result.put("id", alias.getId());
142        
143        return result;
144    }
145    
146    /**
147     * Update an alias
148     * @param id the id of the alias to update
149     * @param type the type (page or url)
150     * @param url the origin url
151     * @param target the target url
152     * @param siteName the site's name
153     * @param dateStr the optional expiration date of the alias
154     * @return a map
155     */
156    @Callable (rights = "Web_Rights_Alias_Handle")
157    public Map<String, String> updateAlias(String id, String type, String url, String target, String siteName, String dateStr)
158    {
159        Map<String, String> result = new HashMap<>();
160        
161        // Check the alias does not already exist
162        if (_checkExistence(id, siteName, url))
163        {
164            result.put("msg", "already-exists");
165            return result;
166        }
167        
168        if (!isValidUrl(url))
169        {
170            result.put("msg", "invalid-url");
171            return result;
172        }
173        
174        try
175        {
176            DefaultAlias alias = _ametysObjectResolver.resolveById(id);
177            
178            if (TargetType.URL.equals(alias.getType()) && !isValidTargetUrl(target))
179            {
180                result.put("msg", "invalid-target-url");
181                return result;
182            }
183            
184            alias.setUrl(url);
185            alias.setTarget(target);
186            if (StringUtils.isNotEmpty(dateStr))
187            {
188                Date date = DateUtils.parse(dateStr);
189                alias.setExpirationDate(date);
190            }
191            else
192            {
193                alias.removeExpirationDate();
194            }
195            
196            alias.saveChanges();
197            
198            result.put("id", alias.getId());
199        }
200        catch (UnknownAmetysObjectException e)
201        {
202            result.put("msg", "unknown-alias");
203            getLogger().error("Unable to edit alias. The alias of id '" + id + " doesn't exist", e);
204        }
205        
206        return result;
207    }
208    
209    /**
210     * Delete an alias
211     * @param ids the list of ids of aliases to delete
212     * @return a map
213     */
214    @Callable (rights = "Web_Rights_Alias_Handle")
215    public Map<String, String> deleteAlias(List<String> ids)
216    {
217        Map<String, String> result = new HashMap<>();
218        for (String id : ids)
219        {
220            try
221            {
222                DefaultAlias alias = _ametysObjectResolver.resolveById(id);
223                ModifiableAmetysObject parent = alias.getParent();
224                alias.remove(); 
225                
226                parent.saveChanges();
227            }
228            catch (UnknownAmetysObjectException e)
229            {
230                result.put("msg", "unknown-alias");
231                getLogger().error("Unable to delete alias. The alias of id '" + id + " doesn't exist", e);
232            }
233        }
234        
235        return result;
236    }
237    
238    /**
239     * Move an alias
240     * @param id the id of the alias to move
241     * @param role the action to perform
242     * @return an empty map
243     * @throws RepositoryException if an error occurs
244     */
245    @Callable (rights = "Web_Rights_Alias_Handle")
246    public Map<String, Object> moveAlias(String id, String role) throws RepositoryException
247    {
248        Map<String, Object> result = new HashMap<> ();
249         
250        DefaultAlias alias = _ametysObjectResolver.resolveById(id);
251        
252        if ("move-first".equals(role))
253        {
254            _moveFirst(alias);
255        }
256        else if ("move-up".equals(role))
257        {
258            _moveUp(alias);
259        }
260        else if ("move-down".equals(role))
261        {
262            _moveDown(alias);
263        }
264        else if ("move-last".equals(role))
265        {
266            _moveLast(alias);
267        }
268        
269        return result;
270    }
271    
272    
273    /**
274     * Move first.
275     * @param alias the alias to move 
276     * @throws RepositoryException if an errors occurs while moving
277     */
278    private void _moveFirst(DefaultAlias alias) throws RepositoryException
279    {
280        try (AmetysObjectIterable<AmetysObject>  children = ((TraversableAmetysObject) alias.getParent()).getChildren();)
281        {
282            // Resolve the link in the same session or the linkRoot.saveChanges() call below won't see the order changes.
283            alias.orderBefore(((TraversableAmetysObject) alias.getParent()).getChildren().iterator().next());
284            ((ModifiableAmetysObject) alias.getParent()).saveChanges();
285        }
286    }
287
288    /**
289     * Move down.
290     * @param alias the alias to move 
291     * @throws RepositoryException if an errors occurs while moving
292     */
293    private void _moveDown(DefaultAlias alias) throws RepositoryException
294    {
295        TraversableAmetysObject parentNode = alias.getParent();
296        boolean iterate = true;
297
298        try (AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();)
299        {
300            Iterator<AmetysObject> it = siblings.iterator();
301            while (it.hasNext() && iterate)
302            {
303                DefaultAlias sibling = (DefaultAlias) it.next();
304                iterate = !sibling.getName().equals(alias.getName());
305            }
306            
307            // Move the link after his next sibling: move the next sibling before the link to move.
308            DefaultAlias nextLink = (DefaultAlias) it.next();
309            nextLink.orderBefore(alias);
310    
311            alias.saveChanges();
312        }
313    }
314    
315    /**
316     * Move up.
317     * @param alias the alias to move
318     * @throws RepositoryException if an errors occurs while moving
319     */
320    private void _moveUp(DefaultAlias alias) throws RepositoryException
321    {
322        TraversableAmetysObject parentNode = alias.getParent();
323        DefaultAlias previousLink = null;
324
325        try (AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();)
326        {
327            Iterator<AmetysObject> it = siblings.iterator();
328            while (it.hasNext())
329            {
330                DefaultAlias sibling = (DefaultAlias) it.next();
331                if (sibling.getName().equals(alias.getName()))
332                {
333                    break;
334                }
335                
336                previousLink = sibling;
337            }
338                
339            // Move the link after his next sibling: move the next sibling before the link to move.
340            alias.orderBefore(previousLink);
341            alias.saveChanges();
342        }
343    }
344    
345    /**
346     * Move last.
347     * @param link the alias to move 
348     * @throws RepositoryException if an errors occurs while moving
349     */
350    private void _moveLast(DefaultAlias link) throws RepositoryException
351    {
352        link.moveTo(link.getParent(), false);
353        ((ModifiableAmetysObject) link.getParent()).saveChanges();
354    }
355    
356    
357    /**
358     * Checks the existence of an alias with same URL
359     * @param id the alias
360     * @param siteName The site name
361     * @param url The alias URL
362     * @return true if an alias with the same URL exists
363     */
364    private boolean _checkExistence (String id, String siteName, String url)
365    {
366        String xpathQuery = AliasHelper.getXPath(siteName, url);
367
368        try (AmetysObjectIterable<DefaultAlias> aliases = _ametysObjectResolver.query(xpathQuery);)
369        {
370            Iterator<DefaultAlias> it = aliases.iterator();
371            while (it.hasNext())
372            {
373                DefaultAlias alias = it.next();
374                if (!id.equals(alias.getId()))
375                {
376                    return true;
377                }
378            }
379            return false;
380        }
381        catch (Exception e)
382        {
383            return false;
384        }
385    }
386    
387    /**
388     * Checks the existence of an alias
389     * @param siteName The site name
390     * @param url The alias URL
391     * @return true if an alias with the same URL exists
392     */
393    private boolean _checkExistence (String siteName, String url)
394    {
395        try
396        {
397            String xpathQuery = AliasHelper.getXPath(siteName, url);
398            return _ametysObjectResolver.query(xpathQuery).iterator().hasNext();
399        }
400        catch (Exception e)
401        {
402            return false;
403        }
404    }
405    
406    /**
407     * Validates the url 
408     * @param url the url to check
409     * @return true if the url is valid
410     */
411    private boolean isValidUrl (String url)
412    {
413        Matcher matcher = URL_PATTERN.matcher(url);
414        return matcher.matches();
415    }
416    
417    /**
418     * Validates the url 
419     * @param url the url to check
420     * @return true if the url is valid
421     */
422    private boolean isValidTargetUrl (String url)
423    {
424        Matcher matcher = TARGET_URL_PATTERN.matcher(url);
425        return matcher.matches();
426    }
427    
428    /**
429     * Represent an {@link Alias} in JSON
430     * @param alias The alias 
431     * @return the alias in JSON
432     */
433    public Map<String, Object> alias2Json (DefaultAlias alias) 
434    {
435        Map<String, Object> aliasJson = new HashMap<>();
436        aliasJson.put("id", alias.getId());
437        aliasJson.put("url", alias.getUrl());
438        aliasJson.put("target", alias.getTarget());
439
440        TargetType type = alias.getType();
441        aliasJson.put("type", String.valueOf(type));
442        
443        String target = alias.getTarget();
444        
445        if (TargetType.PAGE.equals(type))
446        {
447            try
448            {
449                Page page = _ametysObjectResolver.resolveById(target);
450                aliasJson.put("targetUrl", "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html");
451            }
452            catch (UnknownAmetysObjectException e)
453            {
454                getLogger().warn("Alias '" + alias.getUrl() + "' redirect to an unknown page of id '" + target + "'", e);
455                aliasJson.put("targetUrl", "unknown");
456            }
457        }
458        else
459        {
460            aliasJson.put("targetUrl", target);
461        }
462        
463        aliasJson.put("createAt", DateUtils.dateToString(alias.getCreationDate()));
464
465        Date expirationDate = alias.getExpirationDate();
466        aliasJson.put("expirationDate", expirationDate != null ? DateUtils.dateToString(expirationDate) : "");
467        
468        if (expirationDate != null)
469        {
470            LocalDate localExpDate = expirationDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
471            LocalDate now = LocalDate.now();
472            
473            aliasJson.put("expired", now.compareTo(localExpDate) > 0);
474        }
475        else
476        {
477            aliasJson.put("expired", false);
478        }
479        
480        return aliasJson;
481    }
482}