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