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}