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 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 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 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 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 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}