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 ... Copy of the regexp in Ametys.plugins.web.alias.AliasActions.js#_delayedInitialize */ 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}