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.repository.page; 017 018import java.util.HashMap; 019import java.util.Map; 020import java.util.Set; 021 022import javax.jcr.RepositoryException; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.logger.AbstractLogEnabled; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029import org.apache.commons.lang.ArrayUtils; 030import org.apache.commons.lang.StringUtils; 031import org.apache.commons.lang3.LocaleUtils; 032 033import org.ametys.cms.CmsConstants; 034import org.ametys.cms.contenttype.ContentType; 035import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.repository.DefaultContent; 038import org.ametys.cms.repository.WorkflowAwareContent; 039import org.ametys.core.observation.Event; 040import org.ametys.core.observation.ObservationManager; 041import org.ametys.core.right.RightManager; 042import org.ametys.core.right.RightManager.RightResult; 043import org.ametys.core.ui.Callable; 044import org.ametys.core.user.CurrentUserProvider; 045import org.ametys.core.user.UserIdentity; 046import org.ametys.plugins.repository.AmetysObjectResolver; 047import org.ametys.plugins.repository.AmetysRepositoryException; 048import org.ametys.plugins.repository.UnknownAmetysObjectException; 049import org.ametys.plugins.repository.jcr.JCRAmetysObject; 050import org.ametys.plugins.workflow.support.WorkflowProvider; 051import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 052import org.ametys.web.ObservationConstants; 053import org.ametys.web.repository.content.SharedContent; 054import org.ametys.web.repository.content.WebContent; 055import org.ametys.web.repository.content.jcr.DefaultSharedContent; 056import org.ametys.web.repository.content.shared.SharedContentManager; 057import org.ametys.web.repository.page.ZoneItem.ZoneType; 058import org.ametys.web.service.Service; 059import org.ametys.web.service.ServiceExtensionPoint; 060 061/** 062 * DAO for manipulating {@link Zone} and {@link ZoneItem} 063 * 064 */ 065public class ZoneDAO extends AbstractLogEnabled implements Serviceable, Component 066{ 067 /** Avalon Role */ 068 public static final String ROLE = ZoneDAO.class.getName(); 069 070 private AmetysObjectResolver _resolver; 071 private ObservationManager _observationManager; 072 private CurrentUserProvider _currentUserProvider; 073 private WorkflowProvider _workflowProvider; 074 private SharedContentManager _sharedContentManager; 075 private ContentTypesAssignmentHandler _cTypesAssignmentHandler; 076 private ServicesAssignmentHandler _serviceAssignmentHandler; 077 private ContentTypeExtensionPoint _cTypeEP; 078 private ServiceExtensionPoint _serviceEP; 079 private RightManager _rightManager; 080 081 @Override 082 public void service(ServiceManager smanager) throws ServiceException 083 { 084 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 085 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 086 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 087 _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE); 088 _sharedContentManager = (SharedContentManager) smanager.lookup(SharedContentManager.ROLE); 089 _cTypesAssignmentHandler = (ContentTypesAssignmentHandler) smanager.lookup(ContentTypesAssignmentHandler.ROLE); 090 _serviceAssignmentHandler = (ServicesAssignmentHandler) smanager.lookup(ServicesAssignmentHandler.ROLE); 091 _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 092 _serviceEP = (ServiceExtensionPoint) smanager.lookup(ServiceExtensionPoint.ROLE); 093 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 094 } 095 096 /** 097 * Insert a shared content into a zone 098 * @param pageId The page id 099 * @param zoneName The zone name 100 * @param contentId The content id to insert 101 * @param viewName The view name 102 * @return The result map 103 */ 104 @Callable 105 public Map<String, Object> addSharedContent (String pageId, String zoneName, String contentId, String viewName) 106 { 107 Map<String, Object> result = new HashMap<>(); 108 109 SitemapElement sitemapElement = _resolver.resolveById(pageId); 110 111 if (!(sitemapElement instanceof ModifiableSitemapElement mSitemapElement)) 112 { 113 throw new IllegalArgumentException("Can not add a shared content on a non-modifiable page " + pageId); 114 } 115 116 if (sitemapElement.getTemplate() == null) 117 { 118 throw new IllegalArgumentException("Page '" + pageId + "' is not a container page: unable to add a shared content"); 119 } 120 121 ModifiableZone zone; 122 if (mSitemapElement.hasZone(zoneName)) 123 { 124 zone = mSitemapElement.getZone(zoneName); 125 } 126 else 127 { 128 zone = mSitemapElement.createZone(zoneName); 129 } 130 131 Content content = _resolver.resolveById(contentId); 132 String contentSiteName = null; 133 if (content instanceof WebContent) 134 { 135 contentSiteName = ((WebContent) content).getSiteName(); 136 } 137 138 // Check that the content type is an authorized content type. 139 Set<String> validCTypes = _cTypesAssignmentHandler.getAvailableContentTypes(mSitemapElement, zoneName, true); 140 for (String contentTypeId : content.getTypes()) 141 { 142 if (!validCTypes.contains(contentTypeId)) 143 { 144 ContentType cType = _cTypeEP.getExtension(contentTypeId); 145 result.put("error", true); 146 result.put("invalid-contenttype", cType.getLabel()); 147 return result; 148 } 149 } 150 151 ZoneItem zoneItem = null; 152 if (contentSiteName == null || mSitemapElement.getSiteName().equals(contentSiteName)) 153 { 154 // The content comes from the same site as the page: insert the content as a new zone item. 155 zoneItem = addContentReference(zone, content, viewName); 156 } 157 else 158 { 159 // The content is from a different site: create a shared content in the zone. 160 if (!(content instanceof DefaultContent) || content instanceof SharedContent) 161 { 162 throw new IllegalArgumentException("The source content must be a DefaultContent but not a SharedContent."); 163 } 164 165 DefaultContent defaultContent = (DefaultContent) content; 166 167 if (!ArrayUtils.contains(defaultContent.getAllLabels(), CmsConstants.LIVE_LABEL)) 168 { 169 result.put("error", true); 170 result.put("unpublished-content", defaultContent.getTitle(LocaleUtils.toLocale(mSitemapElement.getSitemapName()))); 171 return result; 172 } 173 174 zoneItem = createSharedContent(zone, defaultContent, viewName); 175 } 176 177 mSitemapElement.saveChanges(); 178 179 String zoneItemId = zoneItem.getId(); 180 result.put("zoneItemId", zoneItemId); 181 182 Map<String, Object> eventParams = new HashMap<>(); 183 eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, mSitemapElement); 184 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId); 185 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT); 186 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_CONTENT, zoneItem.getContent()); 187 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), eventParams)); 188 189 return result; 190 } 191 192 /** 193 * Add the given content as a zone item in the given zone. 194 * @param zone the zone to add the content in. 195 * @param content the content to add. 196 * @param viewName the view name. 197 * @return the added zone item 198 */ 199 protected ZoneItem addContentReference(ModifiableZone zone, Content content, String viewName) 200 { 201 ModifiableZoneItem zoneItem = zone.addZoneItem(); 202 zoneItem.setType(ZoneType.CONTENT); 203 204 zoneItem.setContent(content); 205 zoneItem.setViewName(viewName); 206 207 return zoneItem; 208 } 209 210 /** 211 * Create a shared content referencing the given content and add the shared one to the zone. 212 * @param zone the zone to create the shared content in. 213 * @param originalContent the original content. 214 * @param viewName the view name. 215 * @return the added zone item 216 */ 217 protected ZoneItem createSharedContent(ModifiableZone zone, DefaultContent originalContent, String viewName) 218 { 219 ModifiableZoneItem zoneItem = zone.addZoneItem(); 220 zoneItem.setType(ZoneType.CONTENT); 221 222 DefaultSharedContent content = _sharedContentManager.createSharedContent(zone.getSitemapElement().getSite(), originalContent); 223 224 zoneItem.setContent(content); 225 zoneItem.setViewName(viewName); 226 227 return zoneItem; 228 } 229 230 /** 231 * Remove a zone item from page 232 * @param zoneItemId The id of zone item to delete 233 */ 234 @Callable 235 public void removeZoneItem (String zoneItemId) 236 { 237 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 238 239 if (!(zoneItem instanceof ModifiableZoneItem)) 240 { 241 throw new IllegalArgumentException("Can not remove a non-modifiable zone item " + zoneItemId); 242 } 243 244 ModifiableSitemapElement sitemapElement = (ModifiableSitemapElement) zoneItem.getZone().getSitemapElement(); 245 246 if (sitemapElement instanceof LockablePage lockablePage && lockablePage.isLocked()) 247 { 248 throw new IllegalArgumentException("Can not remove a zone item from a locked page " + lockablePage.getId()); 249 } 250 251 Map<String, Object> eventParams = new HashMap<>(); 252 eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 253 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId); 254 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType()); 255 if (zoneItem.getType() == ZoneType.CONTENT) 256 { 257 // In case of a zone item of type content, provide it. 258 // There should be no problem as the observers are processed synchronously and, 259 // if the user want it to be deleted, it will happen in a future client call. 260 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_CONTENT, zoneItem.getContent()); 261 } 262 else if (zoneItem.getType() == ZoneType.SERVICE) 263 { 264 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_SERVICE, zoneItem.getServiceId()); 265 } 266 267 ((ModifiableZoneItem) zoneItem).remove(); 268 sitemapElement.saveChanges(); 269 270 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_DELETED, _currentUserProvider.getUser(), eventParams)); 271 } 272 273 /** 274 * Get the unreferenced contents of a {@link Page} or a {@link ZoneItem} 275 * @param id The id of page or zone item 276 * @return The list of unreferenced contents 277 */ 278 @Callable 279 public Map<String, Object> getZoneItemElementInfo(String id) 280 { 281 ZoneItem zoneItem = _resolver.resolveById(id); 282 283 Map<String, Object> info = new HashMap<>(); 284 info.put("type", zoneItem.getType().toString().toLowerCase()); 285 286 Content content = getContent(zoneItem); 287 if (content != null) 288 { 289 info.put("id", content.getId()); 290 info.put("name", content.getName()); 291 info.put("title", content.getTitle(LocaleUtils.toLocale(zoneItem.getZone().getSitemapElement().getSitemapName()))); 292 info.put("isNew", _isNew(content)); 293 info.put("isShared", content instanceof SharedContent); 294 info.put("hasShared", _sharedContentManager.hasSharedContents(content)); 295 info.put("isReferenced", _isReferenced(content)); 296 } 297 else 298 { 299 Service service = getService(zoneItem); 300 if (service != null) 301 { 302 info.put("id", service.getId()); 303 info.put("label", service.getLabel()); 304 info.put("url", service.getURL()); 305 } 306 } 307 308 return info; 309 } 310 311 /** 312 * Get the content of a zone item. Can be null if zone item is a service zone item. 313 * @param zoneItem The zone item 314 * @return The content or null if zone item is a service zone item. 315 */ 316 public Content getContent (ZoneItem zoneItem) 317 { 318 if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT) 319 { 320 return zoneItem.getContent(); 321 } 322 return null; 323 } 324 325 private boolean _isReferenced (Content content) 326 { 327 return content instanceof WebContent && ((WebContent) content).getReferencingPages().size() > 1; 328 } 329 330 private boolean _isNew (Content content) 331 { 332 boolean isNew = false; 333 if (content instanceof WorkflowAwareContent) 334 { 335 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 336 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 337 338 long workflowId = waContent.getWorkflowId(); 339 isNew = workflow.getHistorySteps(workflowId).isEmpty(); 340 } 341 return isNew; 342 } 343 344 /** 345 * Get the service of a zone item. Can be null if zone item is not a service zone item. 346 * @param zoneItem The zone item 347 * @return The service or null if zone item is not a service zone item. 348 */ 349 public Service getService (ZoneItem zoneItem) 350 { 351 if (zoneItem.getType() == ZoneItem.ZoneType.SERVICE) 352 { 353 return _serviceEP.getExtension(zoneItem.getServiceId()); 354 } 355 return null; 356 } 357 358 /** 359 * Move a zone item of a page before/after another zone item of the same page 360 * @param zoneItemId The identifier of the zone item to move 361 * @param zoneName The destination zone name 362 * @param beforeOrAfter true if before or false after 363 * @param beforeOrAfterItemId The target zone item can be null 364 * @param pageId The concerned page id 365 * @return true when success 366 * @throws UnknownAmetysObjectException If an error occurred 367 * @throws AmetysRepositoryException If an error occurred 368 * @throws RepositoryException If an error occurred 369 */ 370 @Callable 371 public boolean moveZoneItemTo(String zoneItemId, String zoneName, boolean beforeOrAfter, String beforeOrAfterItemId, String pageId) throws UnknownAmetysObjectException, AmetysRepositoryException, RepositoryException 372 { 373 UserIdentity user = _currentUserProvider.getUser(); 374 375 ModifiableSitemapElement sitemapElement = _resolver.resolveById(pageId); 376 377 Zone zone; 378 if (!sitemapElement.hasZone(zoneName)) 379 { 380 zone = sitemapElement.createZone(zoneName); 381 } 382 else 383 { 384 zone = sitemapElement.getZone(zoneName); 385 } 386 387 ZoneItem zoneItem = sitemapElement instanceof JCRAmetysObject ? (ZoneItem) _resolver.resolveById(zoneItemId, ((JCRAmetysObject) sitemapElement).getNode().getSession()) : (ZoneItem) _resolver.resolveById(zoneItemId); 388 389 _checkZoneItem(sitemapElement, zoneName, zoneItem, user); 390 391 _checkArguments(zoneItemId, zoneName, pageId, user, sitemapElement, zone, zoneItem); 392 393 ((ModifiableZoneItem) zoneItem).moveTo(zone, false); 394 395 // For the moment the zoneItem is at the end 396 if (StringUtils.isNotBlank(beforeOrAfterItemId)) 397 { 398 if (beforeOrAfter) 399 { 400 ZoneItem futureFollowingZoneItem = _resolver.resolveById(beforeOrAfterItemId); 401 ((ModifiableZoneItem) zoneItem).orderBefore(futureFollowingZoneItem); 402 } 403 else 404 { 405 boolean isNext = false; 406 for (ZoneItem zi : zone.getZoneItems()) 407 { 408 if (isNext) 409 { 410 ((ModifiableZoneItem) zoneItem).orderBefore(zi); 411 break; 412 } 413 else if (beforeOrAfterItemId.equals(zi.getId())) 414 { 415 isNext = true; 416 } 417 } 418 } 419 } 420 421 ModifiableSitemapElement zoneItemPage = (ModifiableSitemapElement) zoneItem.getZone().getSitemapElement(); 422 if (zoneItemPage.needsSave()) 423 { 424 zoneItemPage.saveChanges(); 425 } 426 427 Map<String, Object> eventParams = new HashMap<>(); 428 eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 429 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId); 430 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType()); 431 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_MOVED, user, eventParams)); 432 433 return true; 434 } 435 436 /** 437 * Check if the zone item can be moved to the given zone of the given page. 438 * @param page the page. 439 * @param zoneName the zone name. 440 * @param zoneItem the zone item to move. 441 * @param user the user 442 */ 443 protected void _checkZoneItem(ModifiableSitemapElement page, String zoneName, ZoneItem zoneItem, UserIdentity user) 444 { 445 if (ZoneType.CONTENT.equals(zoneItem.getType())) 446 { 447 String[] contentTypes = zoneItem.getContent().getTypes(); 448 449 Set<String> availableCTypes = _cTypesAssignmentHandler.getAvailableContentTypes(page, zoneName, true); 450 451 for (String contentType : contentTypes) 452 { 453 if (!availableCTypes.contains(contentType)) 454 { 455 throw new IllegalArgumentException("The user '" + user + " illegally tried to move a content of type '" + contentType + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'."); 456 } 457 } 458 } 459 else if (ZoneType.SERVICE.equals(zoneItem.getType())) 460 { 461 String serviceId = zoneItem.getServiceId(); 462 463 Set<String> services = _serviceAssignmentHandler.getAvailableServices(page, zoneName); 464 if (!services.contains(serviceId)) 465 { 466 throw new IllegalArgumentException("The user '" + user + " illegally tried to move a service of id '" + serviceId + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'."); 467 } 468 } 469 } 470 471 private void _checkArguments(String zoneItemId, String zoneName, String pageId, UserIdentity user, ModifiableSitemapElement page, Zone zone, ZoneItem zoneItem) 472 { 473 if (_rightManager.hasRight(user, "Web_Rights_Page_OrganizeZoneItem", page) != RightResult.RIGHT_ALLOW) 474 { 475 throw new IllegalArgumentException("User '" + user + "' tryed without convinient privileges to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !"); 476 } 477 else if (zoneItem == null) 478 { 479 throw new IllegalArgumentException("User '" + user + "' tryed to move unexisting zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !"); 480 } 481 else if (zone == null) 482 { 483 throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to an unexisting zone '" + zoneName + "' of the page '" + pageId + "' !"); 484 } 485 else if (!(zone instanceof ModifiableZone)) 486 { 487 throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' but this zone is not modifiable !"); 488 } 489 else if (!(zoneItem instanceof ModifiableZoneItem)) 490 { 491 throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' but this zone item is not modifiable !"); 492 } 493 } 494}