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