001/* 002 * Copyright 2017 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.plugins.userdirectory.clientsideelement; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.stream.Collectors; 026 027import javax.jcr.Node; 028import javax.jcr.RepositoryException; 029import javax.jcr.Value; 030 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.jackrabbit.value.StringValue; 035 036import org.ametys.cms.contenttype.ContentType; 037import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 038import org.ametys.core.observation.Event; 039import org.ametys.core.observation.ObservationManager; 040import org.ametys.core.ui.Callable; 041import org.ametys.core.util.LambdaUtils; 042import org.ametys.plugins.repository.AmetysObjectResolver; 043import org.ametys.plugins.repository.jcr.JCRAmetysObject; 044import org.ametys.plugins.userdirectory.OrganisationChartPageHandler; 045import org.ametys.plugins.userdirectory.UserDirectoryHelper; 046import org.ametys.plugins.userdirectory.observation.ObservationConstants; 047import org.ametys.plugins.userdirectory.page.VirtualOrganisationChartPageFactory; 048import org.ametys.runtime.i18n.I18nizableText; 049import org.ametys.runtime.i18n.I18nizableTextParameter; 050import org.ametys.web.clientsideelement.AbstractPageClientSideElement; 051import org.ametys.web.repository.page.LockablePage; 052import org.ametys.web.repository.page.ModifiablePage; 053import org.ametys.web.repository.page.Page; 054import org.ametys.web.rights.PageRightAssignmentContext; 055 056/** 057 * Client side element for a controller wich set/remove the organisation chart root page 058 */ 059public class SetOrganisationChartRootClientSideElement extends AbstractPageClientSideElement 060{ 061 /** Observer manager. */ 062 protected ObservationManager _observationManager; 063 064 /** The organisation chart page handler */ 065 protected OrganisationChartPageHandler _pageHandler; 066 067 /** The extension point for content types */ 068 protected ContentTypeExtensionPoint _contentTypeEP; 069 070 /** The organization chart page handler */ 071 protected OrganisationChartPageHandler _orgUnitPageHandler; 072 073 @Override 074 public void service(ServiceManager smanager) throws ServiceException 075 { 076 super.service(smanager); 077 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 078 _pageHandler = (OrganisationChartPageHandler) smanager.lookup(OrganisationChartPageHandler.ROLE); 079 _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 080 _orgUnitPageHandler = (OrganisationChartPageHandler) smanager.lookup(OrganisationChartPageHandler.ROLE); 081 } 082 /** 083 * Gets the status of the given page 084 * @param pageId The page id 085 * @return the status of the given page 086 */ 087 @Callable (rights = Callable.NO_CHECK_REQUIRED) 088 public Map<String, Object> getStatus(String pageId) 089 { 090 // Assume that no read access is checked (required to update client side element status) 091 092 Map<String, Object> result = new HashMap<>(); 093 094 Map<String, Object> parameters = this._script.getParameters(); 095 096 Page page = _resolver.resolveById(pageId); 097 098 if (page instanceof JCRAmetysObject) 099 { 100 if (_pageHandler.isOrganisationChartRootPage((JCRAmetysObject) page)) 101 { 102 List<String> i18nParameters = new ArrayList<>(); 103 i18nParameters.add(page.getTitle()); 104 105 I18nizableText ed = (I18nizableText) parameters.get("organisation-chart-page-description"); 106 I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), i18nParameters); 107 result.put("organisation-chart-page-title", msg); 108 109 ed = (I18nizableText) parameters.get("remove-organisation-chart-page-description"); 110 msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), i18nParameters); 111 result.put("remove-organisation-chart-page-title", msg); 112 113 String contentTypeId = page.getValue(OrganisationChartPageHandler.CONTENT_TYPE_DATA_NAME, StringUtils.EMPTY); 114 115 if (StringUtils.isNotEmpty(contentTypeId)) 116 { 117 I18nizableText contentTypeText = _contentTypeEP.hasExtension(contentTypeId) ? _contentTypeEP.getExtension(contentTypeId).getLabel() : new I18nizableText(contentTypeId); 118 119 Map<String, I18nizableTextParameter> contentTypeI18nParameters = new HashMap<>(); 120 contentTypeI18nParameters.put("0", contentTypeText); 121 122 ed = (I18nizableText) parameters.get("contenttype-organisation-chart-page-description"); 123 msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), contentTypeI18nParameters); 124 result.put("contenttype-organisation-chart-page-description", msg); 125 } 126 127 result.put("organisation-chart-page-id", new I18nizableText(page.getId())); 128 } 129 else 130 { 131 List<String> i18nParameters = new ArrayList<>(); 132 i18nParameters.add(page.getTitle()); 133 134 I18nizableText ed = (I18nizableText) parameters.get("add-organisation-chart-page-description"); 135 I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), i18nParameters); 136 137 result.put("add-organisation-chart-page-id", new I18nizableText(page.getId())); 138 result.put("add-organisation-chart-page-title", msg); 139 } 140 } 141 else 142 { 143 List<String> noJcrI18nParameters = new ArrayList<>(); 144 noJcrI18nParameters.add(page.getTitle()); 145 146 I18nizableText ed = (I18nizableText) parameters.get("no-jcr-page-description"); 147 I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), noJcrI18nParameters); 148 149 result.put("no-jcr-page-id", new I18nizableText(page.getId())); 150 result.put("no-jcr-page-title", msg); 151 } 152 153 return result; 154 } 155 156 /** 157 * Sets the given page as the root of a organization chart 158 * @param pageId The id of the page 159 * @param contentType The id of the content type 160 * @param pageVisible true to make the organization page visible 161 * @return A result map 162 * @throws RepositoryException if a repository error occurred 163 */ 164 @Callable (rights = "User_Directory_Right_Organisation_Chart_SetRoot", rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 165 public Map<String, Object> setOrganisationChartRoot(String pageId, String contentType, boolean pageVisible) throws RepositoryException 166 { 167 Map<String, Object> result = new HashMap<>(); 168 if (!_contentTypeEP.isSameOrDescendant(contentType, UserDirectoryHelper.ORGUNIT_CONTENT_TYPE)) 169 { 170 result.put("error", "invalid-content-type"); 171 return result; 172 } 173 174 Page page = _resolver.resolveById(pageId); 175 176 if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 177 { 178 throw new IllegalStateException("Cannot set the locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' as the root of a organization chart"); 179 } 180 181 String oldContentType = page.getValue(OrganisationChartPageHandler.CONTENT_TYPE_DATA_NAME, StringUtils.EMPTY); 182 boolean oldVisiblity = page.getValue(OrganisationChartPageHandler.PAGE_VISIBLE_DATA_NAME, true); 183 184 // Do nothing if page attribute are the same 185 if (!oldContentType.equals(contentType) || oldVisiblity != pageVisible) 186 { 187 Set<Page> currentOrgUnitPages = _orgUnitPageHandler.getOrganisationChartRootPages(page.getSiteName(), page.getSitemapName()); 188 189 Map<String, Object> eventParams = new HashMap<>(); 190 eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page); 191 192 if (currentOrgUnitPages.contains(page)) 193 { 194 // Unindex pages for all workspaces before the properties changed 195 _observationManager.notify(new Event(ObservationConstants.EVENT_ORGANISATION_CHART_ROOT_UPDATING, _currentUserProvider.getUser(), eventParams)); 196 197 _updateOrgUnitRootProperty(page, contentType, pageVisible); 198 } 199 else 200 { 201 _addOrgUnitRootProperty(page, contentType, pageVisible); 202 } 203 204 205 // Live synchronization 206 _notifyPageUpdated(page); 207 208 // Indexation 209 _observationManager.notify(new Event(ObservationConstants.EVENT_ORGANISATION_CHART_ROOT_UPDATED, _currentUserProvider.getUser(), eventParams)); 210 } 211 212 return result; 213 } 214 private void _addOrgUnitRootProperty(Page page, String contentType, boolean pageVisible) throws RepositoryException 215 { 216 if (page instanceof JCRAmetysObject) 217 { 218 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 219 Node node = jcrPage.getNode(); 220 221 List<Value> values = new ArrayList<>(); 222 if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY)) 223 { 224 values.addAll(Arrays.asList(node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues())); 225 } 226 227 StringValue virtualOrgUnitPageFactoryClassName = new StringValue(VirtualOrganisationChartPageFactory.class.getName()); 228 if (!values.contains(virtualOrgUnitPageFactoryClassName)) 229 { 230 values.add(virtualOrgUnitPageFactoryClassName); 231 } 232 233 node.setProperty(AmetysObjectResolver.VIRTUAL_PROPERTY, values.toArray(new Value[values.size()])); 234 235 // Set the organisation chart root property 236 if (page instanceof ModifiablePage) 237 { 238 ModifiablePage modifiablePage = (ModifiablePage) page; 239 modifiablePage.setValue(OrganisationChartPageHandler.CONTENT_TYPE_DATA_NAME, contentType); 240 modifiablePage.setValue(OrganisationChartPageHandler.PAGE_VISIBLE_DATA_NAME, pageVisible); 241 } 242 243 jcrPage.saveChanges(); 244 } 245 } 246 247 private void _updateOrgUnitRootProperty(Page page, String contentType, boolean pageVisible) 248 { 249 if (page instanceof ModifiablePage) 250 { 251 ModifiablePage modifiablePage = (ModifiablePage) page; 252 253 // Set the organisation chart root property 254 modifiablePage.setValue(OrganisationChartPageHandler.CONTENT_TYPE_DATA_NAME, contentType); 255 modifiablePage.setValue(OrganisationChartPageHandler.PAGE_VISIBLE_DATA_NAME, pageVisible); 256 257 modifiablePage.saveChanges(); 258 } 259 } 260 /** 261 * Remove the organization chart root status to the given page 262 * @param pageId The id of the page 263 * @return A result map 264 * @throws RepositoryException if a repository error occurred 265 */ 266 @Callable (rights = "User_Directory_Right_Organisation_Chart_SetRoot", rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 267 public Map<String, Object> removeOrganisationChartRoot(String pageId) throws RepositoryException 268 { 269 Map<String, Object> result = new HashMap<>(); 270 271 Page page = _resolver.resolveById(pageId); 272 273 if (page instanceof JCRAmetysObject) 274 { 275 if (!_pageHandler.isOrganisationChartRootPage((JCRAmetysObject) page)) 276 { 277 result.put("error", "no-root"); 278 return result; 279 } 280 281 if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 282 { 283 throw new IllegalStateException("Cannot unset root status from a locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'"); 284 } 285 286 Map<String, Object> eventParams = new HashMap<>(); 287 eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page); 288 289 // Unindex pages for all workspaces before the properties were removed 290 _observationManager.notify(new Event(ObservationConstants.EVENT_ORGANISATION_CHART_ROOT_DELETING, _currentUserProvider.getUser(), eventParams)); 291 292 _removeOrgUnitRootProperty(page); 293 294 _notifyPageUpdated(page); 295 296 // After live synchronization 297 _observationManager.notify(new Event(ObservationConstants.EVENT_ORGANISATION_CHART_ROOT_DELETED, _currentUserProvider.getUser(), eventParams)); 298 } 299 else 300 { 301 result.put("error", "no-root"); 302 } 303 return result; 304 } 305 306 307 /** 308 * Gets the content types which can build an organisation chart 309 * @param pageId The id of the page being edited 310 * @return the content types which can build an organisation chart 311 */ 312 @Callable (rights = "User_Directory_Right_Organisation_Chart_SetRoot", rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 313 public List<Map<String, Object>> getSupportedContentTypes(String pageId) 314 { 315 List<Map<String, Object>> result = new ArrayList<>(); 316 Page page = _resolver.resolveById(pageId); 317 318 Set<String> orgUnitContentTypes = new HashSet<>(); 319 320 orgUnitContentTypes.add(UserDirectoryHelper.ORGUNIT_CONTENT_TYPE); 321 orgUnitContentTypes.addAll(_contentTypeEP.getSubTypes(UserDirectoryHelper.ORGUNIT_CONTENT_TYPE)); 322 323 for (String contentTypeId : orgUnitContentTypes) 324 { 325 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 326 Page orgUnitRootPage = _orgUnitPageHandler.getOrganisationChartRootPage(page.getSiteName(), page.getSitemapName(), contentTypeId); 327 if (!contentType.isAbstract() && (orgUnitRootPage == null || orgUnitRootPage.equals(page))) 328 { 329 // The content type is not already a root of an organisation chart or is the root of the currently edited page 330 Map<String, Object> entry = new HashMap<>(); 331 entry.put("value", contentType.getId()); 332 entry.put("text", contentType.getLabel()); 333 result.add(entry); 334 } 335 } 336 337 return result; 338 } 339 340 private void _removeOrgUnitRootProperty(Page page) throws RepositoryException 341 { 342 if (page instanceof JCRAmetysObject) 343 { 344 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 345 Node node = jcrPage.getNode(); 346 347 if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY)) 348 { 349 List<Value> values = new ArrayList<>(Arrays.asList(node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues())); 350 int index = values.stream() 351 .map(LambdaUtils.wrap(Value::getString)) 352 .collect(Collectors.toList()) 353 .indexOf(VirtualOrganisationChartPageFactory.class.getName()); 354 355 if (index != -1) 356 { 357 values.remove(index); 358 node.setProperty(AmetysObjectResolver.VIRTUAL_PROPERTY, values.toArray(new Value[values.size()])); 359 } 360 361 // Remove the organization chart root property 362 if (page instanceof ModifiablePage) 363 { 364 ModifiablePage modifiablePage = (ModifiablePage) page; 365 modifiablePage.removeValue(OrganisationChartPageHandler.CONTENT_TYPE_DATA_NAME); 366 modifiablePage.removeValue(OrganisationChartPageHandler.PAGE_VISIBLE_DATA_NAME); 367 } 368 jcrPage.saveChanges(); 369 } 370 } 371 } 372 373 private void _notifyPageUpdated(Page page) 374 { 375 Map<String, Object> eventParams = new HashMap<>(); 376 eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page); 377 _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams)); 378 } 379 380 381 /** 382 * Gets information about organisation chart root status on the given. 383 * @param pageId The id of the page 384 * @return information about organisation chart root status on the given. 385 */ 386 @Callable (rights = "User_Directory_Right_Organisation_Chart_SetRoot", rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 387 public Map<String, Object> getRootPageInfo(String pageId) 388 { 389 Map<String, Object> result = new HashMap<>(); 390 391 Page page = _resolver.resolveById(pageId); 392 393 if (page.hasValue(OrganisationChartPageHandler.CONTENT_TYPE_DATA_NAME)) 394 { 395 result.put("isRoot", true); 396 result.put("contentType", page.getValue(OrganisationChartPageHandler.CONTENT_TYPE_DATA_NAME)); 397 result.put("pageVisible", page.getValue(OrganisationChartPageHandler.PAGE_VISIBLE_DATA_NAME, true)); 398 } 399 else 400 { 401 result.put("isRoot", false); 402 } 403 404 return result; 405 } 406}