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